Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbb4dd6e1f | ||
|
|
f977b0fdd0 | ||
|
|
a3968bd35b | ||
|
|
2b94a29963 | ||
|
|
4b8abc2005 | ||
|
|
2627264eea | ||
|
|
2c7186275f | ||
|
|
a4d8cd9dfc | ||
|
|
ed4be2f1dd | ||
|
|
cd6d504f06 | ||
|
|
e68d37f24d |
No files matched your search
@@ -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);
|
||||
|
||||
+31
-31
@@ -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);
|
||||
}
|
||||
}
|
||||
+88
-88
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
-86
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+95
-95
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+153
-171
@@ -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();
|
||||
}
|
||||
}
|
||||
+73
-73
@@ -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() { }
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+242
-195
@@ -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;
|
||||
}
|
||||
}
|
||||
File renamed without changes.
+226
-177
@@ -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));
|
||||
}
|
||||
}
|
||||
+77
-77
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+25
-25
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
+101
-101
@@ -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;
|
||||
}
|
||||
}
|
||||
+19
-19
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+245
-245
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+849
-849
File diff suppressed because it is too large.
Load diff
+34
-34
@@ -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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
+160
-160
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+157
-157
@@ -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;
|
||||
}
|
||||
}
|
||||
+483
-483
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+667
-667
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);
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
+94
-94
@@ -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>
|
||||
+73
-72
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user