Compare commits

...
Author SHA1 Message Date
John Colvin fbb4dd6e1f chore: Move 'Order' classes into Model/Trading dir 2022-07-08 23:42:23 -04:00
John Colvin f977b0fdd0 fix (ui): Validate width/height before updating DragRectAdorner 2022-07-08 23:42:22 -04:00
John Colvin a3968bd35b ui: Add theme picker ui and stock dark theme 2022-07-08 23:42:22 -04:00
John Colvin 2b94a29963 chore: Clean up ui settings classes
* Move most settings files into new dir 'Model/Settings'
* Conform code style to our current practices
2022-07-08 14:44:57 -04:00
John Colvin 4b8abc2005 fix: Add null-check
A null-ref exception surfaced when running client w/ low bandwidth
2022-07-08 13:11:50 -04:00
John Colvin 2627264eea ui: Speed up expand/collapse animation in settings dialogs 2022-07-08 13:11:50 -04:00
John Colvin 2c7186275f chore (refactor): Move synth mgmt settings classes into sep files 2022-07-08 10:27:45 -04:00
John Colvin a4d8cd9dfc fix: Gather individual win settings on dispatcher thread
In practice, window-states live on their respective dispatcher threads.
Window settings factories shouldn't be expected to be thread-safe.
2022-07-07 02:35:06 -04:00
John Colvin ed4be2f1dd fix (vs ui): Don't clip vs time sched ui 2022-07-07 02:14:02 -04:00
John Colvin cd6d504f06 ui (vs grid settings): Constrain window height 2022-07-07 00:35:56 -04:00
John Colvin e68d37f24d build (client cpp): Remove ZERO_CHECK.vcxproj from spark_client.sln
The new cmake generates ZERO_CHECK.proj instead, which doesn't need to be
included in the sln
2022-07-06 10:05:21 -04:00
71 changed files with 4815 additions and 4463 deletions

No files matched your search

-10
View File
@@ -12,8 +12,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "sparkcmd", "src\client\spar
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_core", "src\client\spark_core\spark_core.csproj", "{6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}"
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZERO_CHECK", "build\ZERO_CHECK.vcxproj", "{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_sdk", "src\client\spark_sdk\spark_sdk.csproj", "{6963C314-83C4-4667-B0D0-C2F7AE208C29}"
ProjectSection(ProjectDependencies) = postProject
{9FA60215-26AF-3EB7-AB89-D4F4B126EF40} = {9FA60215-26AF-3EB7-AB89-D4F4B126EF40}
@@ -69,14 +67,6 @@ Global
{6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.Release|x64.Build.0 = Release|x64
{6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.RelWithDebInfo|x64.ActiveCfg = Release|x64
{6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.RelWithDebInfo|x64.Build.0 = Release|x64
{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}.Debug|x64.ActiveCfg = Debug|x64
{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}.Debug|x64.Build.0 = Debug|x64
{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64
{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}.MinSizeRel|x64.Build.0 = MinSizeRel|x64
{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}.Release|x64.ActiveCfg = Release|x64
{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}.Release|x64.Build.0 = Release|x64
{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64
{A1B7276B-8F81-3D58-A22E-870AF28DF9B9}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64
{6963C314-83C4-4667-B0D0-C2F7AE208C29}.Debug|x64.ActiveCfg = Debug|x64
{6963C314-83C4-4667-B0D0-C2F7AE208C29}.Debug|x64.Build.0 = Debug|x64
{6963C314-83C4-4667-B0D0-C2F7AE208C29}.MinSizeRel|x64.ActiveCfg = Release|x64
@@ -19,8 +19,9 @@ namespace mbts.analytics
/// with default inputs
public static VolSurfaceInput sync_input_expirations_to_master_list(VolSurfaceInput vs_input, IEnumerable<VolSurfaceExpiration> system_exp_list)
{
HashSet<VolSurfaceExpiration> vs_exp_map = new HashSet<VolSurfaceExpiration>(vs_input.surface_parameters.curve_parameters_map.Select(t => t.Item1)),
sys_exp_map = new HashSet<VolSurfaceExpiration>(system_exp_list);
var vs_exp_map = vs_input.surface_parameters.curve_parameters_map
.empty_if_null().Select(t => t.Item1).ToHashSet();
var sys_exp_map = system_exp_list.ToHashSet();
sys_exp_map.difference(
vs_exp_map,
out HashSet<VolSurfaceExpiration> deleted_exps,
@@ -674,7 +674,7 @@ namespace mbts.analytics {
var vol_time = VolSurfaceCoreFunctions.compute_vol_time(
vs_input, vs_standard_time.server_time_utc);
var exp_inp_data = vs_nonvol_input.get_option_exp_input();
foreach (var (exp, curve_params) in vs_input.surface_parameters.curve_parameters_map) {
foreach (var (exp, curve_params) in vs_input.surface_parameters.curve_parameters_map.empty_if_null()) {
var time_shift = DoubleHelper.CoerceNaN(curve_params.core_parameters.time_shift);
_compute_option_bizt(
vs_input, exp, vol_time, time_shift, out var biz_yrs, out var biz_days_per_yr);
@@ -1,31 +1,31 @@
using bts.utils;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
[ProtoContract]
class GlobalAlertSettings : IEquatable<GlobalAlertSettings> {
[ProtoMember(1)]
TradeAlert[] _trade_alerts;
public GlobalAlertSettings() { }
public GlobalAlertSettings(IEnumerable<TradeAlert> trade_alerts) =>
_trade_alerts = trade_alerts.ToArray();
public IReadOnlyList<TradeAlert> trade_alerts => _trade_alerts ?? Array.Empty<TradeAlert>();
public bool Equals(GlobalAlertSettings other) =>
other != null &&
trade_alerts.SequenceEqual(other.trade_alerts);
public override bool Equals(object obj) =>
Equals(obj as GlobalAlertSettings);
public override int GetHashCode() =>
EqualityComparer.auto_phase(trade_alerts);
}
}
using bts.utils;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
[ProtoContract]
class GlobalAlertSettings : IEquatable<GlobalAlertSettings> {
[ProtoMember(1)]
TradeAlert[] _trade_alerts;
public GlobalAlertSettings() { }
public GlobalAlertSettings(IEnumerable<TradeAlert> trade_alerts) =>
_trade_alerts = trade_alerts.ToArray();
public IReadOnlyList<TradeAlert> trade_alerts => _trade_alerts ?? Array.Empty<TradeAlert>();
public bool Equals(GlobalAlertSettings other) =>
other != null &&
trade_alerts.SequenceEqual(other.trade_alerts);
public override bool Equals(object obj) =>
Equals(obj as GlobalAlertSettings);
public override int GetHashCode() =>
EqualityComparer.auto_phase(trade_alerts);
}
}
@@ -1,88 +1,88 @@
using bts;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Composition;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.ui.settings {
public interface ISavedWindowFactory {
Task<IShellWindow> restore(WindowState window);
}
[Export(typeof(ISavedWindowFactory))]
public class WindowFactory : ISavedWindowFactory {
IShellWindowMigrationRule[] _compat;
[ImportingConstructor]
public WindowFactory([ImportMany] IShellWindowMigrationRule[] migration) {
_compat = migration;
}
[Import]
public ObservableCollection<IShellModule> modules { get; set; }
public Task<IShellWindow> restore(WindowState window) {
Log.Ui.info("Loading window '{0}'".fmt(window.title));
bool changed;
int count = 0;
do {
count++;
changed = apply_compatability_migrations(ref window);
} while (count < 100 && changed);
if (count == 100) {
Log.Persist.error("Error: Window state migration still changing after 100 iterations. Please check IShellWindowMigrationRule logic.");
}
return restore(window.owner, window.app_data, window.position);
}
/// When we apply the migration changes to a window we stop as soon as one "matches" and updates the state
/// Since it's possible that we have multiple levels of migration we run this function until the data stops changing (or 100 iterations)
/// ex: If change the data format twice from A to B to C then we would write 2 IShellWindowMigration instances.
/// Starting in state A one of the _compat tests changes to state B. Now we need to re-run
/// the migrations because state B will match a different rule and be converted to state C.
private bool apply_compatability_migrations(ref WindowState window) {
foreach (var test in _compat) {
try {
if (test.apply(ref window)) {
return true;
}
} catch (Exception e) {
Log.Persist.error("Exception running compat rule {0}.\n{1}".fmt(test.GetType().Name, e.ToString()));
}
}
return false;
}
private async Task<IShellWindow> restore(string owner_name, byte[] state, WindowPosition windowPosition) {
IShellModule owner = modules.empty_if_null().SingleOrDefault(m => m.key() == owner_name);
if (owner == null) {
Log.Ui.warn("Could not locate module '{0}'".fmt(owner_name));
return null;
}
try {
Task<IShellWindow> task = owner.create(new MemoryStream(state));
if (task != null) {
IShellWindow window = await task;
if (window != null) {
window.set_start_position(windowPosition);
Log.Ui.info($"Restoring window {owner_name} - {window.diagnostic_name()}.");
}
return window;
}
} catch (Exception e) {
Log.Ui.error("Failure to re-create window for module {0}. Error: {1}".fmt(owner_name, e.Message));
}
return null;
}
}
}
using bts;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Composition;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.ui.settings {
public interface ISavedWindowFactory {
Task<IShellWindow> restore(WindowState window);
}
[Export(typeof(ISavedWindowFactory))]
public class WindowFactory : ISavedWindowFactory {
IShellWindowMigrationRule[] _compat;
[ImportingConstructor]
public WindowFactory([ImportMany] IShellWindowMigrationRule[] migration) {
_compat = migration;
}
[Import]
public ObservableCollection<IShellModule> modules { get; set; }
public Task<IShellWindow> restore(WindowState window) {
Log.Ui.info("Loading window '{0}'".fmt(window.title));
bool changed;
int count = 0;
do {
count++;
changed = apply_compatability_migrations(ref window);
} while (count < 100 && changed);
if (count == 100) {
Log.Persist.error("Error: Window state migration still changing after 100 iterations. Please check IShellWindowMigrationRule logic.");
}
return restore(window.owner, window.app_data, window.position);
}
/// When we apply the migration changes to a window we stop as soon as one "matches" and updates the state
/// Since it's possible that we have multiple levels of migration we run this function until the data stops changing (or 100 iterations)
/// ex: If change the data format twice from A to B to C then we would write 2 IShellWindowMigration instances.
/// Starting in state A one of the _compat tests changes to state B. Now we need to re-run
/// the migrations because state B will match a different rule and be converted to state C.
private bool apply_compatability_migrations(ref WindowState window) {
foreach (var test in _compat) {
try {
if (test.apply(ref window)) {
return true;
}
} catch (Exception e) {
Log.Persist.error("Exception running compat rule {0}.\n{1}".fmt(test.GetType().Name, e.ToString()));
}
}
return false;
}
private async Task<IShellWindow> restore(string owner_name, byte[] state, WindowPosition windowPosition) {
IShellModule owner = modules.empty_if_null().SingleOrDefault(m => m.key() == owner_name);
if (owner == null) {
Log.Ui.warn("Could not locate module '{0}'".fmt(owner_name));
return null;
}
try {
Task<IShellWindow> task = owner.create(new MemoryStream(state));
if (task != null) {
IShellWindow window = await task;
if (window != null) {
window.set_start_position(windowPosition);
Log.Ui.info($"Restoring window {owner_name} - {window.diagnostic_name()}.");
}
return window;
}
} catch (Exception e) {
Log.Ui.error("Failure to re-create window for module {0}. Error: {1}".fmt(owner_name, e.Message));
}
return null;
}
}
}
@@ -1,86 +1,113 @@
using bts;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Composition;
using System.IO;
using System.Linq;
namespace mbts.ui.settings {
public interface ISettingsCollector {
// Get the save state for every open window
IEnumerable<WindowState> get_window_state();
WindowState get_window_state(IShellWindow window);
IEnumerable<ModuleState> get_module_state();
WindowPosition get_launcher_state();
}
[Export(typeof(ISettingsCollector))]
class SettingsCollector : ISettingsCollector {
WindowManager _wm;
ILauncher _launcher;
[ImportingConstructor]
public SettingsCollector(WindowManager wm, ILauncher launcher) {
_wm = wm;
_launcher = launcher;
}
[Import]
public ObservableCollection<IShellModule> modules { get; set; }
IWindowsInterop _interop => _wm;
public IEnumerable<WindowState> get_window_state() {
foreach (var window in _wm.list_windows()) {
// Check that the window is still open since we might be iterating asyncronously
if (_wm.contains(window)) {
var state = get_window_state(window);
if (state != null) {
yield return state;
}
}
}
}
public IEnumerable<ModuleState> get_module_state() {
//Note: I'm assuming that the list of modules is stable and I don't have to protect against it changing here
foreach (var module in modules.OfTypeSafe<IPersistShellModule>()) {
MemoryStream out_stream = new MemoryStream();
module.get_state(out_stream);
yield return new ModuleState() {
name = module.key(),
app_data = out_stream.ToArray(),
};
}
}
public WindowPosition get_launcher_state() {
return _launcher.get_position();
}
public WindowState get_window_state(IShellWindow window) {
try {
if (window == null || window.Owner == null) return null;
MemoryStream out_stream = new MemoryStream();
window.GetState(out_stream);
var pos = _interop.GetPosition(window);
var state = new WindowState() {
owner = window.Owner.key(),
app_data = out_stream.ToArray(),
position = pos,
title = string.IsNullOrEmpty(pos.custom_title) ? window.Title : pos.custom_title
};
return state;
} catch (Exception e) {
Log.Ui.error("Error saving window state for type {0}.\n {1}".fmt(window.GetType(), e.Message));
}
return null;
}
}
}
using bts;
using mbts.ui.shell;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Composition;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Threading;
namespace mbts.ui.settings {
public interface ISettingsCollector {
// Get the save state for every open window
Task<List<WindowState>> get_window_state();
Task<WindowState> get_window_state(IShellWindow window);
IEnumerable<ModuleState> get_module_state();
WindowPosition get_launcher_state();
}
[Export(typeof(ISettingsCollector))]
class SettingsCollector : ISettingsCollector {
WindowManager _wm;
ILauncher _launcher;
[ImportingConstructor]
public SettingsCollector(WindowManager wm, ILauncher launcher) {
_wm = wm;
_launcher = launcher;
}
[Import]
public ObservableCollection<IShellModule> modules { get; set; }
[Import]
public IShellThreadFactory thread_factory { get; set; }
IWindowsInterop _interop => _wm;
public async Task<List<WindowState>> get_window_state() {
var windows = _wm.list_windows();
var rv = new List<WindowState>(windows.Count);
foreach (var window in windows) {
// Check that the window is still open since we might be iterating asyncronously
if (_wm.contains(window)) {
var state = await get_window_state(window);
if (state != null) {
rv.Add(state);
}
}
}
return rv;
}
public IEnumerable<ModuleState> get_module_state() {
//Note: I'm assuming that the list of modules is stable and I don't have to protect against it changing here
foreach (var module in modules.OfTypeSafe<IPersistShellModule>()) {
MemoryStream out_stream = new MemoryStream();
module.get_state(out_stream);
yield return new ModuleState() {
name = module.key(),
app_data = out_stream.ToArray(),
};
}
}
public WindowPosition get_launcher_state() {
return _launcher.get_position();
}
public async Task<WindowState> get_window_state(IShellWindow window) {
try {
if (window == null || window.Owner == null) return null;
Dispatcher dispatcher = null;
if (window is ISlowWindow) {
var tf = thread_factory;
if (tf is not null) {
var thread_id = (window as ISharedWindow)?.thread_id;
dispatcher = tf.get_dispatcher(thread_id);
if (dispatcher is null) {
Log.Gen.error(
$"Unable to get window state for '{window.Title}.' " +
$"Didn't find dispatcher for thread {thread_id}.");
return null;
}
}
}
var pos = _interop.GetPosition(window);
var state = new WindowState() {
owner = window.Owner.key(),
app_data = dispatcher is null ?
(await dispatcher.InvokeAsync(serialize)) : serialize(),
position = pos,
title = string.IsNullOrEmpty(pos.custom_title) ? window.Title : pos.custom_title
};
return state;
byte[] serialize() {
using var ms = new MemoryStream();
window.GetState(ms);
return ms.ToArray();
}
} catch (Exception e) {
Log.Ui.error("Error saving window state for type {0}.\n {1}".fmt(window.GetType(), e.Message));
}
return null;
}
}
}
@@ -1,95 +1,95 @@
using bts.shell.pnl;
using bts.utils;
using mbts.ui.settings;
using System;
using System.Collections.Generic;
namespace bts.settings {
interface ISettingsMigrationRule<T> {
bool apply(T settings, out T new_settings);
}
// Migration rules of type ISettingsMigrationRule<T>, where 'T' is a common type, such as GridSettings,
// should be exposed through explicitly named interfaces that inherit ISettingsMigrationRule<T>.
// This avoids magic const names in the MEF import/export contracts, which would otherwise be needed to
// determine the various windows to which such rules apply.
interface IFillsGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
interface ISyntheticManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IReconcileReportMigrationRule : ISettingsMigrationRule<GridSettings> { }
static class SettingsMigrationHelper {
public static bool apply<T>(T settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, out T new_settings) {
bool applied = false;
foreach (var r in migration_rules) {
if (r.apply(settings, out var migrated)) {
settings = migrated;
applied = true;
}
}
new_settings = applied ? settings : default;
return applied;
}
public static void apply_quiet<T>(ref T settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules) {
if (apply(settings, migration_rules, out var new_settings))
settings = new_settings;
}
public static ISettingsMigrationRule<T> merge<T>(this IEnumerable<ISettingsMigrationRule<T>> rules) =>
new MergeWrapper<T>(rules);
public static ISettingsMigrationRule<TOuter> wrap<TInner, TOuter>(this ISettingsMigrationRule<TInner> rule, IImmutableProperty<TOuter, TInner> propi) =>
new SelectorWrapper<TInner, TOuter>(rule, propi.get, propi.set);
public static ISettingsMigrationRule<TOuter> wrap<TInner, TOuter>(this ISettingsMigrationRule<TInner> rule, Func<TOuter, TInner> get, Func<TOuter, TInner, TOuter> set) =>
new SelectorWrapper<TInner, TOuter>(rule, get, set);
class MergeWrapper<T> : ISettingsMigrationRule<T> {
readonly IEnumerable<ISettingsMigrationRule<T>> _rules;
public MergeWrapper(IEnumerable<ISettingsMigrationRule<T>> rules) =>
_rules = rules;
public bool apply(T settings, out T new_settings) {
bool applied = false;
foreach (var r in _rules) {
if (r.apply(settings, out var migrated)) {
settings = migrated;
applied = true;
}
}
new_settings = applied ? settings : default;
return applied;
}
}
class SelectorWrapper<TInner, TOuter> : ISettingsMigrationRule<TOuter> {
readonly ISettingsMigrationRule<TInner> _rule;
readonly Func<TOuter, TInner> _get;
readonly Func<TOuter, TInner, TOuter> _set;
public SelectorWrapper(ISettingsMigrationRule<TInner> rule, Func<TOuter, TInner> get, Func<TOuter, TInner, TOuter> set) {
_rule = rule;
_get = get;
_set = set;
}
public bool apply(TOuter settings, out TOuter new_settings) {
if (settings != null) {
var ival = _get(settings);
if (_rule.apply(ival, out var new_ival)) {
new_settings = _set(settings, new_ival);
return true;
}
}
new_settings = default;
return false;
}
}
}
}
using bts.shell.pnl;
using bts.utils;
using mbts.ui.settings;
using System;
using System.Collections.Generic;
namespace bts.settings {
interface ISettingsMigrationRule<T> {
bool apply(T settings, out T new_settings);
}
// Migration rules of type ISettingsMigrationRule<T>, where 'T' is a common type, such as GridSettings,
// should be exposed through explicitly named interfaces that inherit ISettingsMigrationRule<T>.
// This avoids magic const names in the MEF import/export contracts, which would otherwise be needed to
// determine the various windows to which such rules apply.
interface IFillsGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
interface ISyntheticManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
interface IReconcileReportMigrationRule : ISettingsMigrationRule<GridSettings> { }
static class SettingsMigrationHelper {
public static bool apply<T>(T settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, out T new_settings) {
bool applied = false;
foreach (var r in migration_rules) {
if (r.apply(settings, out var migrated)) {
settings = migrated;
applied = true;
}
}
new_settings = applied ? settings : default;
return applied;
}
public static void apply_quiet<T>(ref T settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules) {
if (apply(settings, migration_rules, out var new_settings))
settings = new_settings;
}
public static ISettingsMigrationRule<T> merge<T>(this IEnumerable<ISettingsMigrationRule<T>> rules) =>
new MergeWrapper<T>(rules);
public static ISettingsMigrationRule<TOuter> wrap<TInner, TOuter>(this ISettingsMigrationRule<TInner> rule, IImmutableProperty<TOuter, TInner> propi) =>
new SelectorWrapper<TInner, TOuter>(rule, propi.get, propi.set);
public static ISettingsMigrationRule<TOuter> wrap<TInner, TOuter>(this ISettingsMigrationRule<TInner> rule, Func<TOuter, TInner> get, Func<TOuter, TInner, TOuter> set) =>
new SelectorWrapper<TInner, TOuter>(rule, get, set);
class MergeWrapper<T> : ISettingsMigrationRule<T> {
readonly IEnumerable<ISettingsMigrationRule<T>> _rules;
public MergeWrapper(IEnumerable<ISettingsMigrationRule<T>> rules) =>
_rules = rules;
public bool apply(T settings, out T new_settings) {
bool applied = false;
foreach (var r in _rules) {
if (r.apply(settings, out var migrated)) {
settings = migrated;
applied = true;
}
}
new_settings = applied ? settings : default;
return applied;
}
}
class SelectorWrapper<TInner, TOuter> : ISettingsMigrationRule<TOuter> {
readonly ISettingsMigrationRule<TInner> _rule;
readonly Func<TOuter, TInner> _get;
readonly Func<TOuter, TInner, TOuter> _set;
public SelectorWrapper(ISettingsMigrationRule<TInner> rule, Func<TOuter, TInner> get, Func<TOuter, TInner, TOuter> set) {
_rule = rule;
_get = get;
_set = set;
}
public bool apply(TOuter settings, out TOuter new_settings) {
if (settings != null) {
var ival = _get(settings);
if (_rule.apply(ival, out var new_ival)) {
new_settings = _set(settings, new_ival);
return true;
}
}
new_settings = default;
return false;
}
}
}
}
@@ -1,171 +1,153 @@
using mbts.spark;
using mbts.utils;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
namespace mbts.persist {
interface IWindowSettingsManager<T> where T : class {
T instance_settings { get; set; }
T default_settings { get; }
event Action<T> instance_settings_changed;
IObservable<T> subscribe_default();
Task save_default_settings(T value);
IWindowSettingsManagerNonShared<T> as_non_shared();
}
/// <summary>
/// An implementation of IWindowSettingsManager that uses a clone of the actual default settings;
/// provides a convenient way to avoid problems that arise from sharing a single default settings object among multiple window instances.
/// </summary>
interface IWindowSettingsManagerNonShared<T> : IWindowSettingsManager<T>, IDisposable where T : class { }
class WindowSettingsManager<T> : IWindowSettingsManager<T> where T : class {
readonly UserSettingsHub _hub;
readonly Func<UserSettings, T> _get;
readonly Func<UserSettings, T, UserSettings> _set;
T _instance_settings;
public WindowSettingsManager(UserSettingsHub hub, T instance_settings, Func<UserSettings, T> get, Func<UserSettings, T, UserSettings> set) {
_hub = hub;
_instance_settings = instance_settings;
_get = get;
_set = set;
}
public T instance_settings {
get => _instance_settings;
set {
_instance_settings = value;
instance_settings_changed?.Invoke(value);
}
}
public T default_settings => _get(_hub.current);
public event Action<T> instance_settings_changed;
public async Task save_default_settings(T value) =>
await _hub.save_default(_set(_hub.current, value));
public IObservable<T> subscribe_default() =>
_hub.subscribe_default_settings().Select(_get).DistinctUntilChanged();
public IWindowSettingsManagerNonShared<T> as_non_shared() =>
new NonSharedWrapper(this);
class NonSharedWrapper : IWindowSettingsManagerNonShared<T> {
readonly IWindowSettingsManager<T> _wsm;
readonly BehaviorSubject<T> _default_subject;
IDisposable _dispose;
public NonSharedWrapper(IWindowSettingsManager<T> wsm) {
_wsm = wsm;
_default_subject = new BehaviorSubject<T>(_wsm.default_settings);
_dispose = wsm.subscribe_default().Subscribe(s =>
_default_subject.OnNext(default_settings = MiscUtils.clone_proto(s)));
}
public T instance_settings {
get => _wsm.instance_settings;
set => _wsm.instance_settings = value;
}
public T default_settings { get; private set; }
public event Action<T> instance_settings_changed {
add => _wsm.instance_settings_changed += value;
remove => _wsm.instance_settings_changed -= value;
}
public async Task save_default_settings(T value) =>
await _wsm.save_default_settings(value);
public IObservable<T> subscribe_default() =>
_default_subject.AsObservable();
public IWindowSettingsManagerNonShared<T> as_non_shared() =>
new NonSharedWrapper(_wsm);
public void Dispose() => Disposable.dispose(ref _dispose);
}
public class DisconnectedImpl : IWindowSettingsManager<T> {
readonly BehaviorSubject<T> _default_settings_subject;
T _instance_settings, _default_settings;
public DisconnectedImpl(T settings) : this(settings, settings) { }
public DisconnectedImpl(T instance_settings, T default_settings) {
_instance_settings = instance_settings;
_default_settings = default_settings;
_default_settings_subject = new BehaviorSubject<T>(default_settings);
}
public T instance_settings {
get => _instance_settings;
set {
if (!Equals(_instance_settings, value)) {
_instance_settings = value;
instance_settings_changed?.Invoke(value);
}
}
}
public T default_settings {
get => _default_settings;
set {
if (!Equals(_default_settings, _default_settings)) {
_default_settings = value;
_default_settings_subject.OnNext(value);
}
}
}
public event Action<T> instance_settings_changed;
public IWindowSettingsManagerNonShared<T> as_non_shared() => new NonSharedWrapper(this);
public Task save_default_settings(T value) {
default_settings = value;
return Task.CompletedTask;
}
public IObservable<T> subscribe_default() => _default_settings_subject;
}
}
static class WindowSettingsManagerExtensions {
/// <summary>
/// Returns an observable sequence of the settings object used by a particular window.
/// <para/>Note: 'Instance' settings take priority over 'default' settings. While the former is present, any changes to the latter will be ignored.
/// </summary>
public static IObservable<T> subscribe_current<T>(this IWindowSettingsManager<T> wsm) where T : class =>
Observable.CombineLatest(
Observable.FromEvent<Action<T>, T>(cb => wsm.instance_settings_changed += cb, cb => wsm.instance_settings_changed -= cb)
.StartWith(wsm.instance_settings),
wsm.subscribe_default(),
(i, d) => i ?? d
).Where(v => v != null)
.DistinctUntilChanged();
public static IObservable<T> subscribe_current<T>(this IWindowSettingsManager<T> wsm, Func<(T defaults, T instance), T> merge_settings) where T : class, IDefaultableSettings {
return Observable.Create<T>(o => {
IDisposable defaults_sub = wsm.subscribe_default().Subscribe(_ => o.OnNext(get_current()));
wsm.instance_settings_changed += on_instance_settings_changed;
return Disposable.Create(() => {
defaults_sub.Dispose();
wsm.instance_settings_changed -= on_instance_settings_changed;
});
void on_instance_settings_changed(T _) =>
o.OnNext(get_current());
});
T get_current() {
var inst = wsm.instance_settings;
if (inst == null) return wsm.default_settings;
if (inst.use_defaults) return merge_settings((wsm.default_settings, inst));
return inst;
}
}
public static T current_settings<T>(this IWindowSettingsManager<T> wsm) where T : class =>
wsm.instance_settings ?? wsm.default_settings;
}
}
using mbts.spark;
using mbts.utils;
using System;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
namespace mbts.persist {
interface IWindowSettingsManager<T> where T : class {
T instance_settings { get; set; }
T default_settings { get; }
event Action<T> instance_settings_changed;
IObservable<T> subscribe_default();
Task save_default_settings(T value);
IWindowSettingsManagerNonShared<T> as_non_shared();
}
/// <summary>
/// An implementation of IWindowSettingsManager that uses a clone of the actual default settings;
/// provides a convenient way to avoid problems that arise from sharing a single default settings object among multiple window instances.
/// </summary>
interface IWindowSettingsManagerNonShared<T> : IWindowSettingsManager<T>, IDisposable where T : class { }
class WindowSettingsManager<T> : IWindowSettingsManager<T> where T : class {
readonly UserSettingsHub _hub;
readonly Func<UserSettings, T> _get;
readonly Func<UserSettings, T, UserSettings> _set;
T _instance_settings;
public WindowSettingsManager(UserSettingsHub hub, T instance_settings, Func<UserSettings, T> get, Func<UserSettings, T, UserSettings> set) {
_hub = hub;
_instance_settings = instance_settings;
_get = get;
_set = set;
hub.request_clear_instance_settings += () => instance_settings = null;
}
public T instance_settings {
get => _instance_settings;
set {
_instance_settings = value;
instance_settings_changed?.Invoke(value);
}
}
public T default_settings => _get(_hub.current);
public event Action<T> instance_settings_changed;
public async Task save_default_settings(T value) =>
await _hub.save_default(_set(_hub.current, value));
public IObservable<T> subscribe_default() =>
_hub.subscribe_default_settings().Select(_get).DistinctUntilChanged();
public IWindowSettingsManagerNonShared<T> as_non_shared() =>
new NonSharedWrapper(this);
class NonSharedWrapper : IWindowSettingsManagerNonShared<T> {
readonly IWindowSettingsManager<T> _wsm;
readonly BehaviorSubject<T> _default_subject;
IDisposable _dispose;
public NonSharedWrapper(IWindowSettingsManager<T> wsm) {
_wsm = wsm;
_default_subject = new BehaviorSubject<T>(_wsm.default_settings);
_dispose = wsm.subscribe_default().Subscribe(s =>
_default_subject.OnNext(default_settings = MiscUtils.clone_proto(s)));
}
public T instance_settings {
get => _wsm.instance_settings;
set => _wsm.instance_settings = value;
}
public T default_settings { get; private set; }
public event Action<T> instance_settings_changed {
add => _wsm.instance_settings_changed += value;
remove => _wsm.instance_settings_changed -= value;
}
public async Task save_default_settings(T value) =>
await _wsm.save_default_settings(value);
public IObservable<T> subscribe_default() =>
_default_subject.AsObservable();
public IWindowSettingsManagerNonShared<T> as_non_shared() =>
new NonSharedWrapper(_wsm);
public void Dispose() => Disposable.dispose(ref _dispose);
}
public class DisconnectedImpl : IWindowSettingsManager<T> {
readonly BehaviorSubject<T> _default_settings_subject;
T _instance_settings, _default_settings;
public DisconnectedImpl(T settings) : this(settings, settings) { }
public DisconnectedImpl(T instance_settings, T default_settings) {
_instance_settings = instance_settings;
_default_settings = default_settings;
_default_settings_subject = new BehaviorSubject<T>(default_settings);
}
public T instance_settings {
get => _instance_settings;
set {
if (!Equals(_instance_settings, value)) {
_instance_settings = value;
instance_settings_changed?.Invoke(value);
}
}
}
public T default_settings {
get => _default_settings;
set {
if (!Equals(_default_settings, _default_settings)) {
_default_settings = value;
_default_settings_subject.OnNext(value);
}
}
}
public event Action<T> instance_settings_changed;
public IWindowSettingsManagerNonShared<T> as_non_shared() => new NonSharedWrapper(this);
public Task save_default_settings(T value) {
default_settings = value;
return Task.CompletedTask;
}
public IObservable<T> subscribe_default() => _default_settings_subject;
}
}
static class WindowSettingsManagerExtensions {
/// <summary>
/// Returns an observable sequence of the settings object used by a particular window.
/// <para/>Note: 'Instance' settings take priority over 'default' settings. While the former is present, any changes to the latter will be ignored.
/// </summary>
public static IObservable<T> subscribe_current<T>(this IWindowSettingsManager<T> wsm) where T : class =>
Observable.CombineLatest(
Observable.FromEvent<Action<T>, T>(cb => wsm.instance_settings_changed += cb, cb => wsm.instance_settings_changed -= cb)
.StartWith(wsm.instance_settings),
wsm.subscribe_default(),
(i, d) => i ?? d
).Where(v => v != null)
.DistinctUntilChanged();
public static T current_settings<T>(this IWindowSettingsManager<T> wsm) where T : class =>
wsm.instance_settings ?? wsm.default_settings;
}
}
@@ -0,0 +1,172 @@
#nullable enable
namespace mbts.persist {
using bts;
using bts.api.core;
using bts.api.persistence;
using bts.spark;
using Spark.Core.Protobuf;
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
public class SettingsPersistenceModel : IPersist, IUserPersist {
readonly PersistenceService _persist;
readonly ErrorReporter _errors;
readonly string _user;
public SettingsPersistenceModel(
string user, PersistenceService persist, ErrorReporter errors) {
_user = user;
_persist = persist;
_errors = errors;
}
public Task set<T>(string ns, string key, T value) {
return _persist.store_async(new StoreRequest() {
Ns = _user_ns(ns),
Record = {
new Record() {
Key = key,
Value = ProtobufSerializationHelper.serialize_to_byte_string(value)
}
}
});
}
public Task clear(string ns, string key) {
return _persist.remove_async(new RemoveRequest() {
Ns = _user_ns(ns),
Key = { key }
});
}
public async Task<T?> get<T>(string ns, string key) {
var ret = await _persist.retrieve_async(new RetrieveRequest() {
Ns = _user_ns(ns),
Key = { key }
});
_check_error(ret, ns, key, out var empty);
if (empty) {
return default;
}
return ProtobufSerializationHelper.deserialize<T>(ret.value.Record.Single().Value);
}
public async Task<T[]> get_many<T>(string ns, string key) {
var ret = await _persist.retrieve_async(new RetrieveRequest() {
Ns = _user_ns(ns),
Key = { key }
});
_check_error(ret, ns, key, out bool empty);
if (empty) {
return Array.Empty<T>();
}
return ProtobufSerializationHelper.deserialize_many<T>(
ret.value.Record.Single().Value.ToByteArray());
}
public async Task<IEnumerable<PersistResult<T>>> query<T>(string ns, string pattern) {
var regex = new Regex(pattern);
var results = await _persist.retrieve_all_async(
new RetrieveAllRequest() {
Ns = _user_ns(ns)
});
_check_error(results, ns, string.Empty, out bool empty);
if (empty) {
return Enumerable.Empty<PersistResult<T>>();
}
return results.value.Record
.Where(r => regex.IsMatch(r.Key))
.Select(r => new PersistResult<T>() {
user = _user,
ns = ns,
key = r.Key,
value = ProtobufSerializationHelper.deserialize<T>(r.Value)
}).ToList();
}
public IDisposable subscribe<T>(
string ns, string pattern, Action<IEnumerable<PersistResult<T>>> callback) =>
subscribe<T>(ns, pattern).Subscribe(callback);
public IObservable<IEnumerable<PersistResult<T>>> subscribe<T>(string ns, string pattern) {
var ctx = SynchronizationContext.Current ??
throw new InvalidOperationException("Current SynchronizationContext is null");
var re = new Regex(pattern);
return _persist
.subscribe_ns(_user_ns(ns))
.Select(update =>
update.added
.Where(item => re.IsMatch(item.Key))
.Select(item => new PersistResult<T>() {
user = _user,
ns = ns,
key = item.Key,
value = ProtobufSerializationHelper.deserialize<T>(item.Value)
}))
.SubscribeOn(ctx);
}
public void Dispose() =>
GC.SuppressFinalize(this);
string _user_ns(string ns) =>
$"{_user}#{ns}";
void _check_error(Result<RetrieveResponse> result, string ns, string key, out bool empty) {
if (result.success) {
switch (result.value.Result) {
case RetrieveResponse.Types.Result.EmptyNs:
case RetrieveResponse.Types.Result.NoSuchNs:
case RetrieveResponse.Types.Result.NoSuchKey:
empty = true;
return;
case RetrieveResponse.Types.Result.EmptyKey:
case RetrieveResponse.Types.Result.DuplicateKey:
_errors?.report_error(
$"Invalid Key {result} '{key}'",
"Requested Invalid Settings Key",
MessageBoxImage.Information);
empty = true;
return;
default:
case RetrieveResponse.Types.Result.Ok:
empty = false;
return;
}
} else {
_errors?.report_error(
$"There was an error communicating with the settings server: {result.error_message}",
"Failed to retrieve settings from the server");
empty = true;
}
}
}
[Export(typeof(IPersist))]
public class UISettings : SettingsPersistenceModel {
[ImportingConstructor]
public UISettings([Import("settings")] PersistenceService persist, ErrorReporter err)
: base("all_users", persist, err) { }
}
[Export(typeof(IUserPersist))]
public class UserUISettings : SettingsPersistenceModel {
public UserUISettings(
uint user_id, [Import("settings")] PersistenceService persist, ErrorReporter err)
: base($"User {user_id}", persist, err) { }
[ImportingConstructor]
public UserUISettings(
Lazy<UserTuple> user, [Import("settings")] PersistenceService persist, ErrorReporter err)
: this(user.Value.id, persist, err) { }
}
}
@@ -0,0 +1,44 @@
#nullable enable
namespace mbts.spark {
using bts.utils;
using System;
interface ISettingsProfile<T> {
uint id { get; }
string? name { get; }
T? value { get; }
SecurityCriteria? security_criteria { get; }
}
readonly struct SettingsProfile<T> : IEquatable<SettingsProfile<T>> where T : class, IEquatable<T> {
public static readonly Func<SettingsProfile<T>, uint> key_fn = ns => ns.id;
public readonly uint id;
public readonly string? name;
public readonly T? value;
public readonly SecurityCriteria? security_criteria;
public SettingsProfile(uint id, string? name, T? value, SecurityCriteria? security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
public SettingsProfile<T> with(uint? id = null, string? name = null, T? value = null, SecurityCriteria? security_criteria = null) =>
new(id ?? this.id, name ?? this.name, value ?? this.value, security_criteria ?? this.security_criteria);
public bool Equals(SettingsProfile<T> other) =>
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object? obj) =>
obj is SettingsProfile<T> other && Equals(other);
public override int GetHashCode() =>
id.GetHashCode();
}
}
@@ -1,73 +1,73 @@
using mbts.persist;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.ui.tests {
// Stub persistance store for shell unit tests
public class TestPersist : IPersist, IUserPersist {
public Dictionary<string, Dictionary<string, object>> Values = new Dictionary<string, Dictionary<string, object>>();
public Task set<T>(string ns, string key, T value) {
TaskCompletionSource<object> source = new TaskCompletionSource<object>();
try {
Values[ns][key] = value;
source.TrySetResult(null);
} catch (Exception e) {
source.TrySetException(e);
}
return source.Task;
}
public Task clear(string ns, string key) {
TaskCompletionSource<object> source = new TaskCompletionSource<object>();
try {
Values[ns].Remove(key);
source.TrySetResult(null);
} catch (Exception e) {
source.TrySetException(e);
}
return source.Task;
}
public Task<T> get<T>(string ns, string key) {
TaskCompletionSource<T> source = new TaskCompletionSource<T>();
try {
T result = (T)Values[ns][key];
source.TrySetResult(result);
} catch (Exception e) {
source.TrySetException(e);
}
return source.Task;
}
public Task<T[]> get_many<T>(string ns, string key) {
var source = new TaskCompletionSource<T[]>();
try {
source.TrySetResult((T[])Values[ns][key]);
} catch (Exception e) {
source.TrySetException(e);
}
return source.Task;
}
public async Task<IEnumerable<Tuple<T, DateTime>>> get_history<T>(string ns, string key) {
var res = await get<T>(ns, key);
return new Tuple<T, DateTime>[] { Tuple.Create(res, DateTime.Today) };
}
public Task<IEnumerable<PersistResult<T>>> query<T>(string ns, string pattern) {
throw new NotImplementedException();
}
public IDisposable subscribe<T>(string ns, string pattern, Action<IEnumerable<PersistResult<T>>> callback) {
throw new NotImplementedException();
}
public void Dispose() { }
}
}
using mbts.persist;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.ui.tests {
// Stub persistance store for shell unit tests
public class TestPersist : IPersist, IUserPersist {
public Dictionary<string, Dictionary<string, object>> Values = new Dictionary<string, Dictionary<string, object>>();
public Task set<T>(string ns, string key, T value) {
TaskCompletionSource<object> source = new TaskCompletionSource<object>();
try {
Values[ns][key] = value;
source.TrySetResult(null);
} catch (Exception e) {
source.TrySetException(e);
}
return source.Task;
}
public Task clear(string ns, string key) {
TaskCompletionSource<object> source = new TaskCompletionSource<object>();
try {
Values[ns].Remove(key);
source.TrySetResult(null);
} catch (Exception e) {
source.TrySetException(e);
}
return source.Task;
}
public Task<T> get<T>(string ns, string key) {
TaskCompletionSource<T> source = new TaskCompletionSource<T>();
try {
T result = (T)Values[ns][key];
source.TrySetResult(result);
} catch (Exception e) {
source.TrySetException(e);
}
return source.Task;
}
public Task<T[]> get_many<T>(string ns, string key) {
var source = new TaskCompletionSource<T[]>();
try {
source.TrySetResult((T[])Values[ns][key]);
} catch (Exception e) {
source.TrySetException(e);
}
return source.Task;
}
public async Task<IEnumerable<Tuple<T, DateTime>>> get_history<T>(string ns, string key) {
var res = await get<T>(ns, key);
return new Tuple<T, DateTime>[] { Tuple.Create(res, DateTime.Today) };
}
public Task<IEnumerable<PersistResult<T>>> query<T>(string ns, string pattern) {
throw new NotImplementedException();
}
public IDisposable subscribe<T>(string ns, string pattern, Action<IEnumerable<PersistResult<T>>> callback) {
throw new NotImplementedException();
}
public void Dispose() { }
}
}
@@ -1,195 +1,242 @@
using bts.shell.pnl;
using Google.Protobuf.WellKnownTypes;
using mbts.ui.settings;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
namespace mbts.spark {
interface IDefaultableSettings {
bool use_defaults { get; set; }
}
/// <summary>
/// The settings Mothership. All window - and any other future user based - settings should have a corresponding default member here.
/// </summary>
[ProtoContract]
class UserSettings {
public static UserSettings factory_defaults =>
new UserSettings {
_ladder = LadderSettings.Default,
_notification = NotificationSettings.Default,
_market_view_grid = bts.shell.market_view.MarketViewSettings.DefaultGridSettings,
_fills_grid = bts.shell.fills.FillsSettings.DefaultGridSettings,
_time_and_sales_grid = bts.shell.time_and_sales.TimeAndSalesSettings.DefaultGridSettings,
_synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings,
_reconcile_report_grid = Spark.Trading.ReconcileReportSettings.Default().grid,
_order_book_grid = bts.shell.orderbook.OrderBookStartupData.DefaultGrid
};
[ProtoMember(1)]
LadderSettings _ladder;
[ProtoMember(11)]
readonly LadderSettingsProfile[] _ladder_profiles;
/// When Profiles Are Deleted, ladders that were using that profile shall copy the profile's settings to a local profile.
/// Therefor:
/// Deleted profile ids must never be reused.
/// If they were reused, consider an unloaded workspace with ladders that were using the deleted profile.
/// If such a workspace was reloaded after a new profile was saved with the deleted one's id, the ladders using that deleted profile would suddenly be using the new profile.
/// To Prevent IDs From Being Recycled:
/// The last profile id is stored in the protobuf (<see cref="_last_ladder_profile_id"/>).
/// Anytime a profile is added, it shall use <see cref="next_ladder_profile_id"/>.
[ProtoMember(12)]
uint _last_ladder_profile_id;
// removing NotificationSettings these from proto-serialization, since they are disabled by default for now.
// This way, when they are enabled, there won't be any backwards compatability issues to worry about, as no clients will have persisted them yet.
NotificationSettings _notification;
[ProtoMember(5)]
GridSettings _market_view_grid;
[ProtoMember(6)]
GridSettings _time_and_sales_grid;
[ProtoMember(7)]
GridSettings _fills_grid;
[ProtoMember(8)]
GridSettings _synthetic_management_grid;
[ProtoMember(14)]
GridSettings _reconcile_report_grid;
[ProtoMember(9)]
bts.shell.orderbook.OrderBookGridSettings _order_book_grid;
[ProtoMember(10)]
PNLSettings _pnl;
[ProtoMember(13)]
GlobalAlertSettings _alerts;
public UserSettings() { }
public UserSettings(LadderSettingsProfile[] ladder_profiles) =>
_ladder_profiles = ladder_profiles;
public LadderSettings ladder => _ladder ?? (_ladder = LadderSettings.Default);
public IReadOnlyList<LadderSettingsProfile> ladder_profiles => _ladder_profiles ?? Array.Empty<LadderSettingsProfile>();
public uint next_ladder_profile_id => _last_ladder_profile_id + 1;
public NotificationSettings notification => _notification ?? (_notification = NotificationSettings.Default);
public GridSettings market_view_grid => _market_view_grid ?? (_market_view_grid = bts.shell.market_view.MarketViewSettings.DefaultGridSettings);
public GridSettings time_and_sales_grid => _time_and_sales_grid ?? (_time_and_sales_grid = GridSettings.Default.with(bts.shell.time_and_sales.TimeAndSalesSettings.DefaultColumns.all));
public GridSettings fills_grid => _fills_grid ?? (_fills_grid = bts.shell.fills.FillsSettings.DefaultGridSettings);
public GridSettings synthetic_management_grid => _synthetic_management_grid ?? (_synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings);
public GridSettings reconcile_report_grid => _reconcile_report_grid ??= Spark.Trading.ReconcileReportSettings.Default().grid;
public PNLSettings pnl => _pnl ?? (_pnl = PNLSettings.Default);
public bts.shell.orderbook.OrderBookGridSettings order_book_grid => _order_book_grid ?? (_order_book_grid = bts.shell.orderbook.OrderBookStartupData.Default.grid_settings_wrapper());
public GlobalAlertSettings alerts => _alerts ?? (_alerts = new GlobalAlertSettings());
public UserSettings with(LadderSettings ladder = null,
LadderSettingsProfile[] ladder_profiles = null,
NotificationSettings notification = null,
GridSettings provided_market_view_gridsettings = null,
GridSettings time_and_sales = null,
GridSettings provided_fills_gridsettings = null,
GridSettings provided_synthetic_management_gridsettings = null,
GridSettings reconcile_report_grid = null,
PNLSettings pnl = null,
bts.shell.orderbook.OrderBookGridSettings order_book_grid = null,
GlobalAlertSettings alerts = null) {
var us = new UserSettings(ladder_profiles ?? _ladder_profiles) {
_ladder = ladder ?? _ladder,
_notification = notification ?? _notification,
_market_view_grid = provided_market_view_gridsettings ?? _market_view_grid,
_time_and_sales_grid = time_and_sales ?? _time_and_sales_grid,
_fills_grid = provided_fills_gridsettings ?? _fills_grid,
_synthetic_management_grid = provided_synthetic_management_gridsettings ?? _synthetic_management_grid,
_reconcile_report_grid = reconcile_report_grid ?? _reconcile_report_grid,
_pnl = pnl ?? _pnl,
_order_book_grid = order_book_grid ?? _order_book_grid,
_alerts = alerts ?? _alerts
};
us._last_ladder_profile_id = Math.Max(_last_ladder_profile_id, us.ladder_profiles.Select(p => p.id).MaxOrDefault());
return us;
}
public static class FileExtensions {
public const string
GRID = "grid",
LADDER = "ladder",
PNL = "pnl";
}
}
// - currently all notification-triggers are in GridSettings
// - the SoundNotificationSettings below are used to disable/enable SoundNotifications
// - the ToastNotificationSettings below are used to disable/enable ToastNotifications, and adjust the flash-duration (if enabled).
// - since this is a new feature, the ability to quickly enable/disable both types of notifications may save us the trouble of having to redeploy,
// in the unfortunate event that a bug slipped through the cracks, or the performance impact is more significant than expected.
// TODO: maybe globalize notification settings?
[ProtoContract]
class NotificationSettings {
[ProtoMember(1)]
readonly SoundNotificationSettings _sound_notification;
[ProtoMember(2)]
readonly ToastNotificationSettings _toast_notification;
public NotificationSettings() { }
public NotificationSettings(SoundNotificationSettings sound_notification, ToastNotificationSettings toast_notification) {
_sound_notification = sound_notification;
_toast_notification = toast_notification;
}
public static NotificationSettings Default => new NotificationSettings(SoundNotificationSettings.Default, ToastNotificationSettings.Default);
public SoundNotificationSettings sound_notification => _sound_notification;
public ToastNotificationSettings toast_notification => _toast_notification;
}
[ProtoContract]
class SoundNotificationSettings {
[ProtoMember(1)]
readonly bool _enabled;
public SoundNotificationSettings() { }
public SoundNotificationSettings(bool enabled) =>
_enabled = enabled;
public static SoundNotificationSettings Default => new SoundNotificationSettings(true);
public bool enabled => _enabled;
}
[ProtoContract]
class ToastNotificationSettings {
// though it is possible to infer such Modes from Duration values (null = Disabled, max = ManualClose, otherwise Flash)
// this Mode enum makes the behavior very clear, and less likely to be misinterpretted down the road.
// (without this enum, the meaning of "duration = null," and "duration = 0 seconds," is debatable)
public enum Mode {
Disabled,
ManualClose,
Flash
}
public static ToastNotificationSettings Default =>
new ToastNotificationSettings(Mode.Flash, new Duration { Seconds = 5 }, 320);
[ProtoMember(1)]
readonly Mode _mode;
[ProtoMember(2)]
readonly Duration _duration;
[ProtoMember(3)]
readonly double _max_height;
public ToastNotificationSettings() { }
public ToastNotificationSettings(Mode mode, Duration duration, double max_height) {
_mode = mode;
_duration = duration;
_max_height = max_height;
}
public Mode mode => _mode;
public TimeSpan? duration => _mode == Mode.Flash ? _duration?.ToTimeSpan() ?? TimeSpan.FromSeconds(5) : new TimeSpan?();
public double max_height => _max_height;
}
}
#nullable enable
namespace mbts.spark {
using bts.shell;
using bts.shell.fills;
using bts.shell.market_view;
using bts.shell.orderbook;
using bts.shell.pnl;
using bts.shell.time_and_sales;
using bts.utils;
using Google.Protobuf.WellKnownTypes;
using mbts.ui.settings;
using ProtoBuf;
using Spark.Trading;
using System;
using System.Collections.Generic;
using System.Linq;
interface IDefaultableSettings {
bool use_defaults { get; set; }
}
/// <summary>
/// The settings Mothership. All window - and any other future user based - settings should have a corresponding default member here.
/// </summary>
[ProtoContract]
class UserSettings {
public static UserSettings factory_defaults =>
new() {
_ladder = LadderSettings.Default,
_notification = NotificationSettings.Default,
_market_view_grid = MarketViewSettings.DefaultGridSettings,
_fills_grid = FillsSettings.DefaultGridSettings,
_time_and_sales_grid = TimeAndSalesSettings.DefaultGridSettings,
_synthetic_management = SyntheticManagementSettings.default_grid_settings,
_reconcile_report = ReconcileReportSettings.Default().grid,
_order_book = OrderBookStartupData.DefaultGrid
};
[ProtoMember(1)]
LadderSettings? _ladder;
[ProtoMember(11)]
LadderSettingsProfile[]? _ladder_profiles;
/// When Profiles Are Deleted, ladders that were using that profile shall copy the profile's settings to a local profile.
/// Therefor:
/// Deleted profile ids must never be reused.
/// If they were reused, consider an unloaded workspace with ladders that were using the deleted profile.
/// If such a workspace was reloaded after a new profile was saved with the deleted one's id, the ladders using that deleted profile would suddenly be using the new profile.
/// To Prevent IDs From Being Recycled:
/// The last profile id is stored in the protobuf (<see cref="_last_ladder_profile_id"/>).
/// Anytime a profile is added, it shall use <see cref="next_ladder_profile_id"/>.
[ProtoMember(12)]
uint _last_ladder_profile_id;
// removing NotificationSettings these from proto-serialization, since they are disabled by default for now.
// This way, when they are enabled, there won't be any backwards compatability issues to worry about, as no clients will have persisted them yet.
NotificationSettings? _notification;
[ProtoMember(5)]
GridSettings? _market_view_grid;
[ProtoMember(6)]
GridSettings? _time_and_sales_grid;
[ProtoMember(7)]
GridSettings? _fills_grid;
[ProtoMember(8)]
GridSettings? _synthetic_management;
[ProtoMember(14)]
GridSettings? _reconcile_report;
[ProtoMember(9)]
OrderBookGridSettings? _order_book;
[ProtoMember(10)]
PNLSettings? _pnl;
[ProtoMember(13)]
GlobalAlertSettings? _alerts;
/// Actual settings may deviate from those of the theme <see cref="_last_picked_theme"/>, as
/// the user is free to change specific properties after selecting a theme.
[ProtoMember(15)]
string? _last_picked_theme;
public UserSettings() { }
public LadderSettings ladder =>
_ladder ??= LadderSettings.Default;
public IReadOnlyList<LadderSettingsProfile> ladder_profiles =>
_ladder_profiles ?? Array.Empty<LadderSettingsProfile>();
public uint next_ladder_profile_id =>
_last_ladder_profile_id + 1;
public NotificationSettings notification =>
_notification ??= NotificationSettings.Default;
public GridSettings market_view_grid =>
_market_view_grid ??= MarketViewSettings.DefaultGridSettings;
public GridSettings time_and_sales_grid =>
_time_and_sales_grid ??= GridSettings.Default.with(
TimeAndSalesSettings.DefaultColumns.all);
public GridSettings fills_grid =>
_fills_grid ??= FillsSettings.DefaultGridSettings;
public GridSettings synthetic_management_grid =>
_synthetic_management ??=
SyntheticManagementSettings.default_grid_settings;
public GridSettings reconcile_report_grid =>
_reconcile_report ??= ReconcileReportSettings.Default().grid ??
throw new InvalidOperationException();
public PNLSettings pnl =>
_pnl ??= PNLSettings.Default;
public OrderBookGridSettings order_book_grid =>
_order_book ??= OrderBookStartupData.Default.grid_settings_wrapper();
public GlobalAlertSettings alerts =>
_alerts ??= new GlobalAlertSettings();
public string? last_picked_theme => _last_picked_theme;
public UserSettings with(
LadderSettings? ladder = null,
LadderSettingsProfile[]? ladder_profiles = null,
NotificationSettings? notification = null,
GridSettings? market_view = null,
GridSettings? time_and_sales = null,
GridSettings? fills = null,
GridSettings? synthetic_management = null,
GridSettings? reconcile_report = null,
PNLSettings? pnl = null,
OrderBookGridSettings? order_book_grid = null,
GlobalAlertSettings? alerts = null,
Optional<string?> last_picked_theme = default) {
var us = new UserSettings() {
_ladder_profiles = ladder_profiles ?? _ladder_profiles,
_ladder = ladder ?? _ladder,
_notification = notification ?? _notification,
_market_view_grid = market_view ?? _market_view_grid,
_time_and_sales_grid = time_and_sales ?? _time_and_sales_grid,
_fills_grid = fills ?? _fills_grid,
_synthetic_management = synthetic_management ?? _synthetic_management,
_reconcile_report = reconcile_report ?? _reconcile_report,
_pnl = pnl ?? _pnl,
_order_book = order_book_grid ?? _order_book,
_alerts = alerts ?? _alerts,
_last_picked_theme = last_picked_theme.or(_last_picked_theme)
};
us._last_ladder_profile_id = Math.Max(_last_ladder_profile_id, us.ladder_profiles.Select(p => p.id).MaxOrDefault());
return us;
}
public static class FileExtensions {
public const string
GRID = "grid",
LADDER = "ladder",
PNL = "pnl";
}
}
// - currently all notification-triggers are in GridSettings
// - the SoundNotificationSettings below are used to disable/enable SoundNotifications
// - the ToastNotificationSettings below are used to disable/enable ToastNotifications, and adjust the flash-duration (if enabled).
// - since this is a new feature, the ability to quickly enable/disable both types of notifications may save us the trouble of having to redeploy,
// in the unfortunate event that a bug slipped through the cracks, or the performance impact is more significant than expected.
// TODO: maybe globalize notification settings?
[ProtoContract]
class NotificationSettings {
[ProtoMember(1)]
readonly SoundNotificationSettings? _sound_notification;
[ProtoMember(2)]
readonly ToastNotificationSettings? _toast_notification;
public NotificationSettings() { }
public NotificationSettings(SoundNotificationSettings sound_notification, ToastNotificationSettings toast_notification) {
_sound_notification = sound_notification;
_toast_notification = toast_notification;
}
public static NotificationSettings Default => new NotificationSettings(SoundNotificationSettings.Default, ToastNotificationSettings.Default);
public SoundNotificationSettings? sound_notification => _sound_notification;
public ToastNotificationSettings? toast_notification => _toast_notification;
}
[ProtoContract]
class SoundNotificationSettings {
[ProtoMember(1)]
readonly bool _enabled;
public SoundNotificationSettings() { }
public SoundNotificationSettings(bool enabled) =>
_enabled = enabled;
public static SoundNotificationSettings Default => new(true);
public bool enabled => _enabled;
}
[ProtoContract]
class ToastNotificationSettings {
// though it is possible to infer such Modes from Duration values (null = Disabled, max = ManualClose, otherwise Flash)
// this Mode enum makes the behavior very clear, and less likely to be misinterpretted down the road.
// (without this enum, the meaning of "duration = null," and "duration = 0 seconds," is debatable)
public enum Mode {
Disabled,
ManualClose,
Flash
}
public static ToastNotificationSettings Default =>
new(Mode.Flash, new Duration { Seconds = 5 }, 320);
[ProtoMember(1)]
readonly Mode _mode;
[ProtoMember(2)]
readonly Duration? _duration;
[ProtoMember(3)]
readonly double _max_height;
public ToastNotificationSettings() { }
public ToastNotificationSettings(Mode mode, Duration duration, double max_height) {
_mode = mode;
_duration = duration;
_max_height = max_height;
}
public Mode mode => _mode;
public TimeSpan duration =>
_mode switch {
Mode.Flash => _duration?.ToTimeSpan() ?? TimeSpan.FromSeconds(5),
_ => new()
};
public double max_height => _max_height;
}
}
@@ -1,177 +1,226 @@
using bts;
using bts.settings;
using bts.shell.pnl;
using bts.utils;
using mbts.spark;
using mbts.ui.settings;
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading.Tasks;
namespace mbts.persist {
/// <summary>
/// Represents the top level of the persisted settings hierarchy.
/// One UserSettings object, which contains the defaults for every SparkWindow, can be read/written on the server by UISettingsHub.
/// </summary>
[Export(typeof(UserSettingsHub))]
class UserSettingsHub {
// do NOT CHANGE these consts, they represent a file path on the server
const string NAME_SPACE = "settings", DEFAULT_KEY = "default";
static readonly IImmutableProperty<UserSettings, GridSettings>
_fills_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.fills_grid, (us, gs) => us.with(provided_fills_gridsettings: gs)),
_mview_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.market_view_grid, (us, gs) => us.with(provided_market_view_gridsettings: gs)),
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs)),
_reconcile_report_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.reconcile_report_grid, (us, gs) => us.with(reconcile_report_grid: gs));
static readonly IImmutableProperty<UserSettings, PNLSettings>
_pnl_propi = ImmutableProperty.create<UserSettings, PNLSettings>(us => us.pnl, (us, pnl) => us.with(pnl: pnl));
readonly object _default_settings_lock = new object();
readonly BehaviorSubject<UserSettings> _default_settings_subject = new BehaviorSubject<UserSettings>(UserSettings.factory_defaults);
readonly IEnumerable<IFillsGridSettingsMigrationRule> _fills_migration_rules;
readonly IEnumerable<IMarketViewGridSettingsMigrationRule> _mview_migration_rules;
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
readonly IEnumerable<ISyntheticManagementMigrationRule> _synth_migration_rules;
readonly IEnumerable<IReconcileReportMigrationRule> _reconcile_report_migration_rules;
readonly IUserPersist _persist;
UserSettings _default_settings;
[ImportingConstructor]
public UserSettingsHub(
IUserPersist persist,
[ImportMany] IEnumerable<IFillsGridSettingsMigrationRule> fills_migration_rules,
[ImportMany] IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules,
[ImportMany] IEnumerable<IPAndLGridSettingsMigrationRule> pandl_migration_rules,
[ImportMany] IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules,
[ImportMany] IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules,
[ImportMany] IEnumerable<IReconcileReportMigrationRule> reconcile_report_migration_rules) {
//
_persist = persist;
_fills_migration_rules = fills_migration_rules ?? Enumerable.Empty<IFillsGridSettingsMigrationRule>();
_mview_migration_rules = mview_migration_rules ?? Enumerable.Empty<IMarketViewGridSettingsMigrationRule>();
_pnl_migration_rules = pnl_migration_rules ?? Enumerable.Empty<IPNLSettingsMigrationRule>();
_synth_migration_rules = synth_migration_rules ?? Enumerable.Empty<ISyntheticManagementMigrationRule>();
_reconcile_report_migration_rules = reconcile_report_migration_rules.empty_if_null();
init();
async void init() {
UserSettings s;
try {
s = await _persist.get<UserSettings>(NAME_SPACE, DEFAULT_KEY);
if (s != null) {
var wrapped_rules = new[] {
_fills_migration_rules.merge().wrap(_fills_propi),
_mview_migration_rules.merge().wrap(_mview_propi),
_pnl_migration_rules.merge().wrap(_pnl_propi),
_synth_migration_rules.merge().wrap(_synth_propi),
_reconcile_report_migration_rules.merge().wrap(_reconcile_report_propi)
};
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
// TODO: move ladder sanitization logic into ISettingsMigrationRule
s.ladder.sanitize();
// ensure valid named ladder set (w/ distinct, non-zero ids)
var ladder_profile_map = new Dictionary<uint, LadderSettingsProfile>();
var unidentified_lps = new List<LadderSettingsProfile>();
foreach (var lp in s.ladder_profiles) {
lp.value.sanitize();
if (lp.id == 0) {
unidentified_lps.Add(lp);
} else ladder_profile_map[lp.id] = lp;
}
uint id = s.next_ladder_profile_id;
foreach (var lp in unidentified_lps) {
ladder_profile_map[id] = lp.with(id);
id++;
}
var sorted_lps = ladder_profile_map.Values.ToArray();
Array.Sort(sorted_lps, Comparer<uint>.Default.wrap<uint, LadderSettingsProfile>(lp => lp.id));
if (!sorted_lps.SequenceEqual(s.ladder_profiles))
await _persist.set(NAME_SPACE, DEFAULT_KEY, s = s.with(ladder_profiles: sorted_lps));
}
} catch (Exception ex) {
Log.Persist.warn($"Error loading {typeof(UserSettings)} from namespace '{NAME_SPACE}', will restore factory defaults and continue as usual: {ex}");
await save_default(s = UserSettings.factory_defaults);
}
lock (_default_settings_lock) {
initialized = true;
if (s != null)
_default_settings_subject.OnNext(_default_settings = s);
}
}
}
// indicates whether or not the settings have been loaded from the server (this happens once, on startup - see the constructor for details)
public bool initialized { get; private set; }
internal UserSettings current {
get {
lock (_default_settings_lock)
return _default_settings ?? UserSettings.factory_defaults;
}
}
/// Subscribing via _persist.subscribe may be cleaner in theory, however using a local event allows observers to be notified immediately.
/// Assuming there aren't any rogue agents saving UserSettings by themselves, there should never be any discrepancies.
public IObservable<UserSettings> subscribe_default_settings() => _default_settings_subject.AsObservable();
public IWindowSettingsManager<GridSettings> create_fills_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_fills_migration_rules,
_fills_propi);
public IWindowSettingsManager<GridSettings> create_mview_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_mview_migration_rules,
_mview_propi);
public IWindowSettingsManager<PNLSettings> create_pnl_settings_manager(PNLSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_pnl_migration_rules,
_pnl_propi);
public IWindowSettingsManager<GridSettings> create_synth_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_synth_migration_rules,
_synth_propi);
public IWindowSettingsManager<GridSettings> create_reconcile_report_settings_manager(
GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_reconcile_report_migration_rules,
_reconcile_report_propi);
public async Task save_default(UserSettings value) {
// notify observers before persisting so the changes are reflected immediately in the ui
lock (_default_settings_lock) {
_default_settings_subject.OnNext(_default_settings = value);
}
await _persist_async();
}
IWindowSettingsManager<T> _create_settings_manager<T>(T instance_settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, IImmutableProperty<UserSettings, T> propi) where T : class {
if (instance_settings != null && migration_rules.merge().apply(instance_settings, out var new_settings))
instance_settings = new_settings;
return new WindowSettingsManager<T>(this, instance_settings, propi.get, propi.set);
}
async Task _persist_async() {
try {
await _persist.set(NAME_SPACE, DEFAULT_KEY, _default_settings);
} catch (Exception ex) {
Log.Persist.warn($"Error persisting {typeof(UserSettings)} in namespace '{NAME_SPACE}': {ex}");
}
}
}
}
using bts;
using bts.settings;
using bts.shell.pnl;
using bts.utils;
using mbts.spark;
using mbts.ui.settings;
using Spark.Settings;
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Threading;
using System.Threading.Tasks;
namespace mbts.persist {
/// <summary>
/// Represents the top level of the persisted settings hierarchy.
/// One UserSettings object, which contains the defaults for every SparkWindow, can be read/written on the server by UISettingsHub.
/// </summary>
[Export(typeof(UserSettingsHub))]
class UserSettingsHub {
// do NOT CHANGE these consts, they represent a file path on the server
const string NAME_SPACE = "settings", DEFAULT_KEY = "default";
static readonly IImmutableProperty<UserSettings, GridSettings>
_fills_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.fills_grid, (us, gs) => us.with(fills: gs)),
_mview_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.market_view_grid, (us, gs) => us.with(market_view: gs)),
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(synthetic_management: gs)),
_reconcile_report_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.reconcile_report_grid, (us, gs) => us.with(reconcile_report: gs));
static readonly IImmutableProperty<UserSettings, PNLSettings>
_pnl_propi = ImmutableProperty.create<UserSettings, PNLSettings>(us => us.pnl, (us, pnl) => us.with(pnl: pnl));
readonly object _default_settings_lock = new object();
readonly BehaviorSubject<UserSettings> _default_settings_subject = new BehaviorSubject<UserSettings>(UserSettings.factory_defaults);
// A 'settings theme' is activated by updating the default settings object, and clearing all
// 'instance' settings so existing windows adopt the new theme (i.e., the new defaults).
readonly IEnumerable<IFillsGridSettingsMigrationRule> _fills_migration_rules;
readonly IEnumerable<IMarketViewGridSettingsMigrationRule> _mview_migration_rules;
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
readonly IEnumerable<ISyntheticManagementMigrationRule> _synth_migration_rules;
readonly IEnumerable<IReconcileReportMigrationRule> _reconcile_report_migration_rules;
readonly IUserPersist _persist;
readonly Dictionary<string, UserSettingsTheme> _themes;
UserSettings _default_settings;
IDisposable _theme_op = null;
[ImportingConstructor]
public UserSettingsHub(
IUserPersist persist,
[ImportMany] IEnumerable<IFillsGridSettingsMigrationRule> fills_migration_rules,
[ImportMany] IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules,
[ImportMany] IEnumerable<IPAndLGridSettingsMigrationRule> pandl_migration_rules,
[ImportMany] IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules,
[ImportMany] IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules,
[ImportMany] IEnumerable<IReconcileReportMigrationRule> reconcile_report_migration_rules,
[ImportMany] IEnumerable<UserSettingsTheme> themes) {
//
_persist = persist;
_fills_migration_rules = fills_migration_rules ?? Enumerable.Empty<IFillsGridSettingsMigrationRule>();
_mview_migration_rules = mview_migration_rules ?? Enumerable.Empty<IMarketViewGridSettingsMigrationRule>();
_pnl_migration_rules = pnl_migration_rules ?? Enumerable.Empty<IPNLSettingsMigrationRule>();
_synth_migration_rules = synth_migration_rules ?? Enumerable.Empty<ISyntheticManagementMigrationRule>();
_reconcile_report_migration_rules = reconcile_report_migration_rules.empty_if_null();
_themes = themes.ToDictionaryCrushed(t => t.name);
init();
async void init() {
UserSettings s;
try {
s = await _persist.get<UserSettings>(NAME_SPACE, DEFAULT_KEY);
if (s != null) {
var wrapped_rules = new[] {
_fills_migration_rules.merge().wrap(_fills_propi),
_mview_migration_rules.merge().wrap(_mview_propi),
_pnl_migration_rules.merge().wrap(_pnl_propi),
_synth_migration_rules.merge().wrap(_synth_propi),
_reconcile_report_migration_rules.merge().wrap(_reconcile_report_propi)
};
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
// TODO: move ladder sanitization logic into ISettingsMigrationRule
s.ladder.sanitize();
// ensure valid named ladder set (w/ distinct, non-zero ids)
var ladder_profile_map = new Dictionary<uint, LadderSettingsProfile>();
var unidentified_lps = new List<LadderSettingsProfile>();
foreach (var lp in s.ladder_profiles) {
lp.value.sanitize();
if (lp.id == 0) {
unidentified_lps.Add(lp);
} else ladder_profile_map[lp.id] = lp;
}
uint id = s.next_ladder_profile_id;
foreach (var lp in unidentified_lps) {
ladder_profile_map[id] = lp.with(id);
id++;
}
var sorted_lps = ladder_profile_map.Values.ToArray();
Array.Sort(sorted_lps, Comparer<uint>.Default.wrap<uint, LadderSettingsProfile>(lp => lp.id));
if (!sorted_lps.SequenceEqual(s.ladder_profiles))
await _persist.set(NAME_SPACE, DEFAULT_KEY, s = s.with(ladder_profiles: sorted_lps));
}
} catch (Exception ex) {
Log.Persist.warn($"Error loading {typeof(UserSettings)} from namespace '{NAME_SPACE}', will restore factory defaults and continue as usual: {ex}");
await save_default(s = UserSettings.factory_defaults);
}
lock (_default_settings_lock) {
initialized = true;
if (s != null)
_default_settings_subject.OnNext(_default_settings = s);
}
}
}
public event Action request_clear_instance_settings {
add {
lock (_default_settings_lock) {
_request_clear_instance_settings += value;
}
}
remove {
lock (_default_settings_lock) {
_request_clear_instance_settings -= value;
}
}
}
event Action _request_clear_instance_settings;
// indicates whether or not the settings have been loaded from the server (this happens once, on startup - see the constructor for details)
public bool initialized { get; private set; }
public IEnumerable<UserSettingsTheme> themes =>
_themes.Values;
internal UserSettings current {
get {
lock (_default_settings_lock)
return _default_settings ?? UserSettings.factory_defaults;
}
}
/// Subscribing via _persist.subscribe may be cleaner in theory, however using a local event allows observers to be notified immediately.
/// Assuming there aren't any rogue agents saving UserSettings by themselves, there should never be any discrepancies.
public IObservable<UserSettings> subscribe_default_settings() => _default_settings_subject.AsObservable();
public IWindowSettingsManager<GridSettings> create_fills_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_fills_migration_rules,
_fills_propi);
public IWindowSettingsManager<GridSettings> create_mview_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_mview_migration_rules,
_mview_propi);
public IWindowSettingsManager<PNLSettings> create_pnl_settings_manager(PNLSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_pnl_migration_rules,
_pnl_propi);
public IWindowSettingsManager<GridSettings> create_synth_settings_manager(GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_synth_migration_rules,
_synth_propi);
public IWindowSettingsManager<GridSettings> create_reconcile_report_settings_manager(
GridSettings instance_settings) =>
_create_settings_manager(
instance_settings,
_reconcile_report_migration_rules,
_reconcile_report_propi);
public async Task save_default(UserSettings value, bool clear_instance_settings = false) {
// * Notify observers before persisting so the changes are reflected immediately in the ui.
// * If instance settings are to be cleared, don't clear them until the new defaults are
// ready. We want to avoid the case where windows w/ instance settings consume cpu
// loading the original defaults, just to load the new defaults immediately after that.
lock (_default_settings_lock) {
Disposable.dispose(ref _theme_op);
if (clear_instance_settings) {
clear_instance_async().ignore();
}
_default_settings_subject.OnNext(_default_settings = value);
}
await _persist_async();
async Task clear_instance_async() {
var cts = new CancellationTokenSource();
_theme_op = Disposable.Create(() => cts.Cancel());
var next = await _default_settings_subject.Skip(1).FirstOrDefaultAsync();
if (!cts.IsCancellationRequested) {
_request_clear_instance_settings?.Invoke();
}
}
}
IWindowSettingsManager<T> _create_settings_manager<T>(T instance_settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, IImmutableProperty<UserSettings, T> propi) where T : class {
if (instance_settings != null && migration_rules.merge().apply(instance_settings, out var new_settings))
instance_settings = new_settings;
var wsm = new WindowSettingsManager<T>(this, instance_settings, propi.get, propi.set);
lock (_default_settings_lock) {
request_clear_instance_settings += clear_instance_settings;
}
return wsm;
void clear_instance_settings() {
wsm.instance_settings = null;
}
}
async Task _persist_async() {
try {
await _persist.set(NAME_SPACE, DEFAULT_KEY, _default_settings);
} catch (Exception ex) {
Log.Persist.warn($"Error persisting {typeof(UserSettings)} in namespace '{NAME_SPACE}': {ex}");
}
}
}
}
@@ -0,0 +1,36 @@
#nullable enable
namespace Spark.Settings {
using mbts.spark;
using System.Composition;
class UserSettingsTheme {
public UserSettingsTheme() {
name = string.Empty;
value = new();
}
public UserSettingsTheme(string name, UserSettings value) {
this.name = name;
this.value = value;
}
public string name { get; }
public UserSettings value { get; }
}
class ExportedThemes {
[Export]
public UserSettingsTheme light_theme =>
new("Light", UserSettings.factory_defaults);
[Export]
public UserSettingsTheme dark_theme =>
new("Dark", UserSettings.factory_defaults.with(
market_view: bts.shell.market_view.MarketViewSettings.dark_theme_grid_settings,
time_and_sales: bts.shell.time_and_sales.TimeAndSalesSettings.dark_theme_grid_settings,
fills: bts.shell.fills.FillsSettings.dark_theme_grid_settings,
synthetic_management: bts.shell.SyntheticManagementSettings.dark_theme_grid_settings,
ladder: LadderSettings.dark_theme));
}
}
@@ -1,77 +1,77 @@
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.ui.shell {
public enum DockState {
FLOATING = 1,
TOP = 2,
BOTTOM = 3,
LEFT = 4,
RIGHT = 5,
}
[ProtoContract]
public class WindowPosition {
[ProtoMember(3)]
public int width;
[ProtoMember(4)]
public int height;
[ProtoMember(5)]
public int left;
[ProtoMember(6)]
public int top;
[ProtoMember(7)]
public bool is_maximized;
[ProtoMember(8)]
public string host_id;
[ProtoMember(9)]
public int tab_index;
[ProtoMember(100), DefaultValue(false)]
public bool horizontal = false;
[ProtoMember(101), DefaultValue(true)]
public bool show_text = true;
[ProtoMember(102), DefaultValue(true)]
public bool always_on_top = true;
//[ProtoMember(103)] //used
[ProtoMember(104)]
public string custom_title;
}
[ProtoContract]
public class WindowState {
[ProtoMember(1)]
public string owner;
[ProtoMember(2)]
public byte[] app_data;
//optional bool is_open = 3 [default = true];
//optional bool is_favorite = 4;
//optional string title = 5;
//optional string id = 6;
// optional bool is_loaded = 7 [default = false];
[ProtoMember(8)]
public WindowPosition position;
[ProtoMember(9)]
public string title; // for debugging
}
[ProtoContract]
public class ModuleState {
[ProtoMember(1)]
public string name;
[ProtoMember(2)]
public byte[] app_data;
}
}
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.ui.shell {
public enum DockState {
FLOATING = 1,
TOP = 2,
BOTTOM = 3,
LEFT = 4,
RIGHT = 5,
}
[ProtoContract]
public class WindowPosition {
[ProtoMember(3)]
public int width;
[ProtoMember(4)]
public int height;
[ProtoMember(5)]
public int left;
[ProtoMember(6)]
public int top;
[ProtoMember(7)]
public bool is_maximized;
[ProtoMember(8)]
public string host_id;
[ProtoMember(9)]
public int tab_index;
[ProtoMember(100), DefaultValue(false)]
public bool horizontal = false;
[ProtoMember(101), DefaultValue(true)]
public bool show_text = true;
[ProtoMember(102), DefaultValue(true)]
public bool always_on_top = true;
//[ProtoMember(103)] //used
[ProtoMember(104)]
public string custom_title;
}
[ProtoContract]
public class WindowState {
[ProtoMember(1)]
public string owner;
[ProtoMember(2)]
public byte[] app_data;
//optional bool is_open = 3 [default = true];
//optional bool is_favorite = 4;
//optional string title = 5;
//optional string id = 6;
// optional bool is_loaded = 7 [default = false];
[ProtoMember(8)]
public WindowPosition position;
[ProtoMember(9)]
public string title; // for debugging
}
[ProtoContract]
public class ModuleState {
[ProtoMember(1)]
public string name;
[ProtoMember(2)]
public byte[] app_data;
}
}
@@ -1,25 +1,25 @@
using System;
namespace bts.api.orderman {
public readonly struct FillSecurity : IEquatable<FillSecurity> {
public FillSecurity(SecurityImmutable? sec, FillImmutable fill) {
this.sec = sec;
this.fill = fill;
}
public FillSecurity((SecurityImmutable? sec, FillImmutable fill) t) {
sec = t.sec;
fill = t.fill;
}
public static implicit operator FillSecurity(ValueTuple<SecurityImmutable?, FillImmutable> x) => new FillSecurity(x);
public SecurityImmutable? sec { get; }
public FillImmutable fill { get; }
public override bool Equals(object obj) => (obj is FillSecurity os) && Equals(os);
public bool Equals(FillSecurity other) => fill.FillKey == other.fill.FillKey && sec == other.sec;
public override int GetHashCode() => (sec, fill).GetHashCode();
public static bool operator ==(FillSecurity left, FillSecurity right) => left.Equals(right);
public static bool operator !=(FillSecurity left, FillSecurity right) => !left.Equals(right);
}
}
using System;
namespace bts.api.orderman {
public readonly struct FillSecurity : IEquatable<FillSecurity> {
public FillSecurity(SecurityImmutable? sec, FillImmutable fill) {
this.sec = sec;
this.fill = fill;
}
public FillSecurity((SecurityImmutable? sec, FillImmutable fill) t) {
sec = t.sec;
fill = t.fill;
}
public static implicit operator FillSecurity(ValueTuple<SecurityImmutable?, FillImmutable> x) => new FillSecurity(x);
public SecurityImmutable? sec { get; }
public FillImmutable fill { get; }
public override bool Equals(object obj) => (obj is FillSecurity os) && Equals(os);
public bool Equals(FillSecurity other) => fill.FillKey == other.fill.FillKey && sec == other.sec;
public override int GetHashCode() => (sec, fill).GetHashCode();
public static bool operator ==(FillSecurity left, FillSecurity right) => left.Equals(right);
public static bool operator !=(FillSecurity left, FillSecurity right) => !left.Equals(right);
}
}
@@ -1,101 +1,101 @@
using bts;
using bts.api;
using bts.api.orderman;
using bts.utils;
using System;
using static bts.api.orderman.Fill.Types;
namespace mbts.spark {
class FillTuple : IEquatable<FillTuple> {
public static readonly Func<FillTuple, string> key_fn = ft => ft.fill.FillKey;
public readonly FillImmutable fill;
public readonly SecurityImmutable? security;
OrderSecurityRootPair? _order_tuple;
public FillTuple(FillImmutable fill, SecurityImmutable? security, OrderSecurityRootPair? order_tuple) {
this.fill = fill;
this.security = security;
_order_tuple = order_tuple;
}
public FillTuple((SecurityImmutable? sec, FillImmutable val) fill, OrderSecurity? order_tuple)
: this(fill.val, fill.sec, order_tuple.HasValue ? OrderSecurityRootPair.from_order_security(order_tuple.Value) : (OrderSecurityRootPair?)null) { }
public AlgoData? algo =>
_order_tuple.HasValue ? _order_tuple.Value.order.algo : (AlgoData?)null;
public AlgoDefinitionMetadata? algo_meta =>
_order_tuple.HasValue ? _order_tuple.Value.order.meta : null;
public OrderRecordImmutable? order =>
_order_tuple.HasValue ? _order_tuple.Value.order.order : (OrderRecordImmutable?)null;
public AlgoData? root_algo =>
_order_tuple.HasValue ? _order_tuple.Value.root.algo : (AlgoData?)null;
public AlgoDefinitionMetadata? root_algo_meta =>
_order_tuple.HasValue ? _order_tuple.Value.root.meta : null;
public OrderSecurityAlgo? root_order_security => _order_tuple?.root;
public AccountTuple account_tuple => new(account_id, account);
public string account {
get {
var account_string = string.Empty;
if (fill.FillKind != FillKind.Sod && order.HasValue && order.Value.HasDef) account_string = order.Value.Def.Account;
else account_string = fill.Account;
return account_string == null ? string.Empty : account_string;
}
}
public uint account_id =>
fill.FillKind != FillKind.Sod && order.HasValue ?
order.Value.account_id() :
fill.InternalAccountId;
public TimeInForce time_in_force =>
order?.time_in_force() ?? TimeInForce.Undefined;
public bool Equals(FillTuple other) =>
other != null &&
_order_tuple.Equals(other._order_tuple) &&
security.HasValue == other.security.HasValue &&
fill.Equals(other.fill);
public override bool Equals(object obj) =>
Equals(obj as FillTuple);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
fill.FillKey.GetHashCode(),
order.HasValue ? order.Value.Key.GetHashCode() : 0,
security.HasValue ? security.Value.Id.GetHashCode() : 0);
}
static class FillTupleExtensions {
public static DateTime fill_time(this FillTuple fill_tuple) =>
fill_tuple.fill.Timestamp.to_local_time();
public static TimeInForce time_in_force(this FillTuple fill_tuple) =>
fill_tuple.order.HasValue ? fill_tuple.order.Value.time_in_force() : TimeInForce.Undefined;
public static CustomOrderDefinition.CustomOneofCase order_type(this FillTuple fill_tuple) =>
fill_tuple.order.HasValue ? fill_tuple.order.Value.order_type() : CustomOrderDefinition.CustomOneofCase.None;
public static Venue exchange(this FillTuple fill_tuple) =>
fill_tuple.security.HasValue ? fill_tuple.security.Value.Exchange : Venue.Unset;
public static uint order_key(this FillTuple fill_tuple) =>
fill_tuple.order.HasValue ? fill_tuple.order.Value.Key : 0u;
public static string user(this FillTuple fill_tuple) =>
fill_tuple.order.HasValue ? fill_tuple.order.Value.user() : string.Empty;
public static uint user_id(this FillTuple fill_tuple) =>
fill_tuple.order?.user_id() ?? 0;
}
}
using bts;
using bts.api;
using bts.api.orderman;
using bts.utils;
using System;
using static bts.api.orderman.Fill.Types;
namespace mbts.spark {
class FillTuple : IEquatable<FillTuple> {
public static readonly Func<FillTuple, string> key_fn = ft => ft.fill.FillKey;
public readonly FillImmutable fill;
public readonly SecurityImmutable? security;
OrderSecurityRootPair? _order_tuple;
public FillTuple(FillImmutable fill, SecurityImmutable? security, OrderSecurityRootPair? order_tuple) {
this.fill = fill;
this.security = security;
_order_tuple = order_tuple;
}
public FillTuple((SecurityImmutable? sec, FillImmutable val) fill, OrderSecurity? order_tuple)
: this(fill.val, fill.sec, order_tuple.HasValue ? OrderSecurityRootPair.from_order_security(order_tuple.Value) : (OrderSecurityRootPair?)null) { }
public AlgoData? algo =>
_order_tuple.HasValue ? _order_tuple.Value.order.algo : (AlgoData?)null;
public AlgoDefinitionMetadata? algo_meta =>
_order_tuple.HasValue ? _order_tuple.Value.order.meta : null;
public OrderRecordImmutable? order =>
_order_tuple.HasValue ? _order_tuple.Value.order.order : (OrderRecordImmutable?)null;
public AlgoData? root_algo =>
_order_tuple.HasValue ? _order_tuple.Value.root.algo : (AlgoData?)null;
public AlgoDefinitionMetadata? root_algo_meta =>
_order_tuple.HasValue ? _order_tuple.Value.root.meta : null;
public OrderSecurityAlgo? root_order_security => _order_tuple?.root;
public AccountTuple account_tuple => new(account_id, account);
public string account {
get {
var account_string = string.Empty;
if (fill.FillKind != FillKind.Sod && order.HasValue && order.Value.HasDef) account_string = order.Value.Def.Account;
else account_string = fill.Account;
return account_string == null ? string.Empty : account_string;
}
}
public uint account_id =>
fill.FillKind != FillKind.Sod && order.HasValue ?
order.Value.account_id() :
fill.InternalAccountId;
public TimeInForce time_in_force =>
order?.time_in_force() ?? TimeInForce.Undefined;
public bool Equals(FillTuple other) =>
other != null &&
_order_tuple.Equals(other._order_tuple) &&
security.HasValue == other.security.HasValue &&
fill.Equals(other.fill);
public override bool Equals(object obj) =>
Equals(obj as FillTuple);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
fill.FillKey.GetHashCode(),
order.HasValue ? order.Value.Key.GetHashCode() : 0,
security.HasValue ? security.Value.Id.GetHashCode() : 0);
}
static class FillTupleExtensions {
public static DateTime fill_time(this FillTuple fill_tuple) =>
fill_tuple.fill.Timestamp.to_local_time();
public static TimeInForce time_in_force(this FillTuple fill_tuple) =>
fill_tuple.order.HasValue ? fill_tuple.order.Value.time_in_force() : TimeInForce.Undefined;
public static CustomOrderDefinition.CustomOneofCase order_type(this FillTuple fill_tuple) =>
fill_tuple.order.HasValue ? fill_tuple.order.Value.order_type() : CustomOrderDefinition.CustomOneofCase.None;
public static Venue exchange(this FillTuple fill_tuple) =>
fill_tuple.security.HasValue ? fill_tuple.security.Value.Exchange : Venue.Unset;
public static uint order_key(this FillTuple fill_tuple) =>
fill_tuple.order.HasValue ? fill_tuple.order.Value.Key : 0u;
public static string user(this FillTuple fill_tuple) =>
fill_tuple.order.HasValue ? fill_tuple.order.Value.user() : string.Empty;
public static uint user_id(this FillTuple fill_tuple) =>
fill_tuple.order?.user_id() ?? 0;
}
}
@@ -1,19 +1,19 @@
using bts.api.orderman;
namespace mbts.spark {
struct NamedCustomOrderDefinition {
public readonly string id, display_name;
public readonly CustomOrderDefinitionImmutable definition;
public NamedCustomOrderDefinition(CustomOrderDefinitionImmutable definition)
: this(definition.CustomCase.type_id(), definition.CustomCase.display_name(), definition) { }
public NamedCustomOrderDefinition(CompositeAlgoRecordImmutable record)
: this(record.AlgoDefinition.Algo) { }
public NamedCustomOrderDefinition(string id, string display_name, CustomOrderDefinitionImmutable definition) {
this.id = id;
this.display_name = display_name;
this.definition = definition;
}
}
}
using bts.api.orderman;
namespace mbts.spark {
struct NamedCustomOrderDefinition {
public readonly string id, display_name;
public readonly CustomOrderDefinitionImmutable definition;
public NamedCustomOrderDefinition(CustomOrderDefinitionImmutable definition)
: this(definition.CustomCase.type_id(), definition.CustomCase.display_name(), definition) { }
public NamedCustomOrderDefinition(CompositeAlgoRecordImmutable record)
: this(record.AlgoDefinition.Algo) { }
public NamedCustomOrderDefinition(string id, string display_name, CustomOrderDefinitionImmutable definition) {
this.id = id;
this.display_name = display_name;
this.definition = definition;
}
}
}
@@ -1,245 +1,245 @@
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.shell;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui.controls;
using Spark.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using OrderPair = System.ValueTuple<bts.api.SecurityImmutable?, bts.api.orderman.OrderRecordImmutable>;
using TickDisplaySettingsImmutable = bts.api.domain.TickDisplaySettingsImmutable;
namespace bts.spark {
internal class Activity {
public SecurityImmutable? security;
public string message;
public ILogViewerCategory category;
}
class OrderActivitySummarizer : IDisposable {
readonly IBus _bus;
IReadOnlyDictionary<uint, SecurityMD> _security = new Dictionary<uint, SecurityMD>();
IDisposable _dispose;
static readonly IEqualityComparer<OrderRecordImmutable> _id_comparer =
bts.utils.EqualityComparer.Create<OrderRecordImmutable, uint>(a => a.Key);
public OrderActivitySummarizer(IBus bus, IObservable<TableUpdate<(SecurityImmutable? sec, OrderRecordImmutable order), uint>> orders) {
_bus = bus;
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(sec => _security = sec),
orders.Subscribe(orders_updated)
);
}
public void Dispose() => Disposable.dispose(ref _dispose);
#region formatting
SecurityImmutable? try_get_security(uint security_key) {
if (_security.TryGetValue(security_key, out SecurityMD md)) {
return md.security;
}
return null;
}
public static string format_order_success(
uint key, double price, double size, Side side, ContractSpecificationImmutable? spec) =>
$"({key}) {side.ToString().ToLower()} {size} @ {spec.format(price)}";
string format_order_fail(
double price,
double size,
Side side,
ContractSpecificationImmutable? spec,
string error) =>
$"Failed to {side.ToString().ToLower()} {size} @ {spec.format(price)}. {error}";
string format_cancel_success(OrderRecordImmutable order, ContractSpecificationImmutable? spec) {
return $"({order.Key}) Canceled {order.short_description(spec)}, was working {order.working_quantity()}";
}
string format_cancel_error(OrderRecordImmutable order, ContractSpecificationImmutable? spec, string error) {
return $"({order.Key}) Failed to cancel {order.short_description(spec)}: {error}";
}
public static string format_fill_summary(OrderRecordImmutable before, OrderRecordImmutable after) =>
_format_fill_summary(_header(after.Key), before, after);
public static string format_fill_summary(OrderRecordImmutable before, OrderRecordImmutable after, SecurityImmutable? security) =>
_format_fill_summary(_header(after.Key, security), before, after);
public static string format_status_change(uint key, OrderStatus old_status, OrderStatus new_status) =>
_format_status_change(_header(key), key, old_status, new_status);
public static string format_status_change(uint key, OrderStatus old_status, OrderStatus new_status, SecurityImmutable? security) =>
_format_status_change(_header(key, security), key, old_status, new_status);
// since changes are naggled, transition changes aren't always detected (e.g. a new order changes before observers are notified of its existence), so old_status might be null.
public static string format_status_change(uint key, OrderStatus? old_status, OrderStatus new_status, SecurityImmutable? security) {
string header = _header(key, security);
return old_status.HasValue ?
_format_status_change(header, key, old_status.Value, new_status) :
_format_status_change(header, key, new_status);
}
public static string order_completion_debrief(
OrderRecordImmutable order,
SecurityImmutable? security,
TickDisplaySettingsImmutable qty_display) =>
_format_order_completion(
_header(order.Key, security), order, security?.ContractSpec, qty_display);
public static string modified_description(OrderRecordImmutable old_order, OrderRecordImmutable new_order, ContractSpecificationImmutable? spec) {
double old_price = old_order.State.Price, new_price = new_order.State.Price;
uint old_size = old_order.State.Quantity, new_size = new_order.State.Quantity;
uint old_attached_target_key = old_order.attached_target_order_key(), new_attached_target_key = new_order.attached_target_order_key();
return StringBuilderEx.join(prefix, change_descs());
//
string prefix() => $"({old_order.Key}) Changed";
IEnumerable<string> change_descs() {
if (old_price != new_price)
yield return $"price from {spec.format(old_price)} to {spec.format(new_price)}";
if (old_size != new_size)
yield return $"size from {old_size} to {new_size}, working {new_order.working_quantity()}";
if (old_attached_target_key != new_attached_target_key)
yield return $"attached target from {old_attached_target_key} to {new_attached_target_key}";
}
}
static string _header(uint key) => $"({key})";
static string _header(uint key, SecurityImmutable? security) => $"({key}) {security.display_name()}";
static string _format_fill_summary(string header, OrderRecordImmutable before, OrderRecordImmutable after) {
int fb = (int)before.State.FilledQuantity;
int fa = (int)after.State.FilledQuantity;
return $"{header} {after.Def.Side.past_tense()} {(fa - fb)} ({fa} total), still working {after.working_quantity()}";
}
static string _format_status_change(string header, uint key, OrderStatus new_status) =>
$"{header} Status changed to {new_status}";
static string _format_status_change(string header, uint key, OrderStatus old_status, OrderStatus new_status) =>
$"{header} Status changed from {old_status} to {new_status}";
static string _format_order_completion(
string header,
OrderRecordImmutable order,
ContractSpecificationImmutable? spec,
TickDisplaySettingsImmutable qty_display) {
var filled_qty_str = qty_display.format(order.filled_quantity());
return $"{header} Completed: {order.side().past_tense()} {filled_qty_str}, " +
$"@ {spec.spec_or_default().format(order.avg_price())} (avg)";
}
#endregion
void orders_updated(TableUpdate<(SecurityImmutable? sec, OrderRecordImmutable order), uint> update) {
foreach (var o in update.added) {
if (!string.IsNullOrEmpty(o.order.State.LastMsg) && o.order.State.Created.ToDateTime().Date == DateTime.Today) {
if (o.sec.HasValue) {
log_status(ActivityLogCategory.warn, o.sec.Value, o.order.State.LastMsg);
}
}
}
foreach (var order in update.changes) {
var neworder = order.New.order;
var oldorder = order.Old.order;
var security = order.New.sec ?? order.Old.sec;
//
// description
var mod_desc = modified_description(oldorder, neworder, security?.ContractSpec);
if (!string.IsNullOrEmpty(mod_desc)) {
log_status(ActivityLogCategory.modify, security, mod_desc);
}
//
// filled quantity
if (oldorder.State.FilledQuantity != neworder.State.FilledQuantity && !neworder.has_parent()) {
log_status(ActivityLogCategory.fill, security, format_fill_summary(oldorder, neworder));
}
//
// status
if (oldorder.State.Status != neworder.State.Status && !neworder.has_parent()) {
log_status(ActivityLogCategory.modify, security,
format_status_change(neworder.Key, oldorder.State.Status, neworder.State.Status));
}
// last msg
if (oldorder.State.LastMsg != neworder.State.LastMsg) {
if (security.HasValue) {
log_status(ActivityLogCategory.warn, security, neworder.State.LastMsg);
}
}
}
}
public void log_status(ILogViewerCategory category, SecurityImmutable? security, string msg) {
_bus.post(SparkTags.LogActivity, new Activity { security = security, message = msg, category = category });
}
internal void on_send_order(
OrderDefinitionImmutable order, double price, double size, uint? key, string error) {
var security = try_get_security(order.SecurityKey);
log_status(ActivityLogCategory.order, security, key.HasValue ?
format_order_success(key.Value, price, size, order.Side, security?.ContractSpec) :
format_order_fail(price, size, order.Side, security?.ContractSpec, error));
}
internal void on_cancel_order(OrderRecordImmutable order, bool success, string error) {
var security = try_get_security(order.Def.SecurityKey);
log_status(ActivityLogCategory.cancel, security, success ?
format_cancel_success(order, security?.ContractSpec) :
format_cancel_error(order, security?.ContractSpec, error));
}
internal void update_order(
OrderRecordImmutable order, double price, double size, bool algo_changed) =>
_log_order_update(order, price, size, algo_changed, "change from");
internal void update_order_failed(OrderRecordImmutable order, double price, double size, bool algo_changed) =>
_log_order_update(order, price, size, algo_changed, "failed to change from");
void _log_order_update(
OrderRecordImmutable order,
double price,
double size,
bool algo_changed,
string change_desc_prefix
) {
if (price == order.price() && size == order.working_quantity() && !algo_changed) {
// nothing actually changed
return;
}
const string algo_change_text = "change algo parameters";
var security = try_get_security(order.Def.SecurityKey);
var change_descs = new List<string>(1);
TickDisplaySettingsImmutable? price_display =
security.HasValue && security.Value.HasContractSpec ?
security.Value.ContractSpec.tick_factors().to_api_tick_display_settingsi() : null;
var price_qty_change_desc = OrderUtils.update_description(
order, price, size, price_display);
if (!price_qty_change_desc.is_null_or_empty()) {
change_descs.Add(price_qty_change_desc);
}
if (algo_changed) {
change_descs.Add(algo_change_text);
}
var desc = $"({order.Key}) {change_desc_prefix} {"; ".join(change_descs)}";
log_status(ActivityLogCategory.modify, security, desc);
}
}
}
using bts.api;
using bts.api.core;
using bts.api.orderman;
using bts.shell;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui.controls;
using Spark.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using OrderPair = System.ValueTuple<bts.api.SecurityImmutable?, bts.api.orderman.OrderRecordImmutable>;
using TickDisplaySettingsImmutable = bts.api.domain.TickDisplaySettingsImmutable;
namespace bts.spark {
internal class Activity {
public SecurityImmutable? security;
public string message;
public ILogViewerCategory category;
}
class OrderActivitySummarizer : IDisposable {
readonly IBus _bus;
IReadOnlyDictionary<uint, SecurityMD> _security = new Dictionary<uint, SecurityMD>();
IDisposable _dispose;
static readonly IEqualityComparer<OrderRecordImmutable> _id_comparer =
bts.utils.EqualityComparer.Create<OrderRecordImmutable, uint>(a => a.Key);
public OrderActivitySummarizer(IBus bus, IObservable<TableUpdate<(SecurityImmutable? sec, OrderRecordImmutable order), uint>> orders) {
_bus = bus;
_dispose = Disposable.Create(
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(sec => _security = sec),
orders.Subscribe(orders_updated)
);
}
public void Dispose() => Disposable.dispose(ref _dispose);
#region formatting
SecurityImmutable? try_get_security(uint security_key) {
if (_security.TryGetValue(security_key, out SecurityMD md)) {
return md.security;
}
return null;
}
public static string format_order_success(
uint key, double price, double size, Side side, ContractSpecificationImmutable? spec) =>
$"({key}) {side.ToString().ToLower()} {size} @ {spec.format(price)}";
string format_order_fail(
double price,
double size,
Side side,
ContractSpecificationImmutable? spec,
string error) =>
$"Failed to {side.ToString().ToLower()} {size} @ {spec.format(price)}. {error}";
string format_cancel_success(OrderRecordImmutable order, ContractSpecificationImmutable? spec) {
return $"({order.Key}) Canceled {order.short_description(spec)}, was working {order.working_quantity()}";
}
string format_cancel_error(OrderRecordImmutable order, ContractSpecificationImmutable? spec, string error) {
return $"({order.Key}) Failed to cancel {order.short_description(spec)}: {error}";
}
public static string format_fill_summary(OrderRecordImmutable before, OrderRecordImmutable after) =>
_format_fill_summary(_header(after.Key), before, after);
public static string format_fill_summary(OrderRecordImmutable before, OrderRecordImmutable after, SecurityImmutable? security) =>
_format_fill_summary(_header(after.Key, security), before, after);
public static string format_status_change(uint key, OrderStatus old_status, OrderStatus new_status) =>
_format_status_change(_header(key), key, old_status, new_status);
public static string format_status_change(uint key, OrderStatus old_status, OrderStatus new_status, SecurityImmutable? security) =>
_format_status_change(_header(key, security), key, old_status, new_status);
// since changes are naggled, transition changes aren't always detected (e.g. a new order changes before observers are notified of its existence), so old_status might be null.
public static string format_status_change(uint key, OrderStatus? old_status, OrderStatus new_status, SecurityImmutable? security) {
string header = _header(key, security);
return old_status.HasValue ?
_format_status_change(header, key, old_status.Value, new_status) :
_format_status_change(header, key, new_status);
}
public static string order_completion_debrief(
OrderRecordImmutable order,
SecurityImmutable? security,
TickDisplaySettingsImmutable qty_display) =>
_format_order_completion(
_header(order.Key, security), order, security?.ContractSpec, qty_display);
public static string modified_description(OrderRecordImmutable old_order, OrderRecordImmutable new_order, ContractSpecificationImmutable? spec) {
double old_price = old_order.State.Price, new_price = new_order.State.Price;
uint old_size = old_order.State.Quantity, new_size = new_order.State.Quantity;
uint old_attached_target_key = old_order.attached_target_order_key(), new_attached_target_key = new_order.attached_target_order_key();
return StringBuilderEx.join(prefix, change_descs());
//
string prefix() => $"({old_order.Key}) Changed";
IEnumerable<string> change_descs() {
if (old_price != new_price)
yield return $"price from {spec.format(old_price)} to {spec.format(new_price)}";
if (old_size != new_size)
yield return $"size from {old_size} to {new_size}, working {new_order.working_quantity()}";
if (old_attached_target_key != new_attached_target_key)
yield return $"attached target from {old_attached_target_key} to {new_attached_target_key}";
}
}
static string _header(uint key) => $"({key})";
static string _header(uint key, SecurityImmutable? security) => $"({key}) {security.display_name()}";
static string _format_fill_summary(string header, OrderRecordImmutable before, OrderRecordImmutable after) {
int fb = (int)before.State.FilledQuantity;
int fa = (int)after.State.FilledQuantity;
return $"{header} {after.Def.Side.past_tense()} {(fa - fb)} ({fa} total), still working {after.working_quantity()}";
}
static string _format_status_change(string header, uint key, OrderStatus new_status) =>
$"{header} Status changed to {new_status}";
static string _format_status_change(string header, uint key, OrderStatus old_status, OrderStatus new_status) =>
$"{header} Status changed from {old_status} to {new_status}";
static string _format_order_completion(
string header,
OrderRecordImmutable order,
ContractSpecificationImmutable? spec,
TickDisplaySettingsImmutable qty_display) {
var filled_qty_str = qty_display.format(order.filled_quantity());
return $"{header} Completed: {order.side().past_tense()} {filled_qty_str}, " +
$"@ {spec.spec_or_default().format(order.avg_price())} (avg)";
}
#endregion
void orders_updated(TableUpdate<(SecurityImmutable? sec, OrderRecordImmutable order), uint> update) {
foreach (var o in update.added) {
if (!string.IsNullOrEmpty(o.order.State.LastMsg) && o.order.State.Created.ToDateTime().Date == DateTime.Today) {
if (o.sec.HasValue) {
log_status(ActivityLogCategory.warn, o.sec.Value, o.order.State.LastMsg);
}
}
}
foreach (var order in update.changes) {
var neworder = order.New.order;
var oldorder = order.Old.order;
var security = order.New.sec ?? order.Old.sec;
//
// description
var mod_desc = modified_description(oldorder, neworder, security?.ContractSpec);
if (!string.IsNullOrEmpty(mod_desc)) {
log_status(ActivityLogCategory.modify, security, mod_desc);
}
//
// filled quantity
if (oldorder.State.FilledQuantity != neworder.State.FilledQuantity && !neworder.has_parent()) {
log_status(ActivityLogCategory.fill, security, format_fill_summary(oldorder, neworder));
}
//
// status
if (oldorder.State.Status != neworder.State.Status && !neworder.has_parent()) {
log_status(ActivityLogCategory.modify, security,
format_status_change(neworder.Key, oldorder.State.Status, neworder.State.Status));
}
// last msg
if (oldorder.State.LastMsg != neworder.State.LastMsg) {
if (security.HasValue) {
log_status(ActivityLogCategory.warn, security, neworder.State.LastMsg);
}
}
}
}
public void log_status(ILogViewerCategory category, SecurityImmutable? security, string msg) {
_bus.post(SparkTags.LogActivity, new Activity { security = security, message = msg, category = category });
}
internal void on_send_order(
OrderDefinitionImmutable order, double price, double size, uint? key, string error) {
var security = try_get_security(order.SecurityKey);
log_status(ActivityLogCategory.order, security, key.HasValue ?
format_order_success(key.Value, price, size, order.Side, security?.ContractSpec) :
format_order_fail(price, size, order.Side, security?.ContractSpec, error));
}
internal void on_cancel_order(OrderRecordImmutable order, bool success, string error) {
var security = try_get_security(order.Def.SecurityKey);
log_status(ActivityLogCategory.cancel, security, success ?
format_cancel_success(order, security?.ContractSpec) :
format_cancel_error(order, security?.ContractSpec, error));
}
internal void update_order(
OrderRecordImmutable order, double price, double size, bool algo_changed) =>
_log_order_update(order, price, size, algo_changed, "change from");
internal void update_order_failed(OrderRecordImmutable order, double price, double size, bool algo_changed) =>
_log_order_update(order, price, size, algo_changed, "failed to change from");
void _log_order_update(
OrderRecordImmutable order,
double price,
double size,
bool algo_changed,
string change_desc_prefix
) {
if (price == order.price() && size == order.working_quantity() && !algo_changed) {
// nothing actually changed
return;
}
const string algo_change_text = "change algo parameters";
var security = try_get_security(order.Def.SecurityKey);
var change_descs = new List<string>(1);
TickDisplaySettingsImmutable? price_display =
security.HasValue && security.Value.HasContractSpec ?
security.Value.ContractSpec.tick_factors().to_api_tick_display_settingsi() : null;
var price_qty_change_desc = OrderUtils.update_description(
order, price, size, price_display);
if (!price_qty_change_desc.is_null_or_empty()) {
change_descs.Add(price_qty_change_desc);
}
if (algo_changed) {
change_descs.Add(algo_change_text);
}
var desc = $"({order.Key}) {change_desc_prefix} {"; ".join(change_descs)}";
log_status(ActivityLogCategory.modify, security, desc);
}
}
}
@@ -1,34 +1,34 @@
using bts.api;
using bts.spark;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace mbts.ui.spark {
internal static class OrderIconFactory {
static OrderIconFactory() {
_bid_pen.Freeze();
_ask_pen.Freeze();
}
private static readonly Pen _bid_pen = new Pen(BTSColors.MarketData.bid_brush, 4), _ask_pen = new Pen(BTSColors.MarketData.ask_brush, 4);
private static readonly ConcurrentDictionary<uint, Brush> _algo_palette = new ConcurrentDictionary<uint, Brush>();
public static ImageSource Create(OrderMetadata metadata, Side side) {
Brush brush = _algo_palette.GetOrAdd(metadata.Color, c => {
Brush b = new SolidColorBrush(c.rgb_color());
b.Freeze();
return b;
});
var group = new DrawingGroup();
group.Children.Add(new GeometryDrawing(brush, side == Side.Buy ? _bid_pen : _ask_pen, new RectangleGeometry(new Rect(0, 0, 40, 28), 3, 3)));
return new DrawingImage { Drawing = group };
}
}
}
using bts.api;
using bts.spark;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace mbts.ui.spark {
internal static class OrderIconFactory {
static OrderIconFactory() {
_bid_pen.Freeze();
_ask_pen.Freeze();
}
private static readonly Pen _bid_pen = new Pen(BTSColors.MarketData.bid_brush, 4), _ask_pen = new Pen(BTSColors.MarketData.ask_brush, 4);
private static readonly ConcurrentDictionary<uint, Brush> _algo_palette = new ConcurrentDictionary<uint, Brush>();
public static ImageSource Create(OrderMetadata metadata, Side side) {
Brush brush = _algo_palette.GetOrAdd(metadata.Color, c => {
Brush b = new SolidColorBrush(c.rgb_color());
b.Freeze();
return b;
});
var group = new DrawingGroup();
group.Children.Add(new GeometryDrawing(brush, side == Side.Buy ? _bid_pen : _ask_pen, new RectangleGeometry(new Rect(0, 0, 40, 28), 3, 3)));
return new DrawingImage { Drawing = group };
}
}
}
@@ -1,160 +1,160 @@
using bts;
using bts.api;
using bts.api.orderman;
using bts.utils;
using mbts.spark.utilities;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
namespace mbts.spark {
/// <summary>
/// Monitors selected rows, and determines whether or not they are all active-orders, with common parameters.
/// </summary>
class OrderParamsAggregator {
readonly TimeSpan _throttle;
readonly CommonValueGroupMonitor<CustomOrderDefinitionImmutable, CustomOrderDefinitionImmutable> _algo_def_mon =
new CommonValueGroupMonitor<CustomOrderDefinitionImmutable, CustomOrderDefinitionImmutable>(
ask => ask,
OrderDataHelper.EqualsHelpers.custom_order_defi_structure_comparer,
OrderDataHelper.EqualsHelpers.custom_order_defi_comparer);
readonly CommonValueMonitor<SecurityImmutable?> _spreader_security_mon = new CommonValueMonitor<SecurityImmutable?>();
readonly CommonValueMonitor<SecurityImmutable?> _security_mon = new CommonValueMonitor<SecurityImmutable?>();
readonly CommonValueMonitor<double> _price_mon = new CommonValueMonitor<double>(double.NaN);
readonly CommonValueMonitor<double> _size_mon = new();
readonly CommonValueMonitor<AccountTuple> _account_mon = new CommonValueMonitor<AccountTuple>();
readonly CommonValueMonitor<bool> _all_active_mon = new CommonValueMonitor<bool>();
readonly CommonValueMonitorMultiplexor<OrderSecurityAlgo> _group_mon = new CommonValueMonitorMultiplexor<OrderSecurityAlgo>();
readonly DynamicOrderHelper _dyn_helper;
readonly Func<uint, OrderSecurityAlgo?> _lookup_order;
public OrderParamsAggregator(DynamicOrderHelper dyn_helper, TimeSpan throttle, Func<uint, OrderSecurityAlgo?> lookup_order) {
_dyn_helper = dyn_helper;
_throttle = throttle;
_lookup_order = lookup_order;
_group_mon.add_member_mon(_algo_def_mon, _build_algo_tree);
_group_mon.add_member_mon(_spreader_security_mon, _get_spreader_sec, s => s.HasValue);
_group_mon.add_member_mon(_security_mon, osam => osam.security, s => s.HasValue);
_group_mon.add_member_mon(_price_mon, osam => osam.order.price());
_group_mon.add_member_mon(_size_mon, osam => osam.order.quantity());
_group_mon.add_member_mon(_account_mon, osam => new AccountTuple(osam.order.account_id(), osam.order.account()));
_group_mon.add_member_mon(_all_active_mon, osam => osam.order.is_active());
}
public SecurityImmutable? security => _security_mon.common_value;
public SecurityImmutable? spreader_security => _spreader_security_mon.common_value;
public double price => _price_mon.try_get_common_value(out double p) ? p : double.NaN;
public double? size => _size_mon.common_value_nullable();
public AccountTuple? account => _account_mon.common_value_nullable();
public bool all_active => _all_active_mon.common_value;
/// <summary>
/// Notifies observers when the set of <see cref="CustomOrderDefinitionImmutable"/>s changes.
/// The algos in the set are the distinct group of algos for the selected <see cref="OrderSecurityAlgo"/>s.
/// </summary>
// Note: this might be better optimized by adding a 'begin_reset' method to CommonValueMonitor that records the current state,
// and accumulates subsequent changes into a batch change. If the 'begin_reset' method returned an IDisposable,
// the batch change object could be examined when Dispose is called,
// to avoid firing the common_values_changed event if the accumulated changes cancel each other out.
// However, such a change would be non-trivial, so this method will suffice for now.
// It should perform fine in practice, even with a large number of selected orders,
// because the input is throttled.
public IObservable<IEnumerable<CustomOrderDefinitionImmutable>> subscribe_algos() =>
Observable.Create<ImmutableHashSet<CustomOrderDefinitionImmutable>>(o => {
var last_algos = ImmutableHashSet.Create(OrderDataHelper.EqualsHelpers.custom_order_defi_comparer);
return _algo_def_mon
.SubscribeThrottled(_throttle, (ask, vals) => vals, Array.Empty<CustomOrderDefinitionImmutable>())
.Subscribe(algos => {
// only notify the observer if the algo set has actually changed.
if (algos.Count != last_algos.Count || !algos.All(last_algos.Contains))
o.OnNext(last_algos = ImmutableHashSet.CreateRange(OrderDataHelper.EqualsHelpers.custom_order_defi_comparer, algos));
});
});
public IObservable<SecurityImmutable?> subscribe_spreader_security() => _spreader_security_mon.SubscribeThrottled(_throttle);
public IObservable<SecurityImmutable?> subscribe_security() => _security_mon.SubscribeThrottled(_throttle);
public IObservable<double> subscribe_price() => _price_mon.SubscribeThrottled(_throttle);
public IObservable<double?> subscribe_size() => _size_mon.SubscribeThrottledNullable(_throttle);
public IObservable<AccountTuple?> subscribe_account() => _account_mon.SubscribeThrottledNullable(_throttle);
public IObservable<bool> subscribe_all_active() => _all_active_mon.SubscribeThrottled(_throttle);
public IObservable<Plurality> subscribe_security_plurality() => _security_mon.SubscribePluralityThrottled(_throttle);
public IDisposable defer_updates() =>
_group_mon.defer_updates();
public void Add(OrderSecurityAlgo order_tuple) =>
_group_mon.add(order_tuple);
public void Remove(OrderSecurityAlgo order_tuple) =>
_group_mon.remove(order_tuple);
public void Clear() =>
_group_mon.clear();
static SecurityImmutable? _get_spreader_sec(OrderSecurityAlgo osam) =>
OrderDataHelper.enumerate_algo_hierarchy(osam.order.Def.CustomDef.CloneToMutable())
.Any(algo => algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Spreader) ?
osam.security : null;
CustomOrderDefinitionImmutable _build_algo_tree(OrderSecurityAlgo root) =>
root.order.Def.AttachTargetOrderKey == 0 ?
root.order.Def.CustomDef :
new CustomOrderDefinitionImmutable(_add_attached_targets(root.order.Def.CustomDef.CloneToMutable(), root.order.Key, root.order.Def.AttachTargetOrderKey));
CustomOrderDefinition _add_attached_targets(CustomOrderDefinition mroot, uint root_id, uint attached_target) {
var lin = _lineage(root_id, attached_target);
if (lin != null && lin.Count > 0) {
var mparent = mroot;
var pcad = OrderDataHelper.get_primary_child_algo_data(mparent);
if (pcad.HasValue) {
do {
var mchild = lin.Pop().order.Def.CustomDef.CloneToMutable();
pcad.Value.assign_child(mparent, mchild);
mparent = mchild;
pcad = OrderDataHelper.get_primary_child_algo_data(mparent);
} while (lin.Count > 0 && pcad.HasValue);
}
}
return mroot;
}
Stack<OrderSecurityAlgo> _lineage(uint root, uint target) {
// check args/preconditions
if (root == 0) throw new ArgumentException("Cannot be zero.", nameof(root));
if (target == 0) throw new ArgumentException("Cannot be zero.", nameof(target));
if (target == root) return null;
var inner = _lookup_order(target);
if (!inner.HasValue)
return null;
// if 'target' has an attach target iteself (nested), push the nested lineage onto the stack first,
// then push the original target onto the stack, and try to hop up each algo-generation
// via parent-key lookups until we reach the root.
// **If the lineage is disjointed, and we never reach the root, return null**
var ival = inner.Value;
var lin_stack = ival.order.attached_target_order_key() != 0 ?
_lineage(ival.order.Key, ival.order.attached_target_order_key()) ?? new Stack<OrderSecurityAlgo>() :
new Stack<OrderSecurityAlgo>();
lin_stack.Push(ival);
target = ival.order.ParentKey;
while (target != root) {
if (target == 0) {
// **disjointed lineage**
return null;
}
inner = _lookup_order(target);
if (!inner.HasValue) {
// **disjointed lineage**
return null;
}
ival = inner.Value;
lin_stack.Push(ival);
target = ival.order.ParentKey;
}
return lin_stack;
}
}
}
using bts;
using bts.api;
using bts.api.orderman;
using bts.utils;
using mbts.spark.utilities;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
namespace mbts.spark {
/// <summary>
/// Monitors selected rows, and determines whether or not they are all active-orders, with common parameters.
/// </summary>
class OrderParamsAggregator {
readonly TimeSpan _throttle;
readonly CommonValueGroupMonitor<CustomOrderDefinitionImmutable, CustomOrderDefinitionImmutable> _algo_def_mon =
new CommonValueGroupMonitor<CustomOrderDefinitionImmutable, CustomOrderDefinitionImmutable>(
ask => ask,
OrderDataHelper.EqualsHelpers.custom_order_defi_structure_comparer,
OrderDataHelper.EqualsHelpers.custom_order_defi_comparer);
readonly CommonValueMonitor<SecurityImmutable?> _spreader_security_mon = new CommonValueMonitor<SecurityImmutable?>();
readonly CommonValueMonitor<SecurityImmutable?> _security_mon = new CommonValueMonitor<SecurityImmutable?>();
readonly CommonValueMonitor<double> _price_mon = new CommonValueMonitor<double>(double.NaN);
readonly CommonValueMonitor<double> _size_mon = new();
readonly CommonValueMonitor<AccountTuple> _account_mon = new CommonValueMonitor<AccountTuple>();
readonly CommonValueMonitor<bool> _all_active_mon = new CommonValueMonitor<bool>();
readonly CommonValueMonitorMultiplexor<OrderSecurityAlgo> _group_mon = new CommonValueMonitorMultiplexor<OrderSecurityAlgo>();
readonly DynamicOrderHelper _dyn_helper;
readonly Func<uint, OrderSecurityAlgo?> _lookup_order;
public OrderParamsAggregator(DynamicOrderHelper dyn_helper, TimeSpan throttle, Func<uint, OrderSecurityAlgo?> lookup_order) {
_dyn_helper = dyn_helper;
_throttle = throttle;
_lookup_order = lookup_order;
_group_mon.add_member_mon(_algo_def_mon, _build_algo_tree);
_group_mon.add_member_mon(_spreader_security_mon, _get_spreader_sec, s => s.HasValue);
_group_mon.add_member_mon(_security_mon, osam => osam.security, s => s.HasValue);
_group_mon.add_member_mon(_price_mon, osam => osam.order.price());
_group_mon.add_member_mon(_size_mon, osam => osam.order.quantity());
_group_mon.add_member_mon(_account_mon, osam => new AccountTuple(osam.order.account_id(), osam.order.account()));
_group_mon.add_member_mon(_all_active_mon, osam => osam.order.is_active());
}
public SecurityImmutable? security => _security_mon.common_value;
public SecurityImmutable? spreader_security => _spreader_security_mon.common_value;
public double price => _price_mon.try_get_common_value(out double p) ? p : double.NaN;
public double? size => _size_mon.common_value_nullable();
public AccountTuple? account => _account_mon.common_value_nullable();
public bool all_active => _all_active_mon.common_value;
/// <summary>
/// Notifies observers when the set of <see cref="CustomOrderDefinitionImmutable"/>s changes.
/// The algos in the set are the distinct group of algos for the selected <see cref="OrderSecurityAlgo"/>s.
/// </summary>
// Note: this might be better optimized by adding a 'begin_reset' method to CommonValueMonitor that records the current state,
// and accumulates subsequent changes into a batch change. If the 'begin_reset' method returned an IDisposable,
// the batch change object could be examined when Dispose is called,
// to avoid firing the common_values_changed event if the accumulated changes cancel each other out.
// However, such a change would be non-trivial, so this method will suffice for now.
// It should perform fine in practice, even with a large number of selected orders,
// because the input is throttled.
public IObservable<IEnumerable<CustomOrderDefinitionImmutable>> subscribe_algos() =>
Observable.Create<ImmutableHashSet<CustomOrderDefinitionImmutable>>(o => {
var last_algos = ImmutableHashSet.Create(OrderDataHelper.EqualsHelpers.custom_order_defi_comparer);
return _algo_def_mon
.SubscribeThrottled(_throttle, (ask, vals) => vals, Array.Empty<CustomOrderDefinitionImmutable>())
.Subscribe(algos => {
// only notify the observer if the algo set has actually changed.
if (algos.Count != last_algos.Count || !algos.All(last_algos.Contains))
o.OnNext(last_algos = ImmutableHashSet.CreateRange(OrderDataHelper.EqualsHelpers.custom_order_defi_comparer, algos));
});
});
public IObservable<SecurityImmutable?> subscribe_spreader_security() => _spreader_security_mon.SubscribeThrottled(_throttle);
public IObservable<SecurityImmutable?> subscribe_security() => _security_mon.SubscribeThrottled(_throttle);
public IObservable<double> subscribe_price() => _price_mon.SubscribeThrottled(_throttle);
public IObservable<double?> subscribe_size() => _size_mon.SubscribeThrottledNullable(_throttle);
public IObservable<AccountTuple?> subscribe_account() => _account_mon.SubscribeThrottledNullable(_throttle);
public IObservable<bool> subscribe_all_active() => _all_active_mon.SubscribeThrottled(_throttle);
public IObservable<Plurality> subscribe_security_plurality() => _security_mon.SubscribePluralityThrottled(_throttle);
public IDisposable defer_updates() =>
_group_mon.defer_updates();
public void Add(OrderSecurityAlgo order_tuple) =>
_group_mon.add(order_tuple);
public void Remove(OrderSecurityAlgo order_tuple) =>
_group_mon.remove(order_tuple);
public void Clear() =>
_group_mon.clear();
static SecurityImmutable? _get_spreader_sec(OrderSecurityAlgo osam) =>
OrderDataHelper.enumerate_algo_hierarchy(osam.order.Def.CustomDef.CloneToMutable())
.Any(algo => algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Spreader) ?
osam.security : null;
CustomOrderDefinitionImmutable _build_algo_tree(OrderSecurityAlgo root) =>
root.order.Def.AttachTargetOrderKey == 0 ?
root.order.Def.CustomDef :
new CustomOrderDefinitionImmutable(_add_attached_targets(root.order.Def.CustomDef.CloneToMutable(), root.order.Key, root.order.Def.AttachTargetOrderKey));
CustomOrderDefinition _add_attached_targets(CustomOrderDefinition mroot, uint root_id, uint attached_target) {
var lin = _lineage(root_id, attached_target);
if (lin != null && lin.Count > 0) {
var mparent = mroot;
var pcad = OrderDataHelper.get_primary_child_algo_data(mparent);
if (pcad.HasValue) {
do {
var mchild = lin.Pop().order.Def.CustomDef.CloneToMutable();
pcad.Value.assign_child(mparent, mchild);
mparent = mchild;
pcad = OrderDataHelper.get_primary_child_algo_data(mparent);
} while (lin.Count > 0 && pcad.HasValue);
}
}
return mroot;
}
Stack<OrderSecurityAlgo> _lineage(uint root, uint target) {
// check args/preconditions
if (root == 0) throw new ArgumentException("Cannot be zero.", nameof(root));
if (target == 0) throw new ArgumentException("Cannot be zero.", nameof(target));
if (target == root) return null;
var inner = _lookup_order(target);
if (!inner.HasValue)
return null;
// if 'target' has an attach target iteself (nested), push the nested lineage onto the stack first,
// then push the original target onto the stack, and try to hop up each algo-generation
// via parent-key lookups until we reach the root.
// **If the lineage is disjointed, and we never reach the root, return null**
var ival = inner.Value;
var lin_stack = ival.order.attached_target_order_key() != 0 ?
_lineage(ival.order.Key, ival.order.attached_target_order_key()) ?? new Stack<OrderSecurityAlgo>() :
new Stack<OrderSecurityAlgo>();
lin_stack.Push(ival);
target = ival.order.ParentKey;
while (target != root) {
if (target == 0) {
// **disjointed lineage**
return null;
}
inner = _lookup_order(target);
if (!inner.HasValue) {
// **disjointed lineage**
return null;
}
ival = inner.Value;
lin_stack.Push(ival);
target = ival.order.ParentKey;
}
return lin_stack;
}
}
}
@@ -1,157 +1,157 @@
using bts.utils;
using mbts.spark;
using System;
namespace bts.api.orderman {
readonly struct OrderSecurity : IEquatable<OrderSecurity> {
public OrderSecurity((SecurityImmutable? sec, OrderRecordImmutable order) t) {
security = t.sec;
order = t.order;
piq = null;
algo = null;
meta = null;
}
public OrderSecurity(SecurityImmutable? sec, OrderRecordImmutable order, uint? piq) {
this.security = sec;
this.order = order;
this.piq = piq;
algo = null;
meta = null;
}
public OrderSecurity(SecurityImmutable? sec, OrderRecordImmutable order, uint? piq, AlgoData? algo, AlgoDefinitionMetadata? meta) {
this.security = sec;
this.order = order;
this.piq = piq;
this.algo = algo;
this.meta = meta;
}
public static implicit operator OrderSecurity(ValueTuple<SecurityImmutable?, OrderRecordImmutable> x) => new OrderSecurity(x);
public SecurityImmutable? security { get; }
public OrderRecordImmutable order { get; }
public uint? piq { get; }
public AlgoData? algo { get; }
public AlgoDefinitionMetadata? meta { get; }
public bool Equals(OrderSecurity other) =>
order == other.order &&
security == other.security &&
piq == other.piq &&
algo == other.algo &&
meta == other.meta;
public override bool Equals(object obj) => EqualityComparer.equals_obj(this, obj);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
order.GetHashCode(),
security?.GetHashCode() ?? 0,
piq?.GetHashCode() ?? 0,
algo?.GetHashCode() ?? 0,
meta?.GetHashCode() ?? 0);
public static bool operator ==(OrderSecurity left, OrderSecurity right) => left.Equals(right);
public static bool operator !=(OrderSecurity left, OrderSecurity right) => !left.Equals(right);
}
// This is a core data type on the front end, so data validation is strict, as bad values may yield weird/difficult to track bugs.
// Descriptive factory methods are favored over public constructors, as they are less likely to be misused.
readonly struct OrderSecurityAlgo : IEquatable<OrderSecurityAlgo> {
public readonly OrderRecordImmutable order;
public readonly SecurityImmutable? security;
public readonly AlgoData algo;
public readonly AlgoDefinitionMetadata? meta;
public readonly uint? piq;
OrderSecurityAlgo(OrderRecordImmutable order, SecurityImmutable? security, AlgoData algo, AlgoDefinitionMetadata? meta, uint? piq) {
this.order = order;
this.security = security;
this.algo = algo;
this.meta = meta;
this.piq = piq;
}
public static OrderSecurityAlgo from_order_security(OrderSecurity os) =>
new OrderSecurityAlgo(
os.order,
os.security,
os.algo ?? AlgoData.from_custom_def(os.order.Def.CustomDef),
os.meta,
os.piq);
public bool Equals(OrderSecurityAlgo other) =>
order == other.order &&
piq == other.piq &&
security == other.security &&
algo == other.algo;
public override bool Equals(object obj) =>
obj is OrderSecurityAlgo other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.auto_phase(
order.Key.GetHashCode(),
piq?.GetHashCode() ?? 0,
security.HasValue ? security.Value.Id.GetHashCode() : 0,
algo.GetHashCode());
public static bool operator ==(OrderSecurityAlgo a, OrderSecurityAlgo b) => a.Equals(b);
public static bool operator !=(OrderSecurityAlgo a, OrderSecurityAlgo b) => !a.Equals(b);
}
// This is a core data type on the front end, so data validation is strict, as bad values may yield weird/difficult to track bugs.
// Descriptive factory methods are favored over public constructors, as they are less likely to be misused.
readonly struct OrderSecurityRootPair : IEquatable<OrderSecurityRootPair> {
public readonly OrderSecurityAlgo order;
readonly OrderSecurityAlgo? _root;
OrderSecurityRootPair(OrderSecurityAlgo order) {
this.order = order;
_root = null;
}
OrderSecurityRootPair(OrderSecurityAlgo order, OrderSecurityAlgo? root) {
this.order = order;
_root = root;
}
public OrderSecurityAlgo root => _root ?? order;
public static OrderSecurityRootPair from_order_security(OrderSecurity order) =>
new OrderSecurityRootPair(OrderSecurityAlgo.from_order_security(order));
public static OrderSecurityRootPair from_order_security(OrderSecurity order, OrderSecurity root) =>
new OrderSecurityRootPair(
OrderSecurityAlgo.from_order_security(order),
OrderSecurityAlgo.from_order_security(root));
public bool Equals(OrderSecurityRootPair other) =>
order == other.order &&
_root == other._root;
public override bool Equals(object obj) =>
obj is OrderSecurityRootPair other && Equals(other);
public override int GetHashCode() =>
_root.HasValue ?
(order.GetHashCode() % (1 << 15)) | (_root.Value.GetHashCode() % (1 << 15) << 16) :
order.GetHashCode() % (1 << 15);
public static bool operator ==(OrderSecurityRootPair a, OrderSecurityRootPair b) => a.Equals(b);
public static bool operator !=(OrderSecurityRootPair a, OrderSecurityRootPair b) => !a.Equals(b);
}
static class OrderSecurityHelper {
public static readonly Func<OrderSecurity, uint>
key_fn = o => o.order.Key,
parent_key_fn = o => o.order.ParentKey;
public static readonly Func<OrderSecurityRootPair, uint>
root_pair_key_fn = o => o.order.order.Key,
root_pair_parent_key_fn = o => o.order.order.ParentKey;
}
}
using bts.utils;
using mbts.spark;
using System;
namespace bts.api.orderman {
readonly struct OrderSecurity : IEquatable<OrderSecurity> {
public OrderSecurity((SecurityImmutable? sec, OrderRecordImmutable order) t) {
security = t.sec;
order = t.order;
piq = null;
algo = null;
meta = null;
}
public OrderSecurity(SecurityImmutable? sec, OrderRecordImmutable order, uint? piq) {
this.security = sec;
this.order = order;
this.piq = piq;
algo = null;
meta = null;
}
public OrderSecurity(SecurityImmutable? sec, OrderRecordImmutable order, uint? piq, AlgoData? algo, AlgoDefinitionMetadata? meta) {
this.security = sec;
this.order = order;
this.piq = piq;
this.algo = algo;
this.meta = meta;
}
public static implicit operator OrderSecurity(ValueTuple<SecurityImmutable?, OrderRecordImmutable> x) => new OrderSecurity(x);
public SecurityImmutable? security { get; }
public OrderRecordImmutable order { get; }
public uint? piq { get; }
public AlgoData? algo { get; }
public AlgoDefinitionMetadata? meta { get; }
public bool Equals(OrderSecurity other) =>
order == other.order &&
security == other.security &&
piq == other.piq &&
algo == other.algo &&
meta == other.meta;
public override bool Equals(object obj) => EqualityComparer.equals_obj(this, obj);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
order.GetHashCode(),
security?.GetHashCode() ?? 0,
piq?.GetHashCode() ?? 0,
algo?.GetHashCode() ?? 0,
meta?.GetHashCode() ?? 0);
public static bool operator ==(OrderSecurity left, OrderSecurity right) => left.Equals(right);
public static bool operator !=(OrderSecurity left, OrderSecurity right) => !left.Equals(right);
}
// This is a core data type on the front end, so data validation is strict, as bad values may yield weird/difficult to track bugs.
// Descriptive factory methods are favored over public constructors, as they are less likely to be misused.
readonly struct OrderSecurityAlgo : IEquatable<OrderSecurityAlgo> {
public readonly OrderRecordImmutable order;
public readonly SecurityImmutable? security;
public readonly AlgoData algo;
public readonly AlgoDefinitionMetadata? meta;
public readonly uint? piq;
OrderSecurityAlgo(OrderRecordImmutable order, SecurityImmutable? security, AlgoData algo, AlgoDefinitionMetadata? meta, uint? piq) {
this.order = order;
this.security = security;
this.algo = algo;
this.meta = meta;
this.piq = piq;
}
public static OrderSecurityAlgo from_order_security(OrderSecurity os) =>
new OrderSecurityAlgo(
os.order,
os.security,
os.algo ?? AlgoData.from_custom_def(os.order.Def.CustomDef),
os.meta,
os.piq);
public bool Equals(OrderSecurityAlgo other) =>
order == other.order &&
piq == other.piq &&
security == other.security &&
algo == other.algo;
public override bool Equals(object obj) =>
obj is OrderSecurityAlgo other && Equals(other);
public override int GetHashCode() =>
EqualityComparer.auto_phase(
order.Key.GetHashCode(),
piq?.GetHashCode() ?? 0,
security.HasValue ? security.Value.Id.GetHashCode() : 0,
algo.GetHashCode());
public static bool operator ==(OrderSecurityAlgo a, OrderSecurityAlgo b) => a.Equals(b);
public static bool operator !=(OrderSecurityAlgo a, OrderSecurityAlgo b) => !a.Equals(b);
}
// This is a core data type on the front end, so data validation is strict, as bad values may yield weird/difficult to track bugs.
// Descriptive factory methods are favored over public constructors, as they are less likely to be misused.
readonly struct OrderSecurityRootPair : IEquatable<OrderSecurityRootPair> {
public readonly OrderSecurityAlgo order;
readonly OrderSecurityAlgo? _root;
OrderSecurityRootPair(OrderSecurityAlgo order) {
this.order = order;
_root = null;
}
OrderSecurityRootPair(OrderSecurityAlgo order, OrderSecurityAlgo? root) {
this.order = order;
_root = root;
}
public OrderSecurityAlgo root => _root ?? order;
public static OrderSecurityRootPair from_order_security(OrderSecurity order) =>
new OrderSecurityRootPair(OrderSecurityAlgo.from_order_security(order));
public static OrderSecurityRootPair from_order_security(OrderSecurity order, OrderSecurity root) =>
new OrderSecurityRootPair(
OrderSecurityAlgo.from_order_security(order),
OrderSecurityAlgo.from_order_security(root));
public bool Equals(OrderSecurityRootPair other) =>
order == other.order &&
_root == other._root;
public override bool Equals(object obj) =>
obj is OrderSecurityRootPair other && Equals(other);
public override int GetHashCode() =>
_root.HasValue ?
(order.GetHashCode() % (1 << 15)) | (_root.Value.GetHashCode() % (1 << 15) << 16) :
order.GetHashCode() % (1 << 15);
public static bool operator ==(OrderSecurityRootPair a, OrderSecurityRootPair b) => a.Equals(b);
public static bool operator !=(OrderSecurityRootPair a, OrderSecurityRootPair b) => !a.Equals(b);
}
static class OrderSecurityHelper {
public static readonly Func<OrderSecurity, uint>
key_fn = o => o.order.Key,
parent_key_fn = o => o.order.ParentKey;
public static readonly Func<OrderSecurityRootPair, uint>
root_pair_key_fn = o => o.order.order.Key,
root_pair_parent_key_fn = o => o.order.order.ParentKey;
}
}
@@ -1,483 +1,483 @@
using bts;
using bts.api;
using bts.api.orderman;
using bts.co2;
using bts.spark;
using bts.utils;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.utilities;
using mbts.ui.utility;
using mbts.utils;
using Spark.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Media;
using static mbts.ui.utilities.DisplayNameHelper;
namespace mbts.spark {
static class OrderUtils {
public static readonly Func<OrderRecordImmutable, uint> RecordKeySelector = o => o.Key;
#region OrderStateImmutable extension methods
public static double quantity(this OrderStateImmutable state) =>
Convert.ToDouble(state.Quantity) +
(state.HasFractionalQuantity ? state.FractionalQuantity.Fraction : 0.0);
public static double filled_quantity(this OrderStateImmutable state) =>
Convert.ToDouble(state.FilledQuantity) +
(state.HasFractionalFilledQuantity ? state.FractionalFilledQuantity.Fraction : 0.0);
public static double working_quantity(this OrderStateImmutable state) =>
state.quantity() - state.filled_quantity();
public static double fill_ratio(this OrderStateImmutable state) =>
state.filled_quantity() / state.quantity();
#endregion
public static double filled_quantity(this OrderRecordImmutable order) =>
order.HasState ? order.State.filled_quantity() : 0.0;
public static double fill_ratio(this OrderRecordImmutable order) =>
order.HasState ? order.State.FilledQuantity / (double)order.State.Quantity : double.NaN;
public static double quantity(this OrderRecordImmutable order) =>
order.HasState ? order.State.quantity() : 0;
public static double working_quantity(this OrderRecordImmutable order) =>
order.HasState ? order.State.working_quantity() : 0;
public static double price(this OrderRecordImmutable order) =>
order.HasState ? order.State.Price : double.NaN;
public static double ui_price(this OrderRecordImmutable order) {
if (order.HasState) {
if (order.order_type() == CustomOrderDefinition.CustomOneofCase.Stop && !order.stop_or_stop_limit_has_been_triggered()) {
return order.stop_price();
}
if (order.order_type() == CustomOrderDefinition.CustomOneofCase.StopLimit && !order.stop_or_stop_limit_has_been_triggered()) {
return order.stop_limit_price();
}
return order.State.Price;
}
return double.NaN;
}
public static double stop_price(this OrderRecordImmutable order) =>
order.HasState && order.State.HasStop && order.State.Stop.HasNativeState ? order.State.Stop.NativeState.StopPx : double.NaN;
public static double stop_limit_price(this OrderRecordImmutable order) =>
order.HasState && order.State.HasStopLimit && order.State.StopLimit.HasNativeState ? order.State.StopLimit.NativeState.StopPx : double.NaN;
public static double avg_price(this OrderRecordImmutable order) =>
order.HasState ? order.State.AvgPrice : double.NaN;
public static double last_price(this OrderRecordImmutable order) =>
order.HasState ? order.State.LastPrice : double.NaN;
public static string last_msg(this OrderRecordImmutable order) =>
order.HasState ? order.State.LastMsg : string.Empty;
public static IReadOnlyDictionary<string, DynamicOrderParam> dynamic_custom_state(this OrderRecordImmutable order) =>
order.HasState && order.State.HasDynamic ?
(order.State.Dynamic.CustomState ?? DictionaryHelper.empty<string, DynamicOrderParam>()) :
DictionaryHelper.empty<string, DynamicOrderParam>();
public static DateTime created(this OrderRecordImmutable order) =>
order.HasState && order.State.HasCreated ? order.State.Created.ToDateTime().ToLocalTime() : DateTime.MinValue;
public static DateTime last_change(this OrderRecordImmutable order) =>
order.HasState && order.State.HasLastChange ? order.State.LastChange.ToDateTime().ToLocalTime() : DateTime.MinValue;
public static OrderStatus status(this OrderRecordImmutable order) =>
order.HasState ? order.State.Status : OrderStatus.Error;
public static uint security_key(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.SecurityKey : 0u;
public static Side side(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.Side : Side.Undefined;
public static string user(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.User : string.Empty;
public static uint user_id(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.UserId : 0;
public static UserTuple user_tuple(this OrderRecordImmutable order) =>
new(order.user_id(), order.user());
public static string account(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.Account : string.Empty;
public static uint account_id(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.AccountId : 0;
public static CustomOrderDefinition.CustomOneofCase order_type(this OrderRecordImmutable order) =>
order.HasDef && order.Def.HasCustomDef ? order.Def.CustomDef.CustomCase : CustomOrderDefinition.CustomOneofCase.None;
/// <summary>
/// Unwraps all <see cref="CompositeOrderDefinition"/>s (if any) to return the underlying order type.
/// </summary>
/// <param name="order">The order who's underlying algo will be returned.</param>
/// <returns>The first non-composite algo definition in the specified <see cref="CustomOrderDefinition"/>.</returns>
public static CustomOrderDefinition.CustomOneofCase actual_order_type(this OrderRecordImmutable order) {
if (!order.HasDef || !order.Def.HasCustomDef)
return CustomOrderDefinition.CustomOneofCase.None;
var cdef = order.Def.CustomDef;
while (cdef.CustomCase == CustomOrderDefinition.CustomOneofCase.Composite) {
if (!cdef.Composite.HasDefinition)
return CustomOrderDefinition.CustomOneofCase.None;
cdef = cdef.Composite.Definition;
}
return cdef.CustomCase;
}
public static string dynamic_order_type_display_name(this OrderRecordImmutable order) =>
order.HasDef && order.Def.HasCustomDef && order.Def.CustomDef.HasDynamic ? order.Def.CustomDef.Dynamic.DynamicOrderType : string.Empty;
public static uint algo_template_key(this OrderRecordImmutable order) =>
order.HasDef && order.Def.HasCustomDef && order.Def.CustomDef.CustomCase == CustomOrderDefinition.CustomOneofCase.Composite ?
order.Def.CustomDef.Composite.AlgoTemplateKey : 0u;
/// <summary>
/// If the definitions is a <see cref="CompositeOrderDefinition"/>, returns the underlying algo, otherwise returns <paramref name="custom_def"/> iteself.
/// </summary>
/// <param name="custom_def">The definition to for which the underlying algo will be returned.</param>
/// <returns>The first non-composite algo definition in the specified <see cref="CustomOrderDefinition"/>.</returns>
public static CustomOrderDefinitionImmutable actual(this CustomOrderDefinitionImmutable custom_def) {
while (custom_def.CustomCase == CustomOrderDefinition.CustomOneofCase.Composite)
custom_def = custom_def.Composite.Definition;
return custom_def;
}
public static OrderSubType sub_type(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.SubType : OrderSubType.None;
public static TimeInForce time_in_force(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.TimeInForce : TimeInForce.Undefined;
public static uint attached_target_order_key(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.AttachTargetOrderKey : 0;
public static bool is_exchange_order(this OrderRecordImmutable order) {
if (order.HasDef && order.Def.HasCustomDef) {
switch (order.Def.CustomDef.CustomCase) {
case CustomOrderDefinition.CustomOneofCase.Limit:
case CustomOrderDefinition.CustomOneofCase.Market:
case CustomOrderDefinition.CustomOneofCase.Stop:
case CustomOrderDefinition.CustomOneofCase.StopLimit:
case CustomOrderDefinition.CustomOneofCase.GlbxIceberg: return true;
}
}
return false;
}
public static bool can_attach(this OrderRecordImmutable order) =>
order.working_quantity() > 0;
public static bool can_be_attached_to(this OrderRecordImmutable order) {
if (order.HasDef && order.Def.HasCustomDef) {
// TODO: implement with real logic (this is impl is a placeholder for development purposes)
switch (order.Def.CustomDef.CustomCase) {
case CustomOrderDefinition.CustomOneofCase.Iceberg:
case CustomOrderDefinition.CustomOneofCase.Dynamic:
return true;
}
}
return false;
}
public static bool can_detach(this OrderRecordImmutable order) =>
order.attached_target_order_key() == 0 && order.has_parent();
public static string display_name(this OrderSecurity order_tuple) =>
display_name(order_tuple.order, order_tuple.algo);
public static string display_name(OrderRecordImmutable order, AlgoData? algo, SecurityImmutable? security = null) {
if (order.order_type() == CustomOrderDefinition.CustomOneofCase.Spreader && security.HasValue)
return security.Value.ToString();
if (algo.HasValue) {
return algo.Value.name;
} else if (order.HasDef && order.Def.HasCustomDef) {
return order.Def.CustomDef.HasDynamic ?
order.Def.CustomDef.Dynamic.DynamicOrderType :
order.Def.CustomDef.CustomCase.display_name();
}
return "Undefined";
}
public static string full_description(
this OrderRecordImmutable order,
SecurityImmutable security,
bts.api.domain.TickDisplaySettingsImmutable? qty_display = null) {
StringBuilder sb = new StringBuilder();
string sec_key_text;
if (order.HasDef) {
sec_key_text = $"Security: {order.Def.SecurityKey}";
if (order.HasState) {
qty_display ??= SecurityDataHelper.Defaults.quantity_display_settingsi;
var qty_str = qty_display.Value.format(order.quantity());
var filled_qty_str = qty_display.Value.format(order.filled_quantity());
sb.AppendLine($"{order.Def.Side}: {TickUtils.format(security.ContractSpec, order.State.Price)}");
sb.AppendLine($"Filled: {filled_qty_str} / {qty_str}");
} else {
sb.AppendLine($"{order.Def.Side}: (price-unknown)");
sb.AppendLine("Filled: (quantity-unknown)");
}
sb.AppendLine($"TIF: {order.Def.TimeInForce}");
sb.AppendLine($"Type: {display_name(order.Def.CustomDef.CustomCase)}");
} else {
sec_key_text = "Security: (id-unknown)";
sb.AppendLine($"(side-unknown): (price-unknown)");
sb.AppendLine("Filled: (quantity-unknown)");
}
sb.AppendLine($"Order: {order.Key}");
if (order.has_parent()) {
sb.AppendLine(sec_key_text);
sb.Append($"Parent Key: {order.ParentKey}");
} else
sb.Append(sec_key_text);
return sb.ToString();
}
public static string short_description(this OrderRecordImmutable order, ContractSpecificationImmutable? spec) =>
$"{(order.HasDef ? order.Def.Side.ToString() : "(unknown-side)")} {(order.HasState ? ($@"{spec.format(order.State.Price)} @ {order.State.Quantity}") : "(unknown-price) @ (unknown-quantity)")}";
public static string short_description(
SecurityImmutable security, Side side, double price, double quantity) =>
new StringBuilderEx(
security.ToString(), " ",
side.ToString(), " ",
price_quantity_description(price, quantity, security.spec_or_default())
).ToString();
public static string short_description(this OrderRecordImmutable order, SecurityImmutable? security) {
var spec = security.spec_or_default();
return new StringBuilderEx(
security.display_name(), " ",
order.side().ToString(), " ",
price_quantity_description(order.price(), order.quantity(), spec), " (",
filled_status_description(order, spec), ")"
).ToString();
}
public static string price_quantity_description(
double price, double quantity, ContractSpecificationImmutable spec) =>
price_quantity_description(
price, quantity, spec.tick_factors().to_api_tick_display_settingsi());
public static string price_quantity_description(
double price,
double quantity,
bts.api.domain.TickDisplaySettingsImmutable? price_display = null,
bts.api.domain.TickDisplaySettingsImmutable? quantity_display = null) {
price_display ??= SecurityDataHelper.Defaults.price_display_settingsi;
quantity_display ??= SecurityDataHelper.Defaults.quantity_display_settingsi;
return $"{quantity_display.Value.format(quantity)} @ {price_display.Value.format(price)}";
}
public static string filled_status_description(
this OrderRecordImmutable order,
ContractSpecificationImmutable spec,
bts.api.domain.TickDisplaySettingsImmutable? qty_display = null
) {
qty_display ??= SecurityDataHelper.Defaults.quantity_display_settingsi;
var sb = new StringBuilderEx(
"filled ", qty_display.Value.format(order.filled_quantity()));
if (order.filled_quantity() > 0)
sb.Add(" @ ", spec.format(order.avg_price()));
sb.Add(", ", camel_to_title_case(order.status().ToString(), TextCapitalizationFormat.NoCaps));
return sb.ToString();
}
public static string update_description(
OrderRecordImmutable order,
double? new_price = null,
double? new_quantity = null,
bts.api.domain.TickDisplaySettingsImmutable? price_display = null,
bts.api.domain.TickDisplaySettingsImmutable? quantity_display = null
) {
var price = new_price ?? order.price();
var quantity = new_quantity ?? order.working_quantity();
if (price == order.price() && quantity == order.working_quantity()) {
// nothing actually changed
return null;
}
price_display ??= SecurityDataHelper.Defaults.price_display_settingsi;
quantity_display ??= SecurityDataHelper.Defaults.quantity_display_settingsi;
var from_desc = price_quantity_description(
order.quantity(), order.price(), price_display, quantity_display);
var to_desc = price_quantity_description(
quantity, price, price_display, quantity_display);
return $"{from_desc} to {to_desc}";
}
public static bool has_parent(this OrderRecordImmutable order) =>
order.ParentKey != 0;
public static bool is_active(this OrderRecordImmutable order) =>
order.HasState && order.State.Status == OrderStatus.Working;
public static bool is_non_terminal(this OrderRecordImmutable order) {
if (!order.HasState)
return false;
switch (order.State.Status) {
case OrderStatus.PendingWorking: return true;
case OrderStatus.Working: return true;
case OrderStatus.Inactive: return true;
case OrderStatus.Disconnected: return true;
default: return false;
}
}
public static bool stop_or_stop_limit_has_been_triggered(this OrderRecordImmutable order) {
if (order.HasState && (order.order_type() == CustomOrderDefinition.CustomOneofCase.Stop || order.order_type() == CustomOrderDefinition.CustomOneofCase.StopLimit)) {
var native_state = order.State.native_order_state();
if (native_state.HasValue) {
if (order.State.HasStop || order.State.HasStopLimit) {
return native_state.Value.NativeType == NativeOrderType.Limit;
}
} else {
return false;
}
}
return false;
}
public static string type_id(this CustomOrderDefinition.CustomOneofCase order_type) =>
order_type.ToString().ToUpper();
public static string display_name(this OrderSecurityAlgo order_sec_algo) =>
display_name(order_sec_algo.order, order_sec_algo.algo, order_sec_algo.security);
public static string display_name(this CustomOrderDefinitionImmutable algo) =>
algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic ? algo.Dynamic.DynamicOrderType : algo.CustomCase.display_name();
public static string display_name(this CustomOrderDefinition.CustomOneofCase order_type) =>
camel_to_title_case(Regex.Replace(order_type.ToString(), "Order", string.Empty, RegexOptions.IgnoreCase));
public static bool is_bts_algo_type(this CustomOrderDefinition.CustomOneofCase algo_type) {
switch (algo_type) {
case CustomOrderDefinition.CustomOneofCase.Iceberg:
case CustomOrderDefinition.CustomOneofCase.Lean:
case CustomOrderDefinition.CustomOneofCase.Join:
case CustomOrderDefinition.CustomOneofCase.Cover:
case CustomOrderDefinition.CustomOneofCase.Top:
case CustomOrderDefinition.CustomOneofCase.Spreader: return true;
}
return false;
}
public static Brush badge_fill(AlgoDefinitionMetadata? algo_meta, OrderMetadata order_meta) =>
new SolidColorBrush(badge_color(algo_meta, order_meta));
public static Color badge_color(AlgoDefinitionMetadata? algo_meta, OrderMetadata order_meta) =>
algo_meta?.badge_color ?? order_meta.Color.rgb_color();
public static string order_collection_summary(
double best_price, double best_price_volume, double total_volume) =>
order_collection_summary(
best_price,
best_price_volume,
total_volume,
SecurityDataHelper.Defaults.ContractSpecificationImmutable);
public static string order_collection_summary(
double best_price,
double best_price_volume,
double total_volume,
ContractSpecificationImmutable spec
) {
if (total_volume == 0)
return string.Empty;
return total_volume == best_price_volume ?
$"{best_price_volume} @ {spec.format(best_price)}" :
$"{best_price_volume} ({total_volume}) @ {spec.format(best_price)}";
}
public static string order_collection_summary(
double best_price,
double avg_price,
double best_price_volume,
double total_volume,
ContractSpecificationImmutable spec) {
if (total_volume == 0)
return string.Empty;
return total_volume == best_price_volume ?
$"{best_price_volume} @ {spec.format(best_price)}" :
$"{best_price_volume} @ {spec.format(best_price)} ({total_volume} @ {spec.format(avg_price)})";
}
public static string past_tense(this Side side) {
switch (side) {
case Side.Buy: return "Bought";
case Side.Sell: return "Sold";
default: return side.ToString();
}
}
public static string abbreviate(this Side side) {
switch (side) {
case Side.Buy: return "B";
case Side.Sell: return "S";
case Side.Undefined:
default: return "E";
}
}
public static string abbreviate_structure(CustomOrderDefinitionImmutable def) {
var builder = new StringBuilder();
impl(builder, def.CloneToMutable());
return builder.ToString();
// local methods
void impl(StringBuilder sb, CustomOrderDefinition mdef) {
sb.Append(abbreviate(mdef.CustomCase));
foreach (var cad in OrderDataHelper.get_child_algo_data(def.CloneToMutable())) {
var child = cad.get_child(mdef);
sb.Append(".[");
impl(sb, child);
sb.Append("]");
}
}
}
static readonly Dictionary<CustomOrderDefinition.CustomOneofCase, string> _abbreviated_exchange_types =
get_word_tree_abbreviations(OrderDataHelper.all_order_types.Select(t => t.ToString().ToUpper()))
.ToDictionary(kv => EnumHelper.parse<CustomOrderDefinition.CustomOneofCase>(kv.Key), kv => kv.Value);
public static string abbreviate(this CustomOrderDefinition.CustomOneofCase type) =>
_abbreviated_exchange_types.TryGetValue(type, out string result) ? result : type.ToString();
public static string order_sub_type_display_name(OrderSubType sub_type, bool use_long_name) {
switch (sub_type) {
case OrderSubType.HedgingLeg:
return use_long_name ? "Hedging (H)" : "H";
case OrderSubType.QuotingLeg:
return use_long_name ? "Quoting (Q)" : "Q";
case OrderSubType.None:
return use_long_name ? "None" : string.Empty;
default:
return string.Empty;
}
}
public static AggressorSide to_aggressor(this Side side) {
switch (side) {
case Side.Buy: return AggressorSide.Buy;
case Side.Sell: return AggressorSide.Sell;
default: return AggressorSide.None;
}
}
}
}
using bts;
using bts.api;
using bts.api.orderman;
using bts.co2;
using bts.spark;
using bts.utils;
using mbts.spark.utilities;
using mbts.ui;
using mbts.ui.utilities;
using mbts.ui.utility;
using mbts.utils;
using Spark.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Windows.Media;
using static mbts.ui.utilities.DisplayNameHelper;
namespace mbts.spark {
static class OrderUtils {
public static readonly Func<OrderRecordImmutable, uint> RecordKeySelector = o => o.Key;
#region OrderStateImmutable extension methods
public static double quantity(this OrderStateImmutable state) =>
Convert.ToDouble(state.Quantity) +
(state.HasFractionalQuantity ? state.FractionalQuantity.Fraction : 0.0);
public static double filled_quantity(this OrderStateImmutable state) =>
Convert.ToDouble(state.FilledQuantity) +
(state.HasFractionalFilledQuantity ? state.FractionalFilledQuantity.Fraction : 0.0);
public static double working_quantity(this OrderStateImmutable state) =>
state.quantity() - state.filled_quantity();
public static double fill_ratio(this OrderStateImmutable state) =>
state.filled_quantity() / state.quantity();
#endregion
public static double filled_quantity(this OrderRecordImmutable order) =>
order.HasState ? order.State.filled_quantity() : 0.0;
public static double fill_ratio(this OrderRecordImmutable order) =>
order.HasState ? order.State.FilledQuantity / (double)order.State.Quantity : double.NaN;
public static double quantity(this OrderRecordImmutable order) =>
order.HasState ? order.State.quantity() : 0;
public static double working_quantity(this OrderRecordImmutable order) =>
order.HasState ? order.State.working_quantity() : 0;
public static double price(this OrderRecordImmutable order) =>
order.HasState ? order.State.Price : double.NaN;
public static double ui_price(this OrderRecordImmutable order) {
if (order.HasState) {
if (order.order_type() == CustomOrderDefinition.CustomOneofCase.Stop && !order.stop_or_stop_limit_has_been_triggered()) {
return order.stop_price();
}
if (order.order_type() == CustomOrderDefinition.CustomOneofCase.StopLimit && !order.stop_or_stop_limit_has_been_triggered()) {
return order.stop_limit_price();
}
return order.State.Price;
}
return double.NaN;
}
public static double stop_price(this OrderRecordImmutable order) =>
order.HasState && order.State.HasStop && order.State.Stop.HasNativeState ? order.State.Stop.NativeState.StopPx : double.NaN;
public static double stop_limit_price(this OrderRecordImmutable order) =>
order.HasState && order.State.HasStopLimit && order.State.StopLimit.HasNativeState ? order.State.StopLimit.NativeState.StopPx : double.NaN;
public static double avg_price(this OrderRecordImmutable order) =>
order.HasState ? order.State.AvgPrice : double.NaN;
public static double last_price(this OrderRecordImmutable order) =>
order.HasState ? order.State.LastPrice : double.NaN;
public static string last_msg(this OrderRecordImmutable order) =>
order.HasState ? order.State.LastMsg : string.Empty;
public static IReadOnlyDictionary<string, DynamicOrderParam> dynamic_custom_state(this OrderRecordImmutable order) =>
order.HasState && order.State.HasDynamic ?
(order.State.Dynamic.CustomState ?? DictionaryHelper.empty<string, DynamicOrderParam>()) :
DictionaryHelper.empty<string, DynamicOrderParam>();
public static DateTime created(this OrderRecordImmutable order) =>
order.HasState && order.State.HasCreated ? order.State.Created.ToDateTime().ToLocalTime() : DateTime.MinValue;
public static DateTime last_change(this OrderRecordImmutable order) =>
order.HasState && order.State.HasLastChange ? order.State.LastChange.ToDateTime().ToLocalTime() : DateTime.MinValue;
public static OrderStatus status(this OrderRecordImmutable order) =>
order.HasState ? order.State.Status : OrderStatus.Error;
public static uint security_key(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.SecurityKey : 0u;
public static Side side(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.Side : Side.Undefined;
public static string user(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.User : string.Empty;
public static uint user_id(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.UserId : 0;
public static UserTuple user_tuple(this OrderRecordImmutable order) =>
new(order.user_id(), order.user());
public static string account(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.Account : string.Empty;
public static uint account_id(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.AccountId : 0;
public static CustomOrderDefinition.CustomOneofCase order_type(this OrderRecordImmutable order) =>
order.HasDef && order.Def.HasCustomDef ? order.Def.CustomDef.CustomCase : CustomOrderDefinition.CustomOneofCase.None;
/// <summary>
/// Unwraps all <see cref="CompositeOrderDefinition"/>s (if any) to return the underlying order type.
/// </summary>
/// <param name="order">The order who's underlying algo will be returned.</param>
/// <returns>The first non-composite algo definition in the specified <see cref="CustomOrderDefinition"/>.</returns>
public static CustomOrderDefinition.CustomOneofCase actual_order_type(this OrderRecordImmutable order) {
if (!order.HasDef || !order.Def.HasCustomDef)
return CustomOrderDefinition.CustomOneofCase.None;
var cdef = order.Def.CustomDef;
while (cdef.CustomCase == CustomOrderDefinition.CustomOneofCase.Composite) {
if (!cdef.Composite.HasDefinition)
return CustomOrderDefinition.CustomOneofCase.None;
cdef = cdef.Composite.Definition;
}
return cdef.CustomCase;
}
public static string dynamic_order_type_display_name(this OrderRecordImmutable order) =>
order.HasDef && order.Def.HasCustomDef && order.Def.CustomDef.HasDynamic ? order.Def.CustomDef.Dynamic.DynamicOrderType : string.Empty;
public static uint algo_template_key(this OrderRecordImmutable order) =>
order.HasDef && order.Def.HasCustomDef && order.Def.CustomDef.CustomCase == CustomOrderDefinition.CustomOneofCase.Composite ?
order.Def.CustomDef.Composite.AlgoTemplateKey : 0u;
/// <summary>
/// If the definitions is a <see cref="CompositeOrderDefinition"/>, returns the underlying algo, otherwise returns <paramref name="custom_def"/> iteself.
/// </summary>
/// <param name="custom_def">The definition to for which the underlying algo will be returned.</param>
/// <returns>The first non-composite algo definition in the specified <see cref="CustomOrderDefinition"/>.</returns>
public static CustomOrderDefinitionImmutable actual(this CustomOrderDefinitionImmutable custom_def) {
while (custom_def.CustomCase == CustomOrderDefinition.CustomOneofCase.Composite)
custom_def = custom_def.Composite.Definition;
return custom_def;
}
public static OrderSubType sub_type(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.SubType : OrderSubType.None;
public static TimeInForce time_in_force(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.TimeInForce : TimeInForce.Undefined;
public static uint attached_target_order_key(this OrderRecordImmutable order) =>
order.HasDef ? order.Def.AttachTargetOrderKey : 0;
public static bool is_exchange_order(this OrderRecordImmutable order) {
if (order.HasDef && order.Def.HasCustomDef) {
switch (order.Def.CustomDef.CustomCase) {
case CustomOrderDefinition.CustomOneofCase.Limit:
case CustomOrderDefinition.CustomOneofCase.Market:
case CustomOrderDefinition.CustomOneofCase.Stop:
case CustomOrderDefinition.CustomOneofCase.StopLimit:
case CustomOrderDefinition.CustomOneofCase.GlbxIceberg: return true;
}
}
return false;
}
public static bool can_attach(this OrderRecordImmutable order) =>
order.working_quantity() > 0;
public static bool can_be_attached_to(this OrderRecordImmutable order) {
if (order.HasDef && order.Def.HasCustomDef) {
// TODO: implement with real logic (this is impl is a placeholder for development purposes)
switch (order.Def.CustomDef.CustomCase) {
case CustomOrderDefinition.CustomOneofCase.Iceberg:
case CustomOrderDefinition.CustomOneofCase.Dynamic:
return true;
}
}
return false;
}
public static bool can_detach(this OrderRecordImmutable order) =>
order.attached_target_order_key() == 0 && order.has_parent();
public static string display_name(this OrderSecurity order_tuple) =>
display_name(order_tuple.order, order_tuple.algo);
public static string display_name(OrderRecordImmutable order, AlgoData? algo, SecurityImmutable? security = null) {
if (order.order_type() == CustomOrderDefinition.CustomOneofCase.Spreader && security.HasValue)
return security.Value.ToString();
if (algo.HasValue) {
return algo.Value.name;
} else if (order.HasDef && order.Def.HasCustomDef) {
return order.Def.CustomDef.HasDynamic ?
order.Def.CustomDef.Dynamic.DynamicOrderType :
order.Def.CustomDef.CustomCase.display_name();
}
return "Undefined";
}
public static string full_description(
this OrderRecordImmutable order,
SecurityImmutable security,
bts.api.domain.TickDisplaySettingsImmutable? qty_display = null) {
StringBuilder sb = new StringBuilder();
string sec_key_text;
if (order.HasDef) {
sec_key_text = $"Security: {order.Def.SecurityKey}";
if (order.HasState) {
qty_display ??= SecurityDataHelper.Defaults.quantity_display_settingsi;
var qty_str = qty_display.Value.format(order.quantity());
var filled_qty_str = qty_display.Value.format(order.filled_quantity());
sb.AppendLine($"{order.Def.Side}: {TickUtils.format(security.ContractSpec, order.State.Price)}");
sb.AppendLine($"Filled: {filled_qty_str} / {qty_str}");
} else {
sb.AppendLine($"{order.Def.Side}: (price-unknown)");
sb.AppendLine("Filled: (quantity-unknown)");
}
sb.AppendLine($"TIF: {order.Def.TimeInForce}");
sb.AppendLine($"Type: {display_name(order.Def.CustomDef.CustomCase)}");
} else {
sec_key_text = "Security: (id-unknown)";
sb.AppendLine($"(side-unknown): (price-unknown)");
sb.AppendLine("Filled: (quantity-unknown)");
}
sb.AppendLine($"Order: {order.Key}");
if (order.has_parent()) {
sb.AppendLine(sec_key_text);
sb.Append($"Parent Key: {order.ParentKey}");
} else
sb.Append(sec_key_text);
return sb.ToString();
}
public static string short_description(this OrderRecordImmutable order, ContractSpecificationImmutable? spec) =>
$"{(order.HasDef ? order.Def.Side.ToString() : "(unknown-side)")} {(order.HasState ? ($@"{spec.format(order.State.Price)} @ {order.State.Quantity}") : "(unknown-price) @ (unknown-quantity)")}";
public static string short_description(
SecurityImmutable security, Side side, double price, double quantity) =>
new StringBuilderEx(
security.ToString(), " ",
side.ToString(), " ",
price_quantity_description(price, quantity, security.spec_or_default())
).ToString();
public static string short_description(this OrderRecordImmutable order, SecurityImmutable? security) {
var spec = security.spec_or_default();
return new StringBuilderEx(
security.display_name(), " ",
order.side().ToString(), " ",
price_quantity_description(order.price(), order.quantity(), spec), " (",
filled_status_description(order, spec), ")"
).ToString();
}
public static string price_quantity_description(
double price, double quantity, ContractSpecificationImmutable spec) =>
price_quantity_description(
price, quantity, spec.tick_factors().to_api_tick_display_settingsi());
public static string price_quantity_description(
double price,
double quantity,
bts.api.domain.TickDisplaySettingsImmutable? price_display = null,
bts.api.domain.TickDisplaySettingsImmutable? quantity_display = null) {
price_display ??= SecurityDataHelper.Defaults.price_display_settingsi;
quantity_display ??= SecurityDataHelper.Defaults.quantity_display_settingsi;
return $"{quantity_display.Value.format(quantity)} @ {price_display.Value.format(price)}";
}
public static string filled_status_description(
this OrderRecordImmutable order,
ContractSpecificationImmutable spec,
bts.api.domain.TickDisplaySettingsImmutable? qty_display = null
) {
qty_display ??= SecurityDataHelper.Defaults.quantity_display_settingsi;
var sb = new StringBuilderEx(
"filled ", qty_display.Value.format(order.filled_quantity()));
if (order.filled_quantity() > 0)
sb.Add(" @ ", spec.format(order.avg_price()));
sb.Add(", ", camel_to_title_case(order.status().ToString(), TextCapitalizationFormat.NoCaps));
return sb.ToString();
}
public static string update_description(
OrderRecordImmutable order,
double? new_price = null,
double? new_quantity = null,
bts.api.domain.TickDisplaySettingsImmutable? price_display = null,
bts.api.domain.TickDisplaySettingsImmutable? quantity_display = null
) {
var price = new_price ?? order.price();
var quantity = new_quantity ?? order.working_quantity();
if (price == order.price() && quantity == order.working_quantity()) {
// nothing actually changed
return null;
}
price_display ??= SecurityDataHelper.Defaults.price_display_settingsi;
quantity_display ??= SecurityDataHelper.Defaults.quantity_display_settingsi;
var from_desc = price_quantity_description(
order.quantity(), order.price(), price_display, quantity_display);
var to_desc = price_quantity_description(
quantity, price, price_display, quantity_display);
return $"{from_desc} to {to_desc}";
}
public static bool has_parent(this OrderRecordImmutable order) =>
order.ParentKey != 0;
public static bool is_active(this OrderRecordImmutable order) =>
order.HasState && order.State.Status == OrderStatus.Working;
public static bool is_non_terminal(this OrderRecordImmutable order) {
if (!order.HasState)
return false;
switch (order.State.Status) {
case OrderStatus.PendingWorking: return true;
case OrderStatus.Working: return true;
case OrderStatus.Inactive: return true;
case OrderStatus.Disconnected: return true;
default: return false;
}
}
public static bool stop_or_stop_limit_has_been_triggered(this OrderRecordImmutable order) {
if (order.HasState && (order.order_type() == CustomOrderDefinition.CustomOneofCase.Stop || order.order_type() == CustomOrderDefinition.CustomOneofCase.StopLimit)) {
var native_state = order.State.native_order_state();
if (native_state.HasValue) {
if (order.State.HasStop || order.State.HasStopLimit) {
return native_state.Value.NativeType == NativeOrderType.Limit;
}
} else {
return false;
}
}
return false;
}
public static string type_id(this CustomOrderDefinition.CustomOneofCase order_type) =>
order_type.ToString().ToUpper();
public static string display_name(this OrderSecurityAlgo order_sec_algo) =>
display_name(order_sec_algo.order, order_sec_algo.algo, order_sec_algo.security);
public static string display_name(this CustomOrderDefinitionImmutable algo) =>
algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic ? algo.Dynamic.DynamicOrderType : algo.CustomCase.display_name();
public static string display_name(this CustomOrderDefinition.CustomOneofCase order_type) =>
camel_to_title_case(Regex.Replace(order_type.ToString(), "Order", string.Empty, RegexOptions.IgnoreCase));
public static bool is_bts_algo_type(this CustomOrderDefinition.CustomOneofCase algo_type) {
switch (algo_type) {
case CustomOrderDefinition.CustomOneofCase.Iceberg:
case CustomOrderDefinition.CustomOneofCase.Lean:
case CustomOrderDefinition.CustomOneofCase.Join:
case CustomOrderDefinition.CustomOneofCase.Cover:
case CustomOrderDefinition.CustomOneofCase.Top:
case CustomOrderDefinition.CustomOneofCase.Spreader: return true;
}
return false;
}
public static Brush badge_fill(AlgoDefinitionMetadata? algo_meta, OrderMetadata order_meta) =>
new SolidColorBrush(badge_color(algo_meta, order_meta));
public static Color badge_color(AlgoDefinitionMetadata? algo_meta, OrderMetadata order_meta) =>
algo_meta?.badge_color ?? order_meta.Color.rgb_color();
public static string order_collection_summary(
double best_price, double best_price_volume, double total_volume) =>
order_collection_summary(
best_price,
best_price_volume,
total_volume,
SecurityDataHelper.Defaults.ContractSpecificationImmutable);
public static string order_collection_summary(
double best_price,
double best_price_volume,
double total_volume,
ContractSpecificationImmutable spec
) {
if (total_volume == 0)
return string.Empty;
return total_volume == best_price_volume ?
$"{best_price_volume} @ {spec.format(best_price)}" :
$"{best_price_volume} ({total_volume}) @ {spec.format(best_price)}";
}
public static string order_collection_summary(
double best_price,
double avg_price,
double best_price_volume,
double total_volume,
ContractSpecificationImmutable spec) {
if (total_volume == 0)
return string.Empty;
return total_volume == best_price_volume ?
$"{best_price_volume} @ {spec.format(best_price)}" :
$"{best_price_volume} @ {spec.format(best_price)} ({total_volume} @ {spec.format(avg_price)})";
}
public static string past_tense(this Side side) {
switch (side) {
case Side.Buy: return "Bought";
case Side.Sell: return "Sold";
default: return side.ToString();
}
}
public static string abbreviate(this Side side) {
switch (side) {
case Side.Buy: return "B";
case Side.Sell: return "S";
case Side.Undefined:
default: return "E";
}
}
public static string abbreviate_structure(CustomOrderDefinitionImmutable def) {
var builder = new StringBuilder();
impl(builder, def.CloneToMutable());
return builder.ToString();
// local methods
void impl(StringBuilder sb, CustomOrderDefinition mdef) {
sb.Append(abbreviate(mdef.CustomCase));
foreach (var cad in OrderDataHelper.get_child_algo_data(def.CloneToMutable())) {
var child = cad.get_child(mdef);
sb.Append(".[");
impl(sb, child);
sb.Append("]");
}
}
}
static readonly Dictionary<CustomOrderDefinition.CustomOneofCase, string> _abbreviated_exchange_types =
get_word_tree_abbreviations(OrderDataHelper.all_order_types.Select(t => t.ToString().ToUpper()))
.ToDictionary(kv => EnumHelper.parse<CustomOrderDefinition.CustomOneofCase>(kv.Key), kv => kv.Value);
public static string abbreviate(this CustomOrderDefinition.CustomOneofCase type) =>
_abbreviated_exchange_types.TryGetValue(type, out string result) ? result : type.ToString();
public static string order_sub_type_display_name(OrderSubType sub_type, bool use_long_name) {
switch (sub_type) {
case OrderSubType.HedgingLeg:
return use_long_name ? "Hedging (H)" : "H";
case OrderSubType.QuotingLeg:
return use_long_name ? "Quoting (Q)" : "Q";
case OrderSubType.None:
return use_long_name ? "None" : string.Empty;
default:
return string.Empty;
}
}
public static AggressorSide to_aggressor(this Side side) {
switch (side) {
case Side.Buy: return AggressorSide.Buy;
case Side.Sell: return AggressorSide.Sell;
default: return AggressorSide.None;
}
}
}
}
File diff suppressed because it is too large. Load diff
@@ -25,6 +25,7 @@
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
Margin="12,12,10,12"
MaxHeight="400"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
Grid.IsSharedSizeScope="True"
ItemsSource="{Binding columns}"
@@ -76,7 +76,7 @@
<!--SPACING-->
<ColumnDefinition Width="8" />
<!--TIME PICKER-->
<ColumnDefinition Width="80" />
<ColumnDefinition Width="Auto" />
<!--PAD RIGHT-->
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
@@ -91,7 +91,6 @@
x:Name="time_picker"
Grid.Column="3"
Background="White"
Width="80"
TimeTuple="{Binding new_actual_time}"
KeyDown="time_picker_KeyDown">
<Validation.ErrorTemplate>
@@ -134,7 +134,14 @@
// don't initialize more than once
if (_time_editor_popup != null) return;
var adorner_layer = AdornerLayer.GetAdornerLayer(adorned_grid);
_time_editor_popup = new PopupAdorner(adorned_grid, vol_time_popup_button, _get_time_editor_control(), HorizontalPosition.Left, VerticalPosition.Top, 0.5 * ((ToggleButton)sender).ActualWidth, 0d) { IsHitTestVisible = true };
_time_editor_popup = new PopupAdorner(
adorned_grid,
vol_time_popup_button,
_get_time_editor_control(),
HorizontalPosition.Left,
VerticalPosition.Top,
44,
0d) { IsHitTestVisible = true };
_time_editor_popup.SetBinding(PopupAdorner.DataContextProperty, "globals");
_time_editor_popup.SetBinding(PopupAdorner.IsOpenProperty, new Binding("vol_time_popup_open") { Mode = BindingMode.TwoWay });
_time_editor_popup.Opened += _time_editor_popup_Opened;
@@ -2,11 +2,12 @@
using System.Collections.Generic;
using System.Composition;
using System.Threading.Tasks;
namespace mbts.ui.shell {
public interface IBookmarks {
void save_bookmark(IShellWindow window);
Task save_bookmark(IShellWindow window);
void save_bookmark(WindowState window);
void remove(WindowState window);
void clear();
@@ -22,15 +23,16 @@ namespace mbts.ui.shell {
public BookmarksModel(ISettingsCollector settings) {
_settings_collector = settings;
}
public void save_bookmark(IShellWindow window) {
var state = _settings_collector.get_window_state(window);
public async Task save_bookmark(IShellWindow window) {
var state = await _settings_collector.get_window_state(window);
if (state != null) {
_bookmarks[state.title] = state;
}
}
public void save_bookmark(WindowState window) {
public void save_bookmark(WindowState window) =>
_bookmarks[window.title] = window;
}
public void clear() {
_bookmarks.Clear();
@@ -38,6 +38,26 @@ namespace bts.shell.market_view {
public static GridSettings DefaultGridSettings =>
GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criteria, enable_background_color: true);
public static GridSettings dark_theme_grid_settings =>
DefaultGridSettings.with(
selection_highlight: BTSDarkColors.selection_highlight.argb_color(),
background: BTSDarkColors.background.argb_color(),
font_color: BTSDarkColors.foreground.argb_color(),
columns: DefaultColumns.all.Select(c => {
Color? fg = c.strong_id<Columns>() switch {
Columns.Ask or Columns.AskDirectQty or Columns.AskMergedQty or Columns.AskImpliedQty or Columns.WorkingAsks => BTSDarkColors.ask_foreground,
Columns.Bid or Columns.BidDirectQty or Columns.BidMergedQty or Columns.BidImpliedQty or Columns.WorkingBids => BTSDarkColors.bid_foreground,
_ => null,
};
Color? bg = c.strong_id<Columns>() switch {
Columns.Ask or Columns.Bid => BTSDarkColors.dark_background,
_ => null
};
return c.with(
background_color_override: new(bg),
font_color_override: new(fg));
}));
public static fills.FillsSettings DefaultAuxiliaryFills {
get {
var grid = fills.FillsSettings.MiniGridSettings;
@@ -41,6 +41,12 @@ namespace bts.shell.time_and_sales {
public static GridSettings DefaultGridSettings =>
GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criteria);
public static GridSettings dark_theme_grid_settings =>
DefaultGridSettings.with(
selection_highlight: BTSDarkColors.selection_highlight.argb_color(),
background: BTSDarkColors.background.argb_color(),
font_color: BTSDarkColors.foreground.argb_color());
[ProtoMember(1)]
public SecurityCriteria? security_criteria { get; }
@@ -8,12 +8,17 @@ using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using mbts.ui.spark;
using mbts.ui.utility;
using Spark.Settings;
using Spark.View;
using System;
using System.Collections.Generic;
using System.Composition;
using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
namespace bts.ui {
@@ -30,7 +35,9 @@ namespace bts.ui {
ws_model = ws;
var actions_list = new List<SparkCommand>(2) {
SparkCommands.ShowGlobalAlerts,
SparkCommands.ShowWorkspaces
SparkCommands.ShowWorkspaces,
new SparkCommandCommand(
"Choose Theme", Icons.ViewSettings, new DialogCommand(mk_theme_picker_dialog, -60, -40))
};
if (Configuration.get<bts.config.toggles.ToastNotifications>().enabled) {
actions_list.Add(SparkCommands.ShowSettings);
@@ -46,6 +53,21 @@ namespace bts.ui {
}));
_bus = bus;
Window mk_theme_picker_dialog() {
var model = new ThemePickerModel(settings_hub);
var vm = new ThemePickerVM(model);
var view = new ThemePickerView() { };
var win = new Window() {
DataContext = vm,
Content = view,
Title = "Choose Theme",
SizeToContent = SizeToContent.WidthAndHeight,
Style = Application.Current.FindResource("BTSDialog") as Style
};
vm.RequestClose += (s, e) => WindowUtilities.force_close(win);
return win;
}
}
public ImageSource icon => Icons.Admin;
@@ -0,0 +1,38 @@
#nullable enable
namespace Spark.Settings {
using bts;
using mbts.persist;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
class ThemePickerModel {
readonly UserSettingsHub _settings_hub;
public ThemePickerModel(UserSettingsHub settings_hub) =>
_settings_hub = settings_hub;
public IObservable<UserSettingsTheme?> selected_theme =>
_settings_hub.subscribe_default_settings()
.Select(us => _settings_hub.themes.FirstOrDefault(t => t.name == us.last_picked_theme))
.DistinctUntilChanged();
public IEnumerable<UserSettingsTheme> themes =>
_settings_hub.themes;
public async Task<bool> set_theme(UserSettingsTheme theme) {
try {
Log.Gen.info($"Applying settings theme '{theme.name}'");
await _settings_hub.save_default(theme.value, true);
return true;
} catch (Exception ex) {
Log.Gen.error($"Error applying theme '{theme.name}': {ex}");
}
return false;
}
}
}
@@ -0,0 +1,52 @@
#nullable enable
namespace Spark.Settings {
using bts;
using bts.collections;
using mbts;
using mbts.ui;
using mbts.ui.shell;
using System;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
class ThemePickerVM : DisposableViewModelBase, IRequestViewClose {
readonly ThemePickerModel _model;
readonly Property<UserSettingsTheme?> _selected_theme;
public ThemePickerVM(ThemePickerModel model) {
_model = model;
themes.AddRange(model.themes);
_selected_theme = backing(nameof(selected_theme), model.selected_theme.OnDispatcher());
commit_cmd = observant_cmd(
commit, t => t is not null, _selected_theme.observe_current(true).OnDispatcher());
cancel_cmd = new Command(close);
async Task commit(UserSettingsTheme? theme) {
if (theme is null) {
Log.Ui.warn($"Unable to apply theme: {nameof(theme)} arg is null");
return;
}
await _model.set_theme(theme);
close();
}
void close() =>
RequestClose?.Invoke(this, EventArgs.Empty);
}
public event EventHandler? RequestClose;
public ObservableCollection<UserSettingsTheme> themes { get; } = new();
public UserSettingsTheme? selected_theme {
get => _selected_theme.Value;
set => _selected_theme.set(value);
}
public ICommand commit_cmd { get; }
public ICommand cancel_cmd { get; }
}
}
@@ -0,0 +1,45 @@
<UserControl
x:Class="Spark.View.ThemePickerView"
x:ClassModifier="internal"
x:Name="me"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:analytics="clr-namespace:mbts.analytics"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:view="clr-namespace:Spark.View">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Resources/CursorsEx.xaml"/>
<ResourceDictionary Source="/view/Resources/DataTemplates.xaml"/>
<ResourceDictionary Source="/view/Controls/ExpandedContentSelector.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ComboBox
Margin="12"
ItemsSource="{Binding themes}"
SelectedItem="{Binding selected_theme}"
DisplayMemberPath="name" />
<c:DialogSkirtOKCancel
Grid.Row="1"
Grid.ColumnSpan="2"
VerticalAlignment="Bottom"
CloseOnOK="False"
CloseOnCancel="False"
OKCommand="{Binding commit_cmd}"
OKCommandParameter="{Binding selected_theme}"
CancelCommand="{Binding cancel_cmd}" />
</Grid>
</UserControl>
@@ -0,0 +1,9 @@
#nullable enable
namespace Spark.View {
using System.Windows.Controls;
partial class ThemePickerView : UserControl {
public ThemePickerView() =>
InitializeComponent();
}
}
@@ -0,0 +1,24 @@
namespace bts.shell {
using mbts.spark.filters;
using mbts.ui;
using ProtoBuf;
[ProtoContract]
class SyntheticManagementFilterSettings {
[ProtoMember(1)]
public string description;
[ProtoMember(2)]
public TimeFilterTuple create_time;
[ProtoMember(3)]
public string name;
[ProtoMember(4)]
public IntMinMaxState n_legs;
[ProtoMember(5)]
public UIntMinMaxState n_working_orders;
[ProtoMember(6)]
public string security_key;
[ProtoMember(7)]
public uint[] user;
}
}
@@ -0,0 +1,111 @@
namespace bts.shell {
using mbts.ui;
using mbts.ui.settings;
using ProtoBuf;
using System.Collections.Generic;
using Columns = mbts.ui.spark.SyntheticManagementView.Columns;
using ColHeaders = mbts.ui.controls.DIYGrid.DefaultColumnHeaders;
using ColWidths = mbts.ui.controls.DIYGrid.DefaultColumnWidths;
[ProtoContract]
class SyntheticManagementSettings {
public const int Version = 1;
[ProtoMember(1)]
public readonly List<SortingCriteria> sorting_criteria;
[ProtoMember(2)]
public readonly SyntheticManagementFilterSettings filters;
[ProtoMember(3)]
public readonly GridSettings grid;
[ProtoMember(4)]
public readonly SparkMenuDisplaySettings menu_display;
public SyntheticManagementSettings()
: this(new List<SortingCriteria>(), null, null) { }
public SyntheticManagementSettings(
List<SortingCriteria> sorting_criteria,
SyntheticManagementFilterSettings filters,
GridSettings grid = default)
: this(sorting_criteria, filters, grid, SparkMenuDisplaySettings.Default) { }
public SyntheticManagementSettings(
List<SortingCriteria> sorting_criteria,
SyntheticManagementFilterSettings filters,
GridSettings grid,
SparkMenuDisplaySettings menu_display) {
this.sorting_criteria = sorting_criteria;
this.filters = filters;
this.grid = grid;
this.menu_display = menu_display;
}
public SyntheticManagementSettings with(
List<SortingCriteria> sorting_criteria = null,
SyntheticManagementFilterSettings filters = null,
GridSettings grid = null,
SparkMenuDisplaySettings? menu_display = null) =>
new(
sorting_criteria ?? this.sorting_criteria,
filters ?? this.filters,
grid ?? this.grid,
menu_display ?? this.menu_display);
public static GridSettings default_grid_settings =>
GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criteria);
public static GridSettings dark_theme_grid_settings =>
default_grid_settings.with(
selection_highlight: BTSDarkColors.selection_highlight.argb_color(),
background: BTSDarkColors.background.argb_color(),
font_color: BTSDarkColors.foreground.argb_color());
public static string col_id(Columns col) =>
col.ToString();
static class DefaultColumns {
public static GridColumnSettings name =>
new(col_id(Columns.Name), ColHeaders.Name, width: ColWidths.Security);
public static GridColumnSettings description =>
new(col_id(Columns.Description), ColHeaders.Description, width: ColWidths.Description);
public static GridColumnSettings n_legs =>
new(col_id(Columns.n_legs), ColHeaders.N_Legs, width: ColWidths.N_Legs);
public static GridColumnSettings n_working_orders => new(
col_id(Columns.n_working_orders),
ColHeaders.N_Working_Orders,
width: ColWidths.N_Working_Orders);
public static GridColumnSettings security_key => new(
col_id(Columns.SecurityKey),
ColHeaders.SecurityKey,
is_visible: false,
width: ColWidths.SecurityKey);
public static GridColumnSettings user =>
new(col_id(Columns.User), ColHeaders.User, is_visible: false, width: ColWidths.User);
public static IEnumerable<SortingCriteria> default_sorting_criteria {
get {
yield return new SortingCriteria(name.id, true);
}
}
public static IEnumerable<GridColumnSettings> all {
get {
yield return name;
yield return description;
yield return n_legs;
yield return n_working_orders;
yield return security_key;
yield return user;
}
}
}
}
}
@@ -22,13 +22,13 @@ using System.Windows;
using static bts.shell.SyntheticManagementWindowFactory;
namespace mbts.spark {
class SyntheticManagementWindow : SparkWindow<StartupData>, ISharedWindow {
class SyntheticManagementWindow : SparkWindow<SyntheticManagementSettings>, ISharedWindow {
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;
SyntheticManagementSettings _state;
OrdersModel _orders_model;
SyntheticManagementView _view;
SparkMenuDisplayVM _menu_display_vm;
@@ -36,7 +36,7 @@ namespace mbts.spark {
List<SparkCommand> _commands;
readonly IWindowSettingsManager<GridSettings> _settings_manager;
public SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state, IWindowSettingsManager<GridSettings> settings_manager)
public SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, SyntheticManagementSettings state, IWindowSettingsManager<GridSettings> settings_manager)
: base("Synthetic Management", bus, factory, SparkCommands.ShowSyntheticManagement.icon) {
_orders_model = orders_model;
_state = state;
@@ -56,9 +56,9 @@ namespace mbts.spark {
public override string window_type => _window_type;
public override UIElement Content => _view;
public override IEnumerable<SparkCommand> Commands => _commands;
public override StartupData SaveState => _state = _vm.value.create().with(menu_display: _menu_display_vm.safe_create());
public override SyntheticManagementSettings SaveState => _state = _vm.value.create().with(menu_display: _menu_display_vm.safe_create());
public override double MinWidth =>
StartupData.DefaultGridSettings.columns.Sum(gcs => gcs.width)
SyntheticManagementSettings.default_grid_settings.columns.Sum(gcs => gcs.width)
+ SPARK_MENU_COLLAPSED_WIDTH
+ LAUNCHER_BUTTON_BORDER_WIDTH
+ EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH;
@@ -133,7 +133,7 @@ namespace mbts.spark {
(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());
_vm.value = new SyntheticManagementDialogVM(_bus, _orders_model, _settings_manager, _state.sorting_criteria, _state.filters ?? new SyntheticManagementFilterSettings());
if (_view == null) _view = new SyntheticManagementView { DataContext = _vm.value };
else _view.DataContext = _vm.value;
@@ -3,23 +3,18 @@ using bts.settings;
using mbts.persist;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.settings;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Composition;
using System.Reactive.Linq;
using System.Threading.Tasks;
using static mbts.ui.controls.DIYGrid;
using static mbts.ui.spark.SyntheticManagementView;
namespace bts.shell {
class SyntheticManagementWindowFactory : ShellWindowFactory<SyntheticManagementWindowFactory.StartupData> {
class SyntheticManagementWindowFactory : ShellWindowFactory<SyntheticManagementSettings> {
readonly IBus _bus;
readonly OrdersModel _orders_model;
readonly UserSettingsHub _settings_hub;
@@ -33,100 +28,20 @@ namespace bts.shell {
public static string col_id(Columns col) => col.ToString();
public override Task<IShellWindow> load(StartupData state) =>
public override Task<IShellWindow> load(SyntheticManagementSettings state) =>
Task.FromResult<IShellWindow>(_create(state));
public async void show(SparkTags.Nothing nothing) =>
_bus.post(SparkTags.ShowWindow, await load(new StartupData()));
_bus.post(SparkTags.ShowWindow, await load(new SyntheticManagementSettings()));
IShellWindow _create(StartupData state) =>
IShellWindow _create(SyntheticManagementSettings state) =>
new SyntheticManagementWindow(this, _bus, _orders_model, state, _settings_hub.create_synth_settings_manager(state.grid));
[ProtoContract]
public class StartupData {
public const int Version = 1;
[ProtoMember(1)]
public readonly List<SortingCriteria> sorting_criteria;
[ProtoMember(2)]
public readonly FilterStates filters;
[ProtoMember(3)]
public readonly GridSettings grid;
[ProtoMember(4)]
public readonly SparkMenuDisplaySettings menu_display;
public StartupData() : this(new List<SortingCriteria>(), null, null) { }
public StartupData(List<SortingCriteria> sorting_criteria, FilterStates filters, GridSettings grid = default)
: this(sorting_criteria, filters, grid, SparkMenuDisplaySettings.Default) { }
public StartupData(List<SortingCriteria> sorting_criteria, FilterStates filters, GridSettings grid, SparkMenuDisplaySettings menu_display) {
this.sorting_criteria = sorting_criteria;
this.filters = filters;
this.grid = grid;
this.menu_display = menu_display;
}
public StartupData with(List<SortingCriteria> sorting_criteria = null, FilterStates filters = null, GridSettings grid = null, SparkMenuDisplaySettings? menu_display = null) =>
new StartupData(
sorting_criteria ?? this.sorting_criteria,
filters ?? this.filters,
grid ?? this.grid,
menu_display ?? this.menu_display);
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criteria);
static class DefaultColumns {
public static GridColumnSettings name => new GridColumnSettings(col_id(Columns.Name), DefaultColumnHeaders.Name, width: DefaultColumnWidths.Security);
public static GridColumnSettings description => new GridColumnSettings(col_id(Columns.Description), DefaultColumnHeaders.Description, width: DefaultColumnWidths.Description);
public static GridColumnSettings n_legs => new GridColumnSettings(col_id(Columns.n_legs), DefaultColumnHeaders.N_Legs, width: DefaultColumnWidths.N_Legs);
public static GridColumnSettings n_working_orders => new GridColumnSettings(col_id(Columns.n_working_orders), DefaultColumnHeaders.N_Working_Orders, width: DefaultColumnWidths.N_Working_Orders);
public static GridColumnSettings security_key => new GridColumnSettings(col_id(Columns.SecurityKey), DefaultColumnHeaders.SecurityKey, is_visible: false, width: DefaultColumnWidths.SecurityKey);
public static GridColumnSettings user => new GridColumnSettings(col_id(Columns.User), DefaultColumnHeaders.User, is_visible: false, width: DefaultColumnWidths.User);
public static IEnumerable<SortingCriteria> default_sorting_criteria {
get {
yield return new SortingCriteria(name.id, true);
}
}
public static IEnumerable<GridColumnSettings> all {
get {
yield return name;
yield return description;
yield return n_legs;
yield return n_working_orders;
yield return security_key;
yield return user;
}
}
}
}
[ProtoContract]
public class FilterStates {
[ProtoMember(1)]
public string description;
[ProtoMember(2)]
public TimeFilterTuple create_time;
[ProtoMember(3)]
public string name;
[ProtoMember(4)]
public IntMinMaxState n_legs;
[ProtoMember(5)]
public UIntMinMaxState n_working_orders;
[ProtoMember(6)]
public string security_key;
[ProtoMember(7)]
public uint[] user;
}
// migration rules
[Export(typeof(ISyntheticManagementMigrationRule))]
class AddMissingColumnsMigrationRule : ISyntheticManagementMigrationRule {
public bool apply(GridSettings settings, out GridSettings new_settings) =>
settings.add_missing_columns(StartupData.DefaultGridSettings.columns, out new_settings);
settings.add_missing_columns(SyntheticManagementSettings.default_grid_settings.columns, out new_settings);
}
}
}
@@ -1,6 +1,7 @@
using bts;
using bts.api;
using bts.collections;
using bts.shell;
using bts.spark;
using bts.utils;
@@ -18,7 +19,6 @@ using System.Reactive.Linq;
using System.Windows.Data;
using System.Windows.Input;
using static bts.shell.SyntheticManagementWindowFactory;
using static mbts.ui.controls.DIYGrid;
using static mbts.ui.spark.SyntheticManagementView;
@@ -29,7 +29,7 @@ namespace mbts.spark {
readonly IWindowSettingsManager<GridSettings> _settings_manager;
public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model, IWindowSettingsManager<GridSettings> settings_manager) : this(bus, orders_model, settings_manager, null, null) { }
public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model, IWindowSettingsManager<GridSettings> settings_manager, IReadOnlyCollection<SortingCriteria> sorting, FilterStates filters)
public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model, IWindowSettingsManager<GridSettings> settings_manager, IReadOnlyCollection<SortingCriteria> sorting, SyntheticManagementFilterSettings filters)
: base(orders_model.spreader.synthetic_tuples.Select(t => t.state.Values), sorting) {
_bus = bus;
_orders_model = orders_model;
@@ -54,7 +54,7 @@ namespace mbts.spark {
Columns.n_working_orders,
n_working_orders_filter,
DataGridFilterKnownTemplates.UIntMinMax) {
id = col_id(Columns.n_working_orders),
id = SyntheticManagementSettings.col_id(Columns.n_working_orders),
CommandBinding = new Binding(nameof(Row.show_order_book)),
TextBinding = new Binding(nameof(Row.n_working_orders)),
Header = DefaultColumnHeaders.N_Working_Orders,
@@ -65,7 +65,7 @@ namespace mbts.spark {
//
void add_col(Columns col_key, string header, Func<Row, string> get_text, string prop, IDataGridColumnFilter filter, string filter_template, double width) =>
columns.Add(new RenderTextColumn<Row, Columns>(col_key, get_text, filter, filter_template) {
id = col_id(col_key),
id = SyntheticManagementSettings.col_id(col_key),
Header = header,
sort_member_path = prop,
Width = width
@@ -96,8 +96,8 @@ namespace mbts.spark {
public GridSettings get_grid_save_state() =>
_settings_manager.instance_settings == null ? null : grid_settings_manager.create();
public StartupData create() =>
new StartupData(rows_view.get_sorting_criteria(columns), _get_filter_states(), get_grid_save_state());
public SyntheticManagementSettings create() =>
new SyntheticManagementSettings(rows_view.get_sorting_criteria(columns), _get_filter_states(), get_grid_save_state());
protected override Row row_factory(SyntheticTuple row_model) =>
new Row(_bus, row_model, _orders_model);
@@ -105,8 +105,8 @@ namespace mbts.spark {
protected override void on_row_removed(Row row) =>
row.Dispose();
FilterStates _get_filter_states() =>
new FilterStates {
SyntheticManagementFilterSettings _get_filter_states() =>
new SyntheticManagementFilterSettings {
description = description_filter.get_state(),
create_time = create_time_filter.get_state(),
name = name_filter.get_state(),
@@ -65,7 +65,7 @@ namespace bts.shell {
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
state.grid,
get: us => us.fills_grid,
set: (us, gs) => us.with(provided_fills_gridsettings: gs));
set: (us, gs) => us.with(fills: gs));
return new FillsWindow(this, _bus, _orders_model, state, settings_manager);
}
@@ -39,6 +39,13 @@ namespace bts.shell.fills {
}
public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criteria);
public static GridSettings dark_theme_grid_settings =>
DefaultGridSettings.with(
selection_highlight: BTSDarkColors.selection_highlight.argb_color(),
background: BTSDarkColors.background.argb_color(),
font_color: BTSDarkColors.foreground.argb_color());
public static GridSettings MiniGridSettings => GridSettings.Default.with(DefaultColumns.Mini.all, DefaultColumns.Mini.sorting_criteria);
public IReadOnlyList<SortingCriteria> sorting_criteria => _sorting_criteria ?? (_sorting_criteria = DefaultColumns.default_sorting_criteria.ToList());
public static FillsSettings Default() =>
@@ -646,6 +646,24 @@ namespace mbts.spark {
public static LadderSettings Default => mimic_tt ?
tt_default : historic_default;
public static LadderSettings dark_theme {
get {
var rv = Default;
rv.orders_column.background = BTSDarkColors.background.argb_color();
rv.orders_column.foreground = BTSDarkColors.foreground.argb_color();
rv.bid_column.background = BTSDarkColors.dark_background.argb_color();
rv.bid_column.foreground = BTSDarkColors.bid_foreground.argb_color();
rv.price_column.background = BTSDarkColors.background.argb_color();
rv.price_column.foreground = BTSDarkColors.foreground.argb_color();
rv.ask_column.background = BTSDarkColors.dark_background.argb_color();
rv.ask_column.foreground = BTSDarkColors.ask_foreground.argb_color();
return rv;
}
}
public enum BadgeStyle { Normal, Alternate }
// TODO: rename to PriceDisplayFormat (today is merge monday, since this isn't a functional change, it is not worth the potential merge headaches right now)
@@ -0,0 +1,48 @@
#nullable enable
namespace mbts.spark {
using bts.utils;
using ProtoBuf;
using System;
[ProtoContract]
class LadderSettingsProfile : ISettingsProfile<LadderSettings>, IEquatable<LadderSettingsProfile> {
public LadderSettingsProfile() { }
public LadderSettingsProfile(
uint id, string? name, LadderSettings? value, SecurityCriteria? security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
[ProtoMember(1)]
public uint id { get; }
[ProtoMember(2)]
public string? name { get; }
[ProtoMember(3)]
public LadderSettings? value { get; }
[ProtoMember(4)]
public SecurityCriteria? security_criteria { get; }
public LadderSettingsProfile with(uint? id = null, LadderSettings? value = null) =>
new(id ?? this.id, name, value ?? this.value, security_criteria);
public bool Equals(LadderSettingsProfile? other) =>
other != null &&
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object? obj) =>
Equals(obj as LadderSettingsProfile);
public override int GetHashCode() => id.GetHashCode();
}
}
@@ -36,10 +36,13 @@ namespace Spark.Trading {
rv.sorting_criteria.AddRange(
new(nameof(ColKey.Security), true),
new(nameof(ColKey.Account), true));
rv.grid = GridSettings.Default.with(columns: DefaultColumns.all);
rv.grid = default_grid_settings;
return rv;
}
public static GridSettings default_grid_settings =>
GridSettings.Default.with(columns: DefaultColumns.all);
public IList<SortingCriteria> sorting_criteria =>
_sorting_criteria ??= new(1);
+1 -1
View File
@@ -90,7 +90,7 @@ namespace mbts.ui {
break;
case ToastNotificationSettings.Mode.Flash:
_toast_notifications_enabled = true;
_toast_controller.flash_duration = s.notification.toast_notification.duration ?? _fallback_flash_duration;
_toast_controller.flash_duration = s.notification.toast_notification.duration;
break;
case ToastNotificationSettings.Mode.ManualClose:
_toast_notifications_enabled = true;
@@ -8,13 +8,11 @@ using System.Threading.Tasks;
namespace mbts.ui.tests {
class TestSettingsCollector : ISettingsCollector {
public IEnumerable<shell.WindowState> get_window_state() {
yield return new shell.WindowState();
}
public Task<List<shell.WindowState>> get_window_state() =>
Task.FromResult(new List<shell.WindowState> { new() });
public shell.WindowState get_window_state(IShellWindow window) {
return new shell.WindowState();
}
public Task<shell.WindowState> get_window_state(IShellWindow window) =>
Task.FromResult(new shell.WindowState());
public IEnumerable<shell.ModuleState> get_module_state() {
yield return new shell.ModuleState();
+35 -7
View File
@@ -4,6 +4,7 @@ using mbts.ui.utility;
using mbts.utils;
using Spark.Core.Reactive;
using Spark.ViewModel.Commands;
using System;
using System.Collections.Generic;
@@ -633,8 +634,9 @@ namespace mbts.ui.shell {
public abstract class DisposableViewModelBase : ViewModelBaseWithValidation, IPropertyChangedDemuxer, IDisposable, IWillLetYouKnowWhenIDispose {
protected IDisposable _dispose = null;
List<IDisposable> _observable_properties_backing;
List<IDisposable> _observable_properties => _observable_properties_backing ?? (_observable_properties_backing = new List<IDisposable>());
readonly List<IDisposable> _internal_resources = new(0);
PropertyChangedDemuxer _property_changed_demuxer_backing;
IPropertyChangedDemuxer _property_changed_demuxer =>
@@ -650,7 +652,7 @@ namespace mbts.ui.shell {
protected Property<T> backing<T>(string name, IObservable<T> source, T default_value = default(T)) {
var prop = new ObservablePropertySink<T>(this, name, default_value);
_observable_properties.Add(source.Subscribe(prop));
_internal_resources.Add(source.Subscribe(prop));
return prop;
}
@@ -660,7 +662,7 @@ namespace mbts.ui.shell {
/// </summary>
protected Property<T> backing<T, TData>(string name, IObservable<TData> source, Func<TData, T> select, T default_value = default) {
var prop = new ObservablePropertySink<T>(this, name, default_value);
_observable_properties.Add(source.OnDispatcher().Subscribe(d => prop.set(select(d))));
_internal_resources.Add(source.OnDispatcher().Subscribe(d => prop.set(select(d))));
return prop;
}
@@ -724,9 +726,35 @@ namespace mbts.ui.shell {
return prop;
}
/// <summary>
/// Creates an <see cref="ICommand"/> that automatically invokes the CanExecuteChanged event
/// in response to notifications from <paramref name="source"/>
/// </summary>
protected ICommand observant_cmd<T>(
Action<T> execute, Func<T, bool> can_execute, IObservable<T> source) {
var cmd = ObservantCommand.create(execute, can_execute, out var obs);
_internal_resources.Add(source.Subscribe(obs));
return cmd;
}
/// <summary>
/// Creates an <see cref="ICommand"/> that automatically invokes the CanExecuteChanged event
/// in response to notifications from <paramref name="source"/>
/// </summary>
protected ICommand observant_cmd<T>(
Func<T, Task> execute,
Func<T, bool> can_execute,
IObservable<T> source,
bool allow_concurrent_executions = false) {
var cmd = ObservantAsyncCommand.create(
execute, can_execute, out var obs, allow_concurrent_executions);
_internal_resources.Add(source.Subscribe(obs));
return cmd;
}
protected void attach<T, V>(SelectorProperty<T, V> selector_property, IObservable<IEnumerable<T>> items, IObservable<InlineUserProperty<T>> desired_item) {
_observable_properties.Add(items.OnDispatcher().Subscribe(selector_property));
_observable_properties.Add(desired_item.OnDispatcher().Subscribe(selector_property));
_internal_resources.Add(items.OnDispatcher().Subscribe(selector_property));
_internal_resources.Add(desired_item.OnDispatcher().Subscribe(selector_property));
}
protected void set_dispose(IDisposable disposable) {
@@ -761,7 +789,7 @@ namespace mbts.ui.shell {
protected virtual void Dispose(bool disposing) { }
public void Dispose() {
Disposable.dispose(ref _dispose);
Disposable.dispose(_observable_properties_backing);
Disposable.dispose(_internal_resources);
Disposable.dispose(ref _property_changed_demuxer_backing);
Dispose(true);
OnDisposed?.Invoke(true);
@@ -0,0 +1,14 @@
#nullable enable
namespace mbts.ui {
using System.Windows.Media;
static class BTSDarkColors {
public static Color light_background => Color.FromArgb(194, 40, 40, 40);
public static Color background => Color.FromArgb(194, 20, 20, 20);
public static Color dark_background => Color.FromArgb(210, 0, 0, 0);
public static Color foreground => Color.FromRgb(236, 246, 255);
public static Color bid_foreground => Color.FromRgb(118, 183, 250);
public static Color ask_foreground => Color.FromRgb(255, 102, 102);
public static Color selection_highlight => Color.FromArgb(96, 0, 0, 0);
}
}
@@ -1,76 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Composition;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Windows;
using System.Windows.Input;
namespace mbts.spark {
static class ShellKeybindings {
// public static class Namespaces {
// public const string ladder
// }
public const string ladder_scroll_down = "ladder.scroll_down";
public const string ladder_scroll_up = "ladder.scroll_up";
public const string ladder_half_page_down = "ladder.half_page_down";
public const string ladder_half_page_up = "ladder.half_page_up";
}
public interface IKeybindingProvider : IObservable<IReadOnlyDictionary<string, IReadOnlyList<KeyGesture>>> {
IReadOnlyDictionary<string, IReadOnlyList<KeyGesture>> value { get; }
}
[Export(typeof(IKeybindingProvider))]
public class KeybindingProvider : IKeybindingProvider {
Dictionary<string, List<KeyGesture>> _keybindings = new Dictionary<string, List<KeyGesture>>();
public KeybindingProvider() {
_keybindings[ShellKeybindings.ladder_scroll_down] = new List<KeyGesture> { new KeyGesture(Key.E, ModifierKeys.Control) };
_keybindings[ShellKeybindings.ladder_scroll_up] = new List<KeyGesture> { new KeyGesture(Key.Y, ModifierKeys.Control) };
}
public IReadOnlyDictionary<string, IReadOnlyList<KeyGesture>> value =>
new CovariantWrapper(_keybindings);
public IDisposable Subscribe(IObserver<IReadOnlyDictionary<string, IReadOnlyList<KeyGesture>>> observer) {
observer.OnNext(new CovariantWrapper(_keybindings));
return Disposable.empty;
}
class CovariantWrapper : IReadOnlyDictionary<string, IReadOnlyList<KeyGesture>> {
Dictionary<string, List<KeyGesture>> _keybindings = new Dictionary<string, List<KeyGesture>>();
public CovariantWrapper(Dictionary<string, List<KeyGesture>> keybindings) =>
_keybindings = keybindings;
public IReadOnlyList<KeyGesture> this[string key] => _keybindings[key];
public IEnumerable<string> Keys => _keybindings.Keys;
public IEnumerable<IReadOnlyList<KeyGesture>> Values => _keybindings.Select(kv => kv.Value);
public int Count => _keybindings.Count;
public bool ContainsKey(string key) => _keybindings.ContainsKey(key);
public IEnumerator<KeyValuePair<string, IReadOnlyList<KeyGesture>>> GetEnumerator() =>
_keybindings.Select(kv => new KeyValuePair<string, IReadOnlyList<KeyGesture>>(kv.Key, kv.Value)).GetEnumerator();
public bool TryGetValue(string key, [MaybeNullWhen(false)] out IReadOnlyList<KeyGesture> value) {
if (_keybindings.TryGetValue(key, out var res)) {
value = res;
return true;
}
value = null;
return false;
}
IEnumerator IEnumerable.GetEnumerator() =>
GetEnumerator();
}
}
}
@@ -1,43 +0,0 @@
using bts.api.core;
using bts.api.persistence;
using bts.api.user;
using mbts.config;
using mbts.persist;
using System;
using System.Collections.Generic;
using System.Composition;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.ui.settings {
//
// This class is used for tools like SettingsManager.exe
//
[Export(typeof(IUserPersistFactory))]
class UISettingsFactory : IUserPersistFactory {
Task<User[]> _users;
readonly PersistenceService _persist;
[ImportingConstructor]
public UISettingsFactory(AuthService login, [Import("settings")] PersistenceService persist) {
_persist = persist;
_users = get_users(login);
}
async Task<User[]> get_users(AuthService login) {
var users = await login.get_users_async(null);
if (users.success) return users.value.Users.ToArray();
else return new User[0];
}
public IEnumerable<string> users() { return _users.Result.Select(u => u.Login); }
public IUserPersist get_persist_for(string name) {
var user = _users.Result.FirstOrDefault(u => u.Login == name);
return new UserUISettings(user.Id, _persist, null);
}
}
}
@@ -1,12 +0,0 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace mbts.ui {
public interface ICloseable {
event EventHandler CloseRequested;
void OnClose();
}
}
@@ -1,182 +0,0 @@
using bts;
using bts.api.core;
using bts.api.persistence;
using bts.spark;
using Spark.Core.Protobuf;
using System;
using System.Collections.Generic;
using System.Composition;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
namespace mbts.persist {
public abstract class UISettingsBase : IPersist, IUserPersist, IDisposable {
protected abstract string user();
string uns(string ns) { return $"{user()}#{ns}"; }
PersistenceService _persist;
ErrorReporter _errors;
private const string _settings_change_base = "/ui/settingschange/";
public UISettingsBase(PersistenceService persist, ErrorReporter errors) {
_persist = persist;
_errors = errors;
}
public Task set<T>(string ns, string key, T value) {
return _persist.store_async(new StoreRequest() {
Ns = uns(ns),
Record = {
new Record() {
Key = key,
Value = serialize(value)
}
}
});
}
public Task clear(string ns, string key) {
return _persist.remove_async(new RemoveRequest() {
Ns = uns(ns),
Key = { key }
});
}
public async Task<T> get<T>(string ns, string key) {
var ret = await _persist.retrieve_async(new RetrieveRequest() {
Ns = uns(ns),
Key = { key }
});
check_error(ret, ns, key, out bool empty);
if (empty) return default(T);
return deserialize<T>(ret.value.Record.Single().Value);
}
public async Task<T[]> get_many<T>(string ns, string key) {
var ret = await _persist.retrieve_async(new RetrieveRequest() {
Ns = uns(ns),
Key = { key }
});
check_error(ret, ns, key, out bool empty);
if (empty) {
return Array.Empty<T>();
}
return ProtobufSerializationHelper.deserialize_many<T>(
ret.value.Record.Single().Value.ToByteArray());
}
private Google.Protobuf.ByteString serialize<T>(T obj) {
var buffer = new MemoryStream();
ProtoBuf.Serializer.SerializeWithLengthPrefix(buffer, obj, ProtoBuf.PrefixStyle.Fixed32);
buffer.Seek(0, SeekOrigin.Begin);
return Google.Protobuf.ByteString.FromStream(buffer);
}
private T deserialize<T>(Google.Protobuf.ByteString buffer) {
var stream = new MemoryStream(buffer.ToByteArray());
try {
var rv = ProtoBuf.Serializer.DeserializeWithLengthPrefix<T>(stream, ProtoBuf.PrefixStyle.Fixed32);
return rv;
} catch (Exception ex) {
Log.Gen.warn($"Error deserializing protobuf of type {typeof(T)}: {ex}");
}
return default;
}
private void check_error(Result<RetrieveResponse> result, string ns, string key, out bool empty) {
if (result.success) {
switch (result.value.Result) {
case RetrieveResponse.Types.Result.EmptyNs:
case RetrieveResponse.Types.Result.NoSuchNs:
case RetrieveResponse.Types.Result.NoSuchKey:
empty = true;
return;
case RetrieveResponse.Types.Result.EmptyKey:
case RetrieveResponse.Types.Result.DuplicateKey:
_errors?.report_error($"Invalid Key {result} '{key}'", "Requested Invalid Settings Key", System.Windows.MessageBoxImage.Information);
empty = true;
return;
default:
case RetrieveResponse.Types.Result.Ok:
empty = false;
return;
}
} else {
_errors?.report_error($"There was an error communicating with the settings server: '{result.error_message}'", "Failed to retrieve settings from the server");
empty = true;
}
}
public async Task<IEnumerable<PersistResult<T>>> query<T>(string ns, string pattern) {
var re = new Regex(pattern);
var results = await _persist.retrieve_all_async(
new RetrieveAllRequest() {
Ns = uns(ns)
});
check_error(results, ns, "", out bool empty);
if (empty) return Enumerable.Empty<PersistResult<T>>();
return results.value.Record
.Where(r => re.IsMatch(r.Key))
.Select(r => new PersistResult<T>() {
user = user(),
ns = ns,
key = r.Key,
value = deserialize<T>(r.Value)
}).ToList();
}
public IDisposable subscribe<T>(string ns, string pattern, Action<IEnumerable<PersistResult<T>>> callback) {
return subscribe<T>(ns, pattern).Subscribe(callback);
}
public IObservable<IEnumerable<PersistResult<T>>> subscribe<T>(string ns, string pattern) {
var re = new Regex(pattern);
return _persist
.subscribe_ns(uns(ns))
.Select(update =>
update.added
.Where(item => re.IsMatch(item.Key))
.Select(item => new PersistResult<T>() {
user = user(),
ns = ns,
key = item.Key,
value = deserialize<T>(item.Value)
}))
.SubscribeOn(SynchronizationContext.Current);
}
public void Dispose() {/*UISettings does not own Persistence*/}
}
[Export(typeof(IPersist))]
public class UISettings : UISettingsBase {
protected override string user() { return "all_users"; }
[ImportingConstructor]
public UISettings([Import("settings")] PersistenceService p, ErrorReporter err) : base(p, err) { }
}
[Export(typeof(IUserPersist))]
public class UserUISettings : UISettingsBase {
string _user;
public UserUISettings(uint user_id, [Import("settings")] PersistenceService persist, ErrorReporter err)
: base(persist, err) {
_user = $"User {user_id}";
}
[ImportingConstructor]
public UserUISettings(Lazy<UserTuple> user, [Import("settings")] PersistenceService persist, ErrorReporter err)
: this(user.Value.id, persist, err) { }
protected override string user() { return _user; }
}
}
@@ -1,82 +0,0 @@
namespace mbts.spark {
using bts.utils;
using ProtoBuf;
using System;
interface ISettingsProfile<T> {
uint id { get; }
string name { get; }
T value { get; }
SecurityCriteria security_criteria { get; }
}
struct SettingsProfile<T> : IEquatable<SettingsProfile<T>> where T : class, IEquatable<T> {
public static readonly Func<SettingsProfile<T>, uint> key_fn = ns => ns.id;
public readonly uint id;
public readonly string name;
public readonly T value;
public readonly SecurityCriteria security_criteria;
public SettingsProfile(uint id, string name, T value, SecurityCriteria security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
public SettingsProfile<T> with(uint? id = null, string name = null, T value = null, SecurityCriteria security_criteria = null) =>
new SettingsProfile<T>(id ?? this.id, name ?? this.name, value ?? this.value, security_criteria ?? this.security_criteria);
public bool Equals(SettingsProfile<T> other) =>
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object obj) =>
obj is SettingsProfile<T> other && Equals(other);
public override int GetHashCode() =>
id.GetHashCode();
}
[ProtoContract]
class LadderSettingsProfile : ISettingsProfile<LadderSettings>, IEquatable<LadderSettingsProfile> {
public LadderSettingsProfile() { }
public LadderSettingsProfile(uint id, string name, LadderSettings value, SecurityCriteria security_criteria = null) {
this.id = id;
this.name = name;
this.value = value;
this.security_criteria = security_criteria;
}
[ProtoMember(1)]
public uint id { get; }
[ProtoMember(2)]
public string name { get; }
[ProtoMember(3)]
public LadderSettings value { get; }
[ProtoMember(4)]
public SecurityCriteria security_criteria { get; }
public LadderSettingsProfile with(uint? id = null, LadderSettings value = null) =>
new LadderSettingsProfile(id ?? this.id, name, value ?? this.value, security_criteria);
public bool Equals(LadderSettingsProfile other) =>
other != null &&
id == other.id &&
name == other.name &&
value.safe_equals(other.value) &&
security_criteria.safe_equals(other.security_criteria);
public override bool Equals(object obj) => Equals(obj as LadderSettingsProfile);
public override int GetHashCode() => id.GetHashCode();
}
}
@@ -302,8 +302,12 @@ namespace mbts.ui.controls {
return;
rect.Intersect(this.GetChildBounds(_drag_rect_adorner.AdornedElement));
_drag_rect_adorner.Child.Margin = new Thickness(rect.Left, rect.Top, 0, 0);
_drag_rect_adorner.Child.Width = rect.Width;
_drag_rect_adorner.Child.Height = rect.Height;
if (rect.Width.is_real_non_negative()) {
_drag_rect_adorner.Child.Width = rect.Width;
}
if (rect.Height.is_real_non_negative()) {
_drag_rect_adorner.Child.Height = rect.Height;
}
}
void _select_items_between(object inclusive_begin, object inclusive_end) {
@@ -159,12 +159,13 @@
double font_size = double.NaN,
uint? background = null,
bool? enable_background_color = null,
uint? selection_highlight = null) =>
uint? selection_highlight = null,
uint? font_color = null) =>
new GridSettings(
font_family ?? _font_family,
DoubleHelper.IsNaNOrInfinityOr(font_size, 0) ? _font_size : font_size,
font_weight,
_font_color,
font_color ?? _font_color,
selection_highlight ?? _selection_highlight,
columns ?? _columns,
sorting_criteria ?? _sorting_criteria,
@@ -104,11 +104,11 @@
<c:TickUpDown x:Name="PART_month_tick" Margin="2,0,0,0" Grid.Column="1" Template="{StaticResource HoverButtonOverlayTemplate}">
<TextBox x:Name="PART_month_text" Height="20" u:TextBoxBehavior.EnableArrowKeyTraverse="True" u:Behavior.DisablePaste="True"/>
</c:TickUpDown>
<TextBlock Grid.Column="2" Margin="2,0">/</TextBlock>
<TextBlock Grid.Column="2" VerticalAlignment="Center" Margin="2,0">/</TextBlock>
<c:TickUpDown x:Name="PART_day_tick" Grid.Column="3" Template="{StaticResource HoverButtonOverlayTemplate}">
<TextBox x:Name="PART_day_text" Height="20" u:TextBoxBehavior.EnableArrowKeyTraverse="True" u:Behavior.DisablePaste="True"/>
</c:TickUpDown>
<TextBlock Grid.Column="4" Margin="2,0">/</TextBlock>
<TextBlock Grid.Column="4" VerticalAlignment="Center" Margin="2,0">/</TextBlock>
<c:TickUpDown x:Name="PART_year_tick" Grid.Column="5" Template="{StaticResource HoverButtonOverlayTemplate}">
<TextBox x:Name="PART_year_text" Height="20" u:TextBoxBehavior.EnableArrowKeyTraverse="True" u:Behavior.DisablePaste="True"/>
</c:TickUpDown>
@@ -1,94 +1,94 @@
<Window
x:Class="mbts.ui.spark.NotificationSettingsView"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sp="clr-namespace:mbts.spark"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
FocusManager.IsFocusScope="True"
u:WindowStyle.SizeToContent="WidthAndHeight"
Background="{StaticResource CommonBackground}"
Style="{StaticResource BTSDialog}"
Title="Notification User Settings"
>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Controls/Time/TimeSpanControl.xaml"/>
</ResourceDictionary.MergedDictionaries>
<sys:Double x:Key="HorizontalSplit">8</sys:Double>
<sys:Double x:Key="VerticalSplit">8</sys:Double>
<cv:ValueMap x:Key="ModeToDurationEnabledConverter" Default="{u:False}">
<cv:KeyValue Key="{u:Enum Flash, Type={u:Nested sp:ToastNotificationSettings.Mode}}" Value="{u:True}"/>
</cv:ValueMap>
</ResourceDictionary>
</Window.Resources>
<Grid DataContext="{Binding Controller, RelativeSource={RelativeSource AncestorType=usp:NotificationSettingsView}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<GroupBox Margin="8,8,8,0" Header="Sounds" Padding="6">
<CheckBox IsChecked="{Binding sound_enabled}">
Enabled
</CheckBox>
</GroupBox>
<GroupBox Grid.Row="2" Margin="8,0" Header="Toast Notifications" Padding="6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<CheckBox Grid.ColumnSpan="3" IsChecked="{Binding toast_enabled}">
Enabled
</CheckBox>
<TextBlock Grid.Row="2">
Max Height
</TextBlock>
<c:TickUpDownDouble Grid.Row="2" Grid.Column="2" Value="{Binding toast_max_height}"/>
<StackPanel Grid.Row="4" Grid.ColumnSpan="3" Orientation="Horizontal">
<TextBlock Foreground="{Binding toast_enabled, Converter={StaticResource IsEnabledForegroundConverter}, Mode=OneWay}">
Mode
</TextBlock>
<c:RadioButtonsControl
Orientation="Horizontal"
ItemsSource="{Binding toast_modes}"
SelectedItem="{Binding toast_selected_mode}"
IsEnabled="{Binding toast_enabled}"
/>
</StackPanel>
<c:TimeSpanControl
Grid.Row="6"
Grid.ColumnSpan="3"
Margin="16,0,0,0"
TimeSpan="{Binding toast_duration}"
>
<c:TimeSpanControl.IsEnabled>
<MultiBinding Converter="{x:Static usp:NotificationSettingsView.DurationEnabledConverter}">
<Binding Path="toast_enabled"/>
<Binding Path="toast_selected_mode"/>
</MultiBinding>
</c:TimeSpanControl.IsEnabled>
</c:TimeSpanControl>
</Grid>
</GroupBox>
<c:DialogSkirt Grid.Row="4" RightItems="{Binding dialog_skirt_items}"/>
</Grid>
</Window>
<Window
x:Class="mbts.ui.spark.NotificationSettingsView"
x:ClassModifier="internal"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sp="clr-namespace:mbts.spark"
xmlns:u="clr-namespace:mbts.ui"
xmlns:c="clr-namespace:mbts.ui.controls"
xmlns:cv="clr-namespace:mbts.ui.converters"
xmlns:usp="clr-namespace:mbts.ui.spark"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
FocusManager.IsFocusScope="True"
u:WindowStyle.SizeToContent="WidthAndHeight"
Background="{StaticResource CommonBackground}"
Style="{StaticResource BTSDialog}"
Title="Notification User Settings"
>
<Window.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="/view/Controls/Time/TimeSpanControl.xaml"/>
</ResourceDictionary.MergedDictionaries>
<sys:Double x:Key="HorizontalSplit">8</sys:Double>
<sys:Double x:Key="VerticalSplit">8</sys:Double>
<cv:ValueMap x:Key="ModeToDurationEnabledConverter" Default="{u:False}">
<cv:KeyValue Key="{u:Enum Flash, Type={u:Nested sp:ToastNotificationSettings.Mode}}" Value="{u:True}"/>
</cv:ValueMap>
</ResourceDictionary>
</Window.Resources>
<Grid DataContext="{Binding Controller, RelativeSource={RelativeSource AncestorType=usp:NotificationSettingsView}}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<GroupBox Margin="8,8,8,0" Header="Sounds" Padding="6">
<CheckBox IsChecked="{Binding sound_enabled}">
Enabled
</CheckBox>
</GroupBox>
<GroupBox Grid.Row="2" Margin="8,0" Header="Toast Notifications" Padding="6">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="{Binding Source={StaticResource VerticalSplit}}"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalSplit}}"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<CheckBox Grid.ColumnSpan="3" IsChecked="{Binding toast_enabled}">
Enabled
</CheckBox>
<TextBlock Grid.Row="2">
Max Height
</TextBlock>
<c:TickUpDownDouble Grid.Row="2" Grid.Column="2" Value="{Binding toast_max_height}"/>
<StackPanel Grid.Row="4" Grid.ColumnSpan="3" Orientation="Horizontal">
<TextBlock Foreground="{Binding toast_enabled, Converter={StaticResource IsEnabledForegroundConverter}, Mode=OneWay}">
Mode
</TextBlock>
<c:RadioButtonsControl
Orientation="Horizontal"
ItemsSource="{Binding toast_modes}"
SelectedItem="{Binding toast_selected_mode}"
IsEnabled="{Binding toast_enabled}"
/>
</StackPanel>
<c:TimeSpanControl
Grid.Row="6"
Grid.ColumnSpan="3"
Margin="16,0,0,0"
TimeSpan="{Binding toast_duration}"
>
<c:TimeSpanControl.IsEnabled>
<MultiBinding Converter="{x:Static usp:NotificationSettingsView.DurationEnabledConverter}">
<Binding Path="toast_enabled"/>
<Binding Path="toast_selected_mode"/>
</MultiBinding>
</c:TimeSpanControl.IsEnabled>
</c:TimeSpanControl>
</Grid>
</GroupBox>
<c:DialogSkirt Grid.Row="4" RightItems="{Binding dialog_skirt_items}"/>
</Grid>
</Window>
@@ -1,72 +1,73 @@
using mbts.spark;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.shell;
using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;
using Duration = Google.Protobuf.WellKnownTypes.Duration;
namespace mbts.ui.spark {
partial class NotificationSettingsView : Window {
public NotificationSettingsView(NotificationSettings settings) {
Controller = new NotificationSettingsController(this, settings);
InitializeComponent();
}
public static IMultiValueConverter DurationEnabledConverter =>
ValueConverter.create<bool, ToastNotificationSettings.Mode, bool>((enabled, mode) => enabled && mode == ToastNotificationSettings.Mode.Flash);
public event Action<NotificationSettings> committed;
public NotificationSettingsController Controller { get; }
public class NotificationSettingsController : ViewModelBaseWithValidation {
readonly Property<ToastNotificationSettings.Mode> _toast_selected_mode;
readonly Property<TimeSpan?> _toast_duration;
readonly Property<double> _toast_max_height;
readonly Property<bool> _sound_enabled, _toast_enabled;
public NotificationSettingsController(NotificationSettingsView view, NotificationSettings settings) {
_toast_selected_mode = backing(nameof(toast_selected_mode), settings.toast_notification.mode);
_toast_duration = backing(nameof(toast_duration), settings.toast_notification.duration);
_sound_enabled = backing(nameof(sound_enabled), settings.sound_notification.enabled);
_toast_enabled = backing(nameof(toast_enabled), settings.toast_notification.mode != ToastNotificationSettings.Mode.Disabled);
_toast_max_height = backing(nameof(toast_max_height), settings.toast_notification.max_height);
var commit_cmd = new DelegateCommand(() => {
view.committed?.Invoke(new NotificationSettings(
new SoundNotificationSettings(sound_enabled),
new ToastNotificationSettings(
toast_enabled ? toast_selected_mode : ToastNotificationSettings.Mode.Disabled,
toast_duration.HasValue ? Duration.FromTimeSpan(toast_duration.Value) : null,
toast_max_height)));
WindowUtilities.force_close(view);
});
var cancel_cmd = new DelegateCommand(() => WindowUtilities.force_close(view));
dialog_skirt_items = new[] {
new DialogSkirtItem("OK", commit_cmd) { is_default = true },
new DialogSkirtItem("Cancel", cancel_cmd) { is_cancel = true }
};
}
public bool sound_enabled { get => _sound_enabled.Value; set => _sound_enabled.set(value); }
// toast
public bool toast_enabled { get => _toast_enabled.Value; set => _toast_enabled.set(value); }
public IEnumerable<ToastNotificationSettings.Mode> toast_modes => EnumHelper.get_values_except(ToastNotificationSettings.Mode.Disabled);
public ToastNotificationSettings.Mode toast_selected_mode { get => _toast_selected_mode.Value; set { _toast_selected_mode.set(value); _validate(); } }
public TimeSpan? toast_duration { get => _toast_duration.Value; set { _toast_duration.set(value); _validate(); } }
public double toast_max_height { get => _toast_max_height.Value; set => _toast_max_height.set(value); }
//
public DialogSkirtItem[] dialog_skirt_items { get; }
void _validate() {
using (validation())
validation_assert(toast_selected_mode != ToastNotificationSettings.Mode.Flash || (toast_duration.HasValue && toast_duration.Value > TimeSpan.Zero), string.Empty, nameof(toast_duration));
}
}
}
}
using mbts.spark;
using mbts.ui.controls;
using mbts.ui.converters;
using mbts.ui.shell;
using mbts.ui.utility;
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Data;
using Duration = Google.Protobuf.WellKnownTypes.Duration;
namespace mbts.ui.spark {
partial class NotificationSettingsView : Window {
public NotificationSettingsView(NotificationSettings settings) {
Controller = new NotificationSettingsController(this, settings);
InitializeComponent();
}
public static IMultiValueConverter DurationEnabledConverter =>
ValueConverter.create<bool, ToastNotificationSettings.Mode, bool>((enabled, mode) => enabled && mode == ToastNotificationSettings.Mode.Flash);
public event Action<NotificationSettings> committed;
public NotificationSettingsController Controller { get; }
public class NotificationSettingsController : ViewModelBaseWithValidation {
readonly Property<ToastNotificationSettings.Mode> _toast_selected_mode;
readonly Property<TimeSpan?> _toast_duration;
readonly Property<double> _toast_max_height;
readonly Property<bool> _sound_enabled, _toast_enabled;
public NotificationSettingsController(NotificationSettingsView view, NotificationSettings settings) {
_toast_selected_mode = backing(nameof(toast_selected_mode), settings.toast_notification.mode);
_toast_duration = backing<TimeSpan?>(
nameof(toast_duration), settings.toast_notification.duration);
_sound_enabled = backing(nameof(sound_enabled), settings.sound_notification.enabled);
_toast_enabled = backing(nameof(toast_enabled), settings.toast_notification.mode != ToastNotificationSettings.Mode.Disabled);
_toast_max_height = backing(nameof(toast_max_height), settings.toast_notification.max_height);
var commit_cmd = new DelegateCommand(() => {
view.committed?.Invoke(new NotificationSettings(
new SoundNotificationSettings(sound_enabled),
new ToastNotificationSettings(
toast_enabled ? toast_selected_mode : ToastNotificationSettings.Mode.Disabled,
toast_duration.HasValue ? Duration.FromTimeSpan(toast_duration.Value) : null,
toast_max_height)));
WindowUtilities.force_close(view);
});
var cancel_cmd = new DelegateCommand(() => WindowUtilities.force_close(view));
dialog_skirt_items = new[] {
new DialogSkirtItem("OK", commit_cmd) { is_default = true },
new DialogSkirtItem("Cancel", cancel_cmd) { is_cancel = true }
};
}
public bool sound_enabled { get => _sound_enabled.Value; set => _sound_enabled.set(value); }
// toast
public bool toast_enabled { get => _toast_enabled.Value; set => _toast_enabled.set(value); }
public IEnumerable<ToastNotificationSettings.Mode> toast_modes => EnumHelper.get_values_except(ToastNotificationSettings.Mode.Disabled);
public ToastNotificationSettings.Mode toast_selected_mode { get => _toast_selected_mode.Value; set { _toast_selected_mode.set(value); _validate(); } }
public TimeSpan? toast_duration { get => _toast_duration.Value; set { _toast_duration.set(value); _validate(); } }
public double toast_max_height { get => _toast_max_height.Value; set => _toast_max_height.set(value); }
//
public DialogSkirtItem[] dialog_skirt_items { get; }
void _validate() {
using (validation())
validation_assert(toast_selected_mode != ToastNotificationSettings.Mode.Flash || (toast_duration.HasValue && toast_duration.Value > TimeSpan.Zero), string.Empty, nameof(toast_duration));
}
}
}
}
@@ -174,7 +174,7 @@ namespace mbts.ui.shell {
return await await vm.Dispatcher.check_invoke_async(mcs.MeasureContentSize);
}
Action _create_undo_action(ShellWindowVM document) {
async Task<Action> _create_undo_action(ShellWindowVM document) {
if (document is null) {
throw new ArgumentNullException(nameof(document));
}
@@ -182,7 +182,7 @@ namespace mbts.ui.shell {
return null;
}
var state = _window_settings.get_window_state(document.Model);
var state = await _window_settings.get_window_state(document.Model);
if (state is null) {
return null;
}
@@ -199,15 +199,15 @@ namespace mbts.ui.shell {
};
}
void _tab_closing(object sender, ShellWindowVM document) {
async void _tab_closing(object sender, ShellWindowVM document) {
if (sender != this) {
var undo = _create_undo_action(document);
var undo = await _create_undo_action(document);
if (undo is not null) {
_recently_closed_windows.Push(undo);
}
}
if (document.HasCustomTitle) {
_bookmark.save_bookmark(document.Model);
_bookmark.save_bookmark(document.Model).ignore();
}
}
}
@@ -29,12 +29,10 @@ namespace mbts.ui.shell {
public async Task<IEnumerable<string>> get_workspaces(User user) {
if (!_cache.TryGetValue(user.id, out List<string> result)) {
using (var svc = new UserUISettings(user.id, _persist, null)) {
var workspaces = await svc.query<WorkspaceDefinition>(WorkspacesModel.WORKSPACE_NAMESPACE, ".*");
result = workspaces.Select(w => w.key).ToList();
_cache[user.id] = result;
return result;
}
var svc = new SettingsPersistenceModel($"User {user.id}", _persist, null);
var workspaces = await svc.query<WorkspaceDefinition>(WorkspacesModel.WORKSPACE_NAMESPACE, ".*");
result = workspaces.Select(w => w.key).ToList();
_cache[user.id] = result;
}
return result;
}
@@ -11,6 +11,7 @@ using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Interop;
using System.Windows.Threading;
@@ -89,7 +90,7 @@ namespace mbts.ui.shell {
public Dispatcher get_dispatcher(ISharedWindow window) {
if (window.thread_id == null) {
window.thread_init();
return Dispatcher.CurrentDispatcher;
return Application.Current.Dispatcher;
}
var uid = get(window.thread_id);
return attach_window(uid, window);
@@ -101,6 +102,9 @@ namespace mbts.ui.shell {
}
public Dispatcher get_dispatcher(string id) {
if (id is null) {
return Application.Current.Dispatcher;
}
return get(id).dispatcher;
}
}
@@ -138,7 +138,7 @@ namespace mbts.ui.shell {
async void _launcher_closing(CancelEventArgs obj) {
_closing = true;
var workspace = get_workspace();
var workspace = await get_workspace();
var task = _private.set(WORKSPACE_NAMESPACE, ACTIVE_WORKSPACE_NAME, workspace);
await _wm.clear();
task.Wait(TimeSpan.FromSeconds(90));
@@ -186,9 +186,9 @@ namespace mbts.ui.shell {
public ObservableCollection<WorkspaceViewModel> Workspaces { get; private set; }
public event Action<string> WorkspaceLoaded;
WorkspaceDefinition get_workspace() {
async Task<WorkspaceDefinition> get_workspace() {
WorkspaceDefinition workspace = new WorkspaceDefinition();
workspace.windows = _settings_collector.get_window_state().ToList();
workspace.windows = await _settings_collector.get_window_state();
workspace.modules = _settings_collector.get_module_state().ToList();
workspace.launcher_state = _settings_collector.get_launcher_state();
workspace.named_windows = _bookmarks.bookmarks.ToList();
@@ -198,7 +198,7 @@ namespace mbts.ui.shell {
async Task<WorkspaceDefinition> get_workspace_async() {
WorkspaceDefinition workspace = new WorkspaceDefinition();
workspace.windows = new List<WindowState>();
foreach (var w in _settings_collector.get_window_state()) {
foreach (var w in await _settings_collector.get_window_state()) {
workspace.windows.Add(w);
await Task.Yield();
}
@@ -212,8 +212,8 @@ namespace mbts.ui.shell {
return workspace;
}
public void save_workspace(string name, bool persist = true, bool ispublic = false) {
WorkspaceDefinition workspace = get_workspace();
public async void save_workspace(string name, bool persist = true, bool ispublic = false) {
WorkspaceDefinition workspace = await get_workspace();
workspace.is_shared = ispublic;
workspace.last_save_lldatetime = LLDateTime.now().value;
if (ispublic) { name += SHARED_MONIKER; }
@@ -226,7 +226,7 @@ namespace mbts.ui.shell {
Workspaces.Add(new WorkspaceViewModel(this, name, workspace));
if (persist) {
(ispublic ? _public : _private).set(WORKSPACE_NAMESPACE, name, workspace);
(ispublic ? _public : _private).set(WORKSPACE_NAMESPACE, name, workspace).ignore();
}
}
@@ -86,11 +86,11 @@ namespace mbts.ui {
return anim;
TimeSpan expand_dur() =>
_compute_dur(expanded_height, (120, .25), (140, .19), (160, .15), (360, .27))
_compute_dur(expanded_height, (120, .20), (140, .15), (160, .07), (360, .08))
.at_least_total_secs(MIN_DUR_SECS);
TimeSpan collapse_dur() =>
_compute_dur(expanded_height, (120, .20), (140, .14), (160, .10), (360, 0.10))
_compute_dur(expanded_height, (120, .14), (140, .08), (160, .06), (360, 0.07))
.at_least_total_secs(MIN_DUR_SECS);
}
@@ -477,6 +477,15 @@ namespace mbts.ui {
public static string join_binding_paths(params string[] paths) => string.Join(".", paths);
public static Rect? try_mk_rect(double left, double top, double right, double bottom) =>
left.is_real()
&& top.is_real()
&& right.is_real()
&& bottom.is_real()
&& left <= right
&& top <= bottom ?
new(left, top, right, bottom) : null;
public static class Hooks {
public static void exchange_is_keyboard_focused_within(FrameworkElement oldval, FrameworkElement newval, DependencyPropertyChangedEventHandler handler) {
if (oldval != null) oldval.IsKeyboardFocusWithinChanged -= handler;
+3
View File
@@ -15,6 +15,9 @@ namespace bts.utils {
public T value => has_value ?
_value : throw new InvalidOperationException($"value uninitialized");
public T or(T fallback) =>
has_value ? _value : fallback;
public bool Equals(Optional<T> other) =>
has_value == other.has_value &&
(!has_value || EqualityComparer<T>.Default.Equals(value, other.value));
@@ -70,6 +70,13 @@ namespace Spark.Core.Protobuf {
return ms.ToArray();
}
public static ByteString serialize_to_byte_string<T>(T value) {
using var ms = new MemoryStream();
Serializer.SerializeWithLengthPrefix(ms, value, PrefixStyle.Fixed32);
ms.Seek(0, SeekOrigin.Begin);
return ByteString.FromStream(ms);
}
public static string serialize_to_string<T>(T value) {
using var ms = new MemoryStream();
serialize(ms, value);
@@ -91,6 +98,11 @@ namespace Spark.Core.Protobuf {
}
}
public static T deserialize<T>(ByteString byte_string) {
using var ms = new MemoryStream(byte_string.ToByteArray());
return Serializer.DeserializeWithLengthPrefix<T>(ms, PrefixStyle.Fixed32);
}
public static T deserialize<T>(byte[] bytes) {
using var ms = new MemoryStream(bytes);
return Serializer.DeserializeWithLengthPrefix<T>(ms, PrefixStyle.Fixed32);