Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3369db7cc | ||
|
|
279abcfefd | ||
|
|
872e2465a6 | ||
|
|
89fb79c556 | ||
|
|
c92e9df77b | ||
|
|
81aaa853d5 | ||
|
|
4f308faba2 | ||
|
|
0b924033c2 | ||
|
|
3606c707e4 | ||
|
|
5998107e6a | ||
|
|
297982433b | ||
|
|
15ac9d945b | ||
|
|
a0f1d11692 | ||
|
|
e7890a459c | ||
|
|
f7fb5ed400 | ||
|
|
75e98d353f | ||
|
|
5fc6ff4c00 | ||
|
|
6951272633 | ||
|
|
548611b44f | ||
|
|
322da04723 | ||
|
|
3544eac815 | ||
|
|
7fef7d88ce | ||
|
|
7531cf5eb4 | ||
|
|
5826a083c0 | ||
|
|
ec0d5d22f1 | ||
|
|
d433368795 | ||
|
|
dc4453e510 | ||
|
|
1b1583abda | ||
|
|
9d2f2888bb | ||
|
|
26816f0048 | ||
|
|
e97e6b93c7 | ||
|
|
0d8252e131 | ||
|
|
801b9622f8 | ||
|
|
a5df3f2da2 | ||
|
|
f03a413855 | ||
|
|
f3a692bda2 | ||
|
|
6c3bdbdc34 | ||
|
|
d4749dd381 | ||
|
|
a008f4d0fd | ||
|
|
0ee9521454 | ||
|
|
62cef7c2f7 | ||
|
|
1b39f4e47c | ||
|
|
3212b2fcee | ||
|
|
1576541bd1 | ||
|
|
62e97364fb | ||
|
|
93b954ce46 | ||
|
|
c9da022c58 | ||
|
|
0772a13b09 | ||
|
|
c53f6ad906 | ||
|
|
1058c8a80e | ||
|
|
7ecb106880 | ||
|
|
b0c1d93c50 | ||
|
|
0c5a039226 | ||
|
|
2677144bfa | ||
|
|
6b30c42c0d | ||
|
|
2f62d29f66 | ||
|
|
828f867a80 | ||
|
|
c9663cf7ab | ||
|
|
b85de51d0c | ||
|
|
dc8e65923d | ||
|
|
1469373b9c | ||
|
|
820e937336 | ||
|
|
8b73878782 | ||
|
|
b15106dac4 | ||
|
|
100316b791 | ||
|
|
3c98425226 | ||
|
|
1aa9c46367 | ||
|
|
5211cab351 | ||
|
|
e89a4cce53 | ||
|
|
cb56b90737 | ||
|
|
761a89f052 | ||
|
|
cab6b56606 | ||
|
|
8a743a91a5 | ||
|
|
5dec5dc392 | ||
|
|
27aa4fca26 | ||
|
|
84a2e50bf7 | ||
|
|
297eb5b25b | ||
|
|
d9d0e1f4ef | ||
|
|
6ec0cc8bbe | ||
|
|
795b77ea3f | ||
|
|
91c97d32bb | ||
|
|
e0c50703d7 | ||
|
|
dab1e76766 | ||
|
|
b8c927df27 | ||
|
|
2684d25f09 | ||
|
|
7c95a77ae2 | ||
|
|
31b115d13b | ||
|
|
d681a6924b | ||
|
|
d0cce84d2e | ||
|
|
532591c34f | ||
|
|
562b9419bd | ||
|
|
7f187224c1 | ||
|
|
79b2121f5e | ||
|
|
0bd477594c | ||
|
|
149cd90fb4 | ||
|
|
f74e719a9f | ||
|
|
8b68beae96 | ||
|
|
9cc0b471f2 | ||
|
|
3e035197d0 | ||
|
|
c9d8a640b6 | ||
|
|
1d97e620fd | ||
|
|
f6299ab5c3 | ||
|
|
eaff4774e0 | ||
|
|
bed52db98a | ||
|
|
bd3ef33607 | ||
|
|
25a594895e | ||
|
|
4d468c8026 | ||
|
|
68ed90487b | ||
|
|
ddc2c67fea | ||
|
|
77459e9c64 | ||
|
|
db64f2f45b | ||
|
|
3f7ea71de7 | ||
|
|
d334418655 | ||
|
|
d1844afeae | ||
|
|
b6dc2bd4d4 | ||
|
|
58906eb3b6 | ||
|
|
50e61604ba | ||
|
|
6bd23f106f | ||
|
|
dfd8348724 | ||
|
|
963a4207e2 | ||
|
|
d2e9352e48 | ||
|
|
da42e95754 | ||
|
|
a878a55ac1 | ||
|
|
4305d560aa | ||
|
|
055f0015e7 | ||
|
|
a39d91ceb3 | ||
|
|
ddc50e1fba | ||
|
|
ffb571d1cf | ||
|
|
2a36c05d75 | ||
|
|
a3730eb203 | ||
|
|
d033275694 | ||
|
|
4e58342677 | ||
|
|
5eeb5a534c | ||
|
|
016ddf9b69 | ||
|
|
0cc5817932 | ||
|
|
032836dea4 | ||
|
|
f20c53bced | ||
|
|
a06461eb5e | ||
|
|
34e7d6a617 | ||
|
|
276e9ad0ef | ||
|
|
5968ac10d2 | ||
|
|
31e44d8c10 | ||
|
|
9bf7666c30 | ||
|
|
8fdab4a510 | ||
|
|
385adaa1b0 | ||
|
|
f9506bc9ad | ||
|
|
b9fd0e2e28 | ||
|
|
6b5b517d70 | ||
|
|
80850146c1 | ||
|
|
0680dddab4 | ||
|
|
70aa62c0cf | ||
|
|
06165b3842 | ||
|
|
f21902d60d | ||
|
|
a27f383b62 | ||
|
|
35d4fcc513 | ||
|
|
8a96b00f64 | ||
|
|
2d06fd3d92 | ||
|
|
76050e0722 | ||
|
|
a3f0c2fbf6 | ||
|
|
1ccc4c3746 | ||
|
|
e5d85db888 | ||
|
|
e47b1c3048 | ||
|
|
039791a296 | ||
|
|
e6884b6654 | ||
|
|
8cbae28dd8 | ||
|
|
010a1fecb1 | ||
|
|
ec1d925dbe | ||
|
|
96bec55ee7 | ||
|
|
424bb36fc7 | ||
|
|
c24700195f | ||
|
|
f7184b0949 | ||
|
|
633ebf38a2 | ||
|
|
1eb8352b6b | ||
|
|
33c201b395 | ||
|
|
999973c349 | ||
|
|
4663ae9e4d | ||
|
|
34fc6af137 | ||
|
|
6cc25a7337 | ||
|
|
0c9a9fcb5e | ||
|
|
48b62f1cab | ||
|
|
5a0ceafa66 | ||
|
|
d31969606e | ||
|
|
6c6fb74506 | ||
|
|
a69ed30e70 | ||
|
|
a654f302a2 | ||
|
|
9ca3285888 | ||
|
|
d7d170f228 | ||
|
|
df4c3b9376 | ||
|
|
cf977befd1 | ||
|
|
1d538b4cb0 | ||
|
|
f21c9f96d1 | ||
|
|
16a71a9f63 | ||
|
|
b42febdf60 | ||
|
|
71915de4b0 | ||
|
|
facb3249d0 | ||
|
|
f323efc61a | ||
|
|
be5f54326e | ||
|
|
a8689aee06 | ||
|
|
85b2d257a7 | ||
|
|
f49ca9f7bd | ||
|
|
297233eae6 | ||
|
|
0114dd0276 | ||
|
|
ffd35aebac | ||
|
|
c9b2ac290c | ||
|
|
b92fed4f47 | ||
|
|
340d65a47c | ||
|
|
f89fb76cb2 | ||
|
|
16adb9ee18 | ||
|
|
154a637ee1 | ||
|
|
399e4bf654 | ||
|
|
5908b4478e | ||
|
|
44571a5af5 | ||
|
|
62274f5129 | ||
|
|
b5b5101faf | ||
|
|
bb232d8f1e | ||
|
|
1b3f5bc8a6 | ||
|
|
1b767f6e5e | ||
|
|
fe647044f8 | ||
|
|
aaca502500 | ||
|
|
69dcfdb2bd | ||
|
|
e179f7e5a4 | ||
|
|
dba3747d98 | ||
|
|
3e7627ac24 | ||
|
|
1b043f77cb |
No files matched your search
@@ -33,3 +33,4 @@ trace_data/
|
||||
/src/lib/orderman/perf_analysis/log.csv
|
||||
/doc/user/public/*
|
||||
.ionide/*
|
||||
/.ignore
|
||||
@@ -154,7 +154,7 @@ insert_anchor_links = "right"
|
||||
|
||||
- Pay Less - If enabled by checking the checkbox, the spreader will
|
||||
attempt to pay less when the quantity at the level of the calculated
|
||||
hedge price is more than the threshold defined. The hedge order is
|
||||
hedge price is less than the threshold defined. The hedge order is
|
||||
paid less by the “Payless Ticks” as defined per leg.
|
||||
- Pay Up - If enabled by checking the checkbox, the spreader will pay
|
||||
up when the quantity at the level of the calculated hedge price is
|
||||
|
||||
@@ -4,19 +4,53 @@ weight = 3
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
## 2.13.0
|
||||
### User Visible Changes
|
||||
- Added the name of the spreader to the parent algo badge (for spreader child orders), in the fills, orderbook and floating orderbook windows
|
||||
- Added support for sounds for new orders
|
||||
- Added a new quoting throttle parameter for the spreader, that throttles quotes that are moving toward the inside market but are still a configurable number of ticks away
|
||||
- Enabled the ladder to remember the left panel scroll position across front end restarts
|
||||
### Bug Fixes
|
||||
- Fixed the local fills window so that it does not show the admin fills
|
||||
- Removed the deleted accounts from the ladder accounts dropdown
|
||||
- Fixed the orderbook price for iceberg of spreaders
|
||||
|
||||
## 2.12.0
|
||||
### User Visible Changes
|
||||
- Added support for ICE market data and order execution
|
||||
### Internal Changes
|
||||
- Lots of permission / admin changes to facilitate ICE
|
||||
|
||||
## 2.11.0
|
||||
### User Visible Changes
|
||||
- Fixed fills form time filter to also apply to SOD fills
|
||||
- Addressed an issue that could prevent the web Admin UI from displaying over slow HTTP connections
|
||||
- Removed the option to enter manual spread fills
|
||||
- Fixed an issue that could cause erroneous fill account attribution in the event of a crash
|
||||
### Internal Changes
|
||||
- Added support for tag 1031. Currently defaults to Y, can it be override through config.
|
||||
- To facilitate support, allowed the bts user to view and edit algos from all other users
|
||||
- Improved system tests
|
||||
- Improved log messages in UI to include dispatcher thread name
|
||||
- Added support for Windows core dumps in the UI
|
||||
|
||||
## 2.10.0
|
||||
### User Visible Changes
|
||||
- Fills now include information about the algo that originated the fill
|
||||
- A new, more powerful PnL form is now available
|
||||
### API Changes
|
||||
- Added security state information (pre open, halted, normal, closed, etc)
|
||||
### Bug Fixes
|
||||
- Brought back the "select all" check box in the mini order book
|
||||
- Fixed an issue in the market data subsystem that on rare occasions could result in a startup crash
|
||||
### Internal Changes
|
||||
- Fixed an issue that was stopping us from interpreting SOD times as local rather than GMT
|
||||
|
||||
## 2.9.3
|
||||
### Internal Changes
|
||||
- Fixed system test scripts
|
||||
## 2.9.2
|
||||
### Bug Fixes
|
||||
- Fixed an issue with the exchange simulator that could lead to crossed markets
|
||||
|
||||
## 2.9.1
|
||||
### Bug Fixes
|
||||
- Fixed an issue with trade sound alerts, where in some cases the use of a "In Between" condition could lead to a UI failure.
|
||||
|
||||
+1
-1
Submodule docker/linux updated: e13bb5d8df...c40e6eba4b.
+1
-1
Submodule externals updated: 67a79263ba...6d8f64ca5c.
@@ -1,83 +1,74 @@
|
||||
using bts;
|
||||
using bts.api.admin;
|
||||
using bts.api.core;
|
||||
using bts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using bts.api.core;
|
||||
using mbts.ui.shell;
|
||||
|
||||
namespace mbts.spark {
|
||||
[Export]
|
||||
class AdminModel : IDisposable {
|
||||
public readonly UserTuple user;
|
||||
|
||||
readonly BehaviorSubject<IReadOnlyDictionary<uint, string>>
|
||||
_accounts_subject = new BehaviorSubject<IReadOnlyDictionary<uint, string>>(new Dictionary<uint, string>()),
|
||||
_users_subject = new BehaviorSubject<IReadOnlyDictionary<uint, string>>(new Dictionary<uint, string>());
|
||||
IReadOnlyDictionary<uint, string>
|
||||
_account_map = new Dictionary<uint, string>(),
|
||||
_user_map = new Dictionary<uint, string>();
|
||||
readonly BehaviorSubject<AdminModelWrapper> _wrapper_subject = new BehaviorSubject<AdminModelWrapper>(new AdminModelWrapper());
|
||||
IDisposable _dispose;
|
||||
|
||||
IDisposable _dispose;
|
||||
IReadOnlyDictionary<uint, string> _account_map = DictionaryHelper.empty<uint, string>();
|
||||
|
||||
[ImportingConstructor]
|
||||
AdminModel(AdminService service, [Import]UserTuple user) {
|
||||
AdminModel(AdminService service, UserTuple user) {
|
||||
this.user = user;
|
||||
_dispose = service.subscribei_admin_data().Select(update => {
|
||||
_wrapper_subject.OnNext(new AdminModelWrapper(update.all.TryFirst(), user.name));
|
||||
IEnumerable<BTSUserImmutable> active_users =
|
||||
from admin_data in update.all
|
||||
from bts_user in admin_data.Users
|
||||
where bts_user.Name == user.name && bts_user.HasUserData && bts_user.UserData.HasAccountPermissions
|
||||
select bts_user;
|
||||
|
||||
IEnumerable<BTSAccountImmutable> active_accounts =
|
||||
from admin_data in update.all
|
||||
from bts_account in admin_data.Accounts
|
||||
where bts_account.IsPresent && _wrapper_subject.Value.can_view_account(bts_account.Name)
|
||||
join account_id in (
|
||||
from bts_user in active_users
|
||||
from account_id in bts_user.UserData.AccountPermissions.AccountPermissions.Keys
|
||||
select account_id
|
||||
) on bts_account.Id equals account_id
|
||||
select bts_account;
|
||||
|
||||
var users = active_users.ToDictionary(u => u.Id, u => u.Name);
|
||||
var accounts = active_accounts.ToDictionary(a => a.Id, a => a.Name);
|
||||
return (users, accounts);
|
||||
}).Subscribe(t => {
|
||||
_account_map = t.accounts;
|
||||
_user_map = t.users;
|
||||
_users_subject.OnNext(_user_map = t.users);
|
||||
_accounts_subject.OnNext(_account_map = t.accounts);
|
||||
});
|
||||
// accounts
|
||||
on_accounts = service.subscribei_admin_data()
|
||||
.Select(up => up.all.TryGetFirst(out var ad) && ad.Users.Where(u => u.Name == user.name).TryGetSingle(out var usr) && usr.HasUserData && usr.UserData.HasAccountPermissions ?
|
||||
usr.UserData.AccountPermissions.AccountPermissions
|
||||
.Where(kv => (kv.Value.TradingPermission != null && kv.Value.TradingPermission.HasPermissions) || kv.Value.AdminPermission.HasPermissions)
|
||||
.Select(kv => ad.Accounts.Where(a => a.Id == kv.Key).TryGetSingle(out var acc) ? acc : (BTSAccountImmutable?)null)
|
||||
.Where(acc => acc.HasValue && acc.Value.IsPresent)
|
||||
.Select(acc => acc.Value)
|
||||
.ToDictionary(acc => acc.Id, acc => acc.Name) :
|
||||
new Dictionary<uint, string>(0))
|
||||
.Replay(1)
|
||||
.RefCount();
|
||||
// users
|
||||
on_users = Observable.Return(DictionaryHelper.wrap_value(user.id, user.name));
|
||||
all_user_tuples = user.permissions == UserTuple.Permissions.Admin ?
|
||||
service.subscribei_admin_data()
|
||||
.Select(u => u.all.TryFirst())
|
||||
.Where(ad => ad.HasValue)
|
||||
.Select(ad => ad.Value.Users
|
||||
.Where(u => user.can_access_user_data(u.Id))
|
||||
.Select(u => new UserTuple(u.Id, u.Name))
|
||||
.ToDictionary(UserTuple.key_fn))
|
||||
.Table(UserTuple.key_fn)
|
||||
.Cache() :
|
||||
Observable.Return(TableUpdate<UserTuple, uint>.mk_empty(UserTuple.key_fn).with_added(new[] { user }));
|
||||
_dispose = on_accounts.Subscribe(map => _account_map = map);
|
||||
}
|
||||
|
||||
public IObservable<IReadOnlyDictionary<uint, string>> on_users => _users_subject.AsObservable();
|
||||
public IObservable<IReadOnlyDictionary<uint, string>> on_accounts => _accounts_subject.AsObservable();
|
||||
public IObservable<IReadOnlyList<UserTuple>> on_user_tuples => on_users.Select(d => d.Select(kv => new UserTuple(kv.Key, kv.Value)).ToList());
|
||||
public IObservable<IReadOnlyList<AccountTuple>> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToList());
|
||||
public IObservable<AdminModelWrapper> on_admin_data => _wrapper_subject.AsObservable();
|
||||
public IObservable<IReadOnlyDictionary<uint, string>> on_users { get; }
|
||||
public IObservable<IReadOnlyDictionary<uint, string>> on_accounts { get; }
|
||||
public IObservable<TableUpdate<UserTuple, uint>> all_user_tuples { get; }
|
||||
public IObservable<IReadOnlyList<AccountTuple>> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToListSorted(a => a.name));
|
||||
|
||||
public AccountTuple? get_default_account() {
|
||||
foreach (var kv in _account_map) {
|
||||
return new AccountTuple(kv.Key, kv.Value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public uint? get_user_id(string user) => _id_for_name(user, _user_map);
|
||||
public uint? get_account_id(string account) => _id_for_name(account, _account_map);
|
||||
public void Dispose() => Disposable.dispose(ref _dispose);
|
||||
public IObservable<Update<T, TKey>> permission_filter<T, TKey>(IObservable<Update<T, TKey>> positions, Func<T, uint> get_account_id) where TKey : IEquatable<TKey> =>
|
||||
user.permissions == UserTuple.Permissions.Admin ?
|
||||
positions :
|
||||
positions.Filter(on_accounts.Select(map => new Func<T, bool>(p => map.ContainsKey(get_account_id(p)))));
|
||||
|
||||
static uint? _id_for_name(string name, IReadOnlyDictionary<uint, string> map) {
|
||||
foreach (var kv in map) {
|
||||
if (kv.Value == name)
|
||||
return kv.Key;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public IObservable<Update<T, TKey>> permission_filter<T, TKey>(IObservable<Update<T, TKey>> positions, Func<T, string> get_account_name) where TKey : IEquatable<TKey> =>
|
||||
user.permissions == UserTuple.Permissions.Admin ?
|
||||
positions :
|
||||
positions.Filter(on_accounts.Select(map => {
|
||||
var hash = new HashSet<string>(map.Values);
|
||||
return new Func<T, bool>(v => hash.Contains(get_account_name(v)));
|
||||
}));
|
||||
|
||||
public AccountTuple? get_default_account() =>
|
||||
_account_map.Select(kv => new AccountTuple(kv.Key, kv.Value)).TryFirst();
|
||||
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
}
|
||||
}
|
||||
@@ -3,15 +3,10 @@ using mbts;
|
||||
using mbts.config;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Input;
|
||||
|
||||
@@ -23,8 +18,8 @@ namespace bts.ui {
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
public AboutDaemon(IBus bus, [Import(LoginContainer.Exports.UserName)]string username) {
|
||||
_user = username;
|
||||
public AboutDaemon(IBus bus, UserTuple user) {
|
||||
_user = user.name;
|
||||
_bus = bus;
|
||||
_dispose = bus.subscribe(SparkTags.About).OnDispatcher().Subscribe(_ => show_about());
|
||||
}
|
||||
@@ -63,4 +58,4 @@ namespace bts.ui {
|
||||
Disposable.dispose(ref _dispose);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,13 +7,11 @@ using mbts.ui;
|
||||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Threading;
|
||||
using System.Windows;
|
||||
using System.Threading.Tasks;
|
||||
using mbts.ui.shell;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace bts.spark
|
||||
{
|
||||
namespace bts.spark {
|
||||
[Export, Export(typeof(IShellDaemon))]
|
||||
public class ErrorReporter : IShellDaemon, IDisposable {
|
||||
readonly ErrorReportService _svc;
|
||||
@@ -21,9 +19,9 @@ namespace bts.spark
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
public ErrorReporter(ErrorReportService svc, IBus bus, [Import(LoginContainer.Exports.UserName)] string username) {
|
||||
public ErrorReporter(ErrorReportService svc, IBus bus, UserTuple user) {
|
||||
_svc = svc;
|
||||
_username = username;
|
||||
_username = user.name;
|
||||
_dispose = bus.subscribe(SparkTags.Support).OnDispatcher().Subscribe(msg => show_support_window(msg, "Report a problem"));
|
||||
}
|
||||
|
||||
@@ -48,6 +46,45 @@ namespace bts.spark
|
||||
ErrorMessage = message,
|
||||
});
|
||||
}
|
||||
public async Task report_silently(string contact, string description, string userdata) {
|
||||
var request = append_logs(new ErrorReport {
|
||||
User = _username,
|
||||
ErrorMessage = description,
|
||||
UserDescription = userdata,
|
||||
Contact = contact
|
||||
}.immutable());
|
||||
var reply = await _svc.send_report_async(request);
|
||||
}
|
||||
|
||||
public static ErrorReportImmutable append_logs(ErrorReportImmutable src) {
|
||||
var result = src.CloneToMutable();
|
||||
|
||||
var windows_log = LogUtil.read_windows_event_log();
|
||||
if (!windows_log.IsEmpty) {
|
||||
result.Files.Add(new FileData {
|
||||
Name = "Application.evtx.gz",
|
||||
Contents = windows_log
|
||||
});
|
||||
}
|
||||
|
||||
var (data,date) = LogUtil.read_latest_crash_dump();
|
||||
if (!data.IsEmpty) {
|
||||
result.Files.Add(new FileData {
|
||||
Name = $"spark-{date.ToString("s")}.dmp.gz",
|
||||
Contents = data
|
||||
});
|
||||
}
|
||||
|
||||
var spark_log = LogUtil.read_logs_compressed();
|
||||
if (!spark_log.IsEmpty) {
|
||||
result.Files.Add(new FileData {
|
||||
Name = "log.gz",
|
||||
Contents = spark_log
|
||||
});
|
||||
}
|
||||
return result.immutable();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class ErrorReporterExtensions {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using bts.api;
|
||||
using bts.api.synthetic;
|
||||
using bts.md;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
using mbts.spark;
|
||||
@@ -7,15 +8,10 @@ using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using SecurityKey = System.UInt32;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using bts.api.user;
|
||||
using bts.md;
|
||||
using mbts.ui.shell;
|
||||
|
||||
namespace bts {
|
||||
[Export(typeof(IInitializable))]
|
||||
@@ -25,17 +21,21 @@ namespace bts {
|
||||
readonly Lazy<SyntheticSecurityService> _ss;
|
||||
readonly Dictionary<SecurityKey, SecurityMD> _deleted_synthetics = new Dictionary<SecurityKey, SecurityMD>();
|
||||
|
||||
static uint get_naggle() => mbts.config.Configuration.get<md.RelayOptions>().naggle_ms;
|
||||
|
||||
[ImportingConstructor]
|
||||
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss, [Import(LoginContainer.Exports.UserID)]uint user_id) :
|
||||
base(get_naggle(), MDSourceOptions.DeferInit, user_id) {
|
||||
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss, UserTuple user)
|
||||
: base(mbts.config.Configuration.get<md.RelayOptions>().naggle_ms, MDSourceOptions.DeferInit, user) {
|
||||
_bus = bus;
|
||||
_token = token;
|
||||
_ss = ss;
|
||||
_ss = ss;
|
||||
}
|
||||
|
||||
override public void initialize() {
|
||||
public int priority => 55;
|
||||
|
||||
[Export]
|
||||
MDConsumer consumer => _consumer;
|
||||
IBus bus => _bus.Value;
|
||||
|
||||
public override void initialize() {
|
||||
_relay.set_token(_token.Value);
|
||||
_dispose.Add(
|
||||
_ss.Value.subscribei_metadata()
|
||||
@@ -60,16 +60,8 @@ namespace bts {
|
||||
// Post full dictionary of syntheics here
|
||||
bus.post(SparkTags.SyntheticstrategiesTag, t);
|
||||
}));
|
||||
_dispose.Add( this.on_securities.Subscribe( (md) => {
|
||||
bus.post(SparkTags.SecuritiesTag, md);
|
||||
}));
|
||||
_dispose.Add(on_securities.Subscribe(md => bus.post(SparkTags.SecuritiesTag, md)));
|
||||
Task.Run(run);
|
||||
}
|
||||
|
||||
[Export]
|
||||
MDConsumer consumer => _consumer;
|
||||
|
||||
public int priority => 55;
|
||||
IBus bus => _bus.Value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using System;
|
||||
using System.ComponentModel.Composition;
|
||||
using mbts.ui;
|
||||
using mbts;
|
||||
using Newtonsoft.Json;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.spark {
|
||||
[Export(typeof(IShellDaemon))]
|
||||
class OopsDaemon : IShellDaemon, IDisposable {
|
||||
[ImportingConstructor]
|
||||
public OopsDaemon(ErrorReporter e) {
|
||||
var pid = get_pid();
|
||||
if (pid.HasValue) {
|
||||
handle_unclean_shutdown(e, pid.Value);
|
||||
}
|
||||
save_pid();
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
clear_pid();
|
||||
}
|
||||
|
||||
void handle_unclean_shutdown(ErrorReporter e, int oldpid) {
|
||||
string extra = "";
|
||||
try {
|
||||
var conf = mbts.config.Configuration.get<bts.config.System>();
|
||||
extra = JsonConvert.SerializeObject(conf, Formatting.Indented);
|
||||
}
|
||||
catch { }
|
||||
Task.Run(() => e.report_silently(
|
||||
$"Host: {Environment.MachineName}",
|
||||
$"pid: {oldpid}\n{extra}",
|
||||
"Automated crash report")
|
||||
);
|
||||
}
|
||||
|
||||
const string _subkey = @"Software\BTS";
|
||||
private void save_pid() {
|
||||
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(_subkey)) {
|
||||
key.SetValue("pid", System.Diagnostics.Process.GetCurrentProcess().Id, Microsoft.Win32.RegistryValueKind.DWord);
|
||||
}
|
||||
}
|
||||
private void clear_pid() {
|
||||
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(_subkey)) {
|
||||
key.DeleteValue("pid",false);
|
||||
}
|
||||
}
|
||||
private int? get_pid() {
|
||||
using (var key = Microsoft.Win32.Registry.CurrentUser.CreateSubKey(_subkey)) {
|
||||
return key.GetValue("pid") as int?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -43,6 +43,11 @@ namespace bts.shell {
|
||||
void _show_order_window(Window window) {
|
||||
foreach (var kv in AlgoStudioModelUtils.get_dynamic_resources(_model.algo_model.ui_configuration))
|
||||
window.Resources[kv.Key] = kv.Value;
|
||||
// the dynamic algo 'SecurityKey' template has a ContractPickerPopup that expects the security map to be available as a DynamicResource.
|
||||
var sub = _bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => window.Resources[ResourceKeys.Securities] = sec_map);
|
||||
window.Closed += (s, e) => Disposable.dispose(ref sub);
|
||||
window.Show(-200, -160, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,17 +3,13 @@ using bts.metrics;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Net.NetworkInformation;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.perf {
|
||||
|
||||
[Export, Export(typeof(IShellDaemon))]
|
||||
class ShellLatencyDaemon : IShellDaemon, IDisposable {
|
||||
readonly ShellLatencyService _service;
|
||||
@@ -24,10 +20,10 @@ namespace bts.perf {
|
||||
Ping _pinger = new Ping();
|
||||
|
||||
[ImportingConstructor]
|
||||
public ShellLatencyDaemon(ShellLatencyService service, [Import(LoginContainer.Exports.UserName)] string user) {
|
||||
public ShellLatencyDaemon(ShellLatencyService service, UserTuple user) {
|
||||
_service = service;
|
||||
_version = Configuration.get<bts.config.System>().version;
|
||||
_username = user;
|
||||
_username = user.name;
|
||||
_hostname = Environment.MachineName;
|
||||
|
||||
_forever_ping_host(_pinger);
|
||||
@@ -105,4 +101,4 @@ namespace bts.perf {
|
||||
Disposable.dispose(ref _subs);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,6 @@ using mbts.co2;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
@@ -13,13 +12,11 @@ using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
using static mbts.ui.shell.ProtoBufSerializationHelper;
|
||||
|
||||
namespace bts.shell
|
||||
{
|
||||
namespace bts.shell {
|
||||
[Export(typeof(IShellModule))]
|
||||
class MarketShellModule : IShellModule
|
||||
{
|
||||
class MarketShellModule : IShellModule {
|
||||
[ImportingConstructor]
|
||||
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model, [Import(LoginContainer.Exports.UserName)]string user_name) {
|
||||
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model) {
|
||||
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub);
|
||||
_mvf = new MarketViewWindowFactory(this, bus, settings_hub, orders_model);
|
||||
}
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace bts.shell {
|
||||
public string strip_name;
|
||||
[ProtoMember(26)]
|
||||
public string last_price_from_spread;
|
||||
[ProtoMember(27)]
|
||||
[ProtoMember(28)]
|
||||
public string mic_code;
|
||||
}
|
||||
|
||||
|
||||
@@ -227,7 +227,7 @@ namespace mbts.ui.spark {
|
||||
}
|
||||
|
||||
DataGridColumn _column_factory(LegSpreaderParamsTuple l) {
|
||||
var col = new DataGridColumnInternal(l.leg.Security, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter) { Width = 75, };
|
||||
var col = new DataGridColumnInternal(l.leg.Security, ParameterCellTemplateConverter, ParameterCellEditingTemplateConverter);
|
||||
col.HeaderTemplate = (DataTemplate)FindResource(IsNew ? RemovableColumnHeaderTemplateKey : NonRemovableColumnHeaderTemplateKey);
|
||||
DataGridEx.SetRightBorder(col, new DataGridExVerticalLine(false, true, Colors.DarkGray, 1));
|
||||
return col;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using bts.api;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.synthetic;
|
||||
using bts.collections;
|
||||
using bts.spark;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
@@ -18,11 +20,36 @@ namespace mbts.spark {
|
||||
readonly uint _user_id;
|
||||
|
||||
[ImportingConstructor]
|
||||
public SpreaderModel(IBus bus, SyntheticSecurityService service, ErrorReporter error, [Import(LoginContainer.Exports.UserID)]uint user_id) {
|
||||
public SpreaderModel(IBus bus, SyntheticSecurityService service, AdminModel admin_model, ErrorReporter error, UserTuple user) {
|
||||
_bus = bus;
|
||||
_service = service;
|
||||
_error = error;
|
||||
_user_id = user_id;
|
||||
_user_id = user.id;
|
||||
// synthetic tuples
|
||||
var strategy_source = _bus.subscribe(SparkTags.SyntheticstrategiesTag)
|
||||
.Select(map => MappedDictionary.create<uint, ExecutionStrategyImmutable, (uint security_id, ExecutionStrategyImmutable strategy)>(map, (k, v) => (k, v)))
|
||||
.Table(tup => tup.security_id);
|
||||
|
||||
var synthetic_source = _bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.Select(map => new FilteredDictionary<uint, SecurityMD>(map, smd => smd.security.ContractCase == Security.ContractOneofCase.Synthetic))
|
||||
.Table(smd => smd.security.Id);
|
||||
|
||||
var synthetic_user_join = synthetic_source.LeftJoin(
|
||||
admin_model.all_user_tuples,
|
||||
SecurityMD.key_fn,
|
||||
UserTuple.key_fn,
|
||||
smd => smd.security.Synthetic.UserId,
|
||||
(smd, sid, ut, uid) => new SyntheticTuple(smd.security, ut.name, null),
|
||||
(smd, sid, uid) => new SyntheticTuple(smd.security, null, null),
|
||||
SyntheticTuple.key_fn);
|
||||
|
||||
synthetic_tuples = synthetic_user_join.LeftJoin(
|
||||
strategy_source,
|
||||
SyntheticTuple.key_fn,
|
||||
tup => tup.security_id,
|
||||
(synth_tup, id, strat_tup) => new SyntheticTuple(synth_tup.security, synth_tup.user_name, strat_tup.strategy),
|
||||
(synth_tup, id) => synth_tup,
|
||||
SyntheticTuple.key_fn);
|
||||
}
|
||||
|
||||
public IObservable<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>> strategy => _bus.subscribe(SparkTags.SyntheticstrategiesTag);
|
||||
@@ -36,6 +63,8 @@ namespace mbts.spark {
|
||||
.LeftJoin(strategies, (security, strategy) => (new ExecutionStrategyImmutable?(strategy), security), security => (new ExecutionStrategyImmutable?(), security))
|
||||
);
|
||||
|
||||
public IObservable<TableUpdate<SyntheticTuple, uint>> synthetic_tuples { get; }
|
||||
|
||||
public IObservable<ExecutionStrategyImmutable> security_strategy(uint security_key) =>
|
||||
strategy.TrySelect(s => {
|
||||
bool success = s.TryGetValue(security_key, out ExecutionStrategyImmutable result);
|
||||
|
||||
@@ -81,9 +81,9 @@ namespace bts.shell {
|
||||
#region QuoteParams
|
||||
public static QuoteParams QuoteParams => new QuoteParams {
|
||||
LeanQuantityRatio = 1,
|
||||
QuoteInsideMarketDistance = QuoteInsideMarketDistance,
|
||||
//QuoteInsideMarketDistance = QuoteInsideMarketDistance,
|
||||
QuoteMaxOrderMove = QuoteMaxOrderMove,
|
||||
QuoteMuteZone = QuoteMuteZone,
|
||||
//QuoteMuteZone = QuoteMuteZone,
|
||||
QuoteRepriceThrottle = QuoteRepriceThrottle,
|
||||
QuoteInsideMarketDistanceWithTravel = QuoteInsideMarketDistanceWithTravel,
|
||||
};
|
||||
@@ -113,6 +113,7 @@ namespace bts.shell {
|
||||
};
|
||||
public static QuoteInsideMarketDistanceWithTravel QuoteInsideMarketDistanceWithTravel => new QuoteInsideMarketDistanceWithTravel { Priority = RulePriority };
|
||||
public static QuoteInsideMarketDistance QuoteInsideMarketDistance => new QuoteInsideMarketDistance { Priority = RulePriority };
|
||||
public static QuoteInsideMarketDistanceThrottle QuoteInsideMarketDistanceThrottle => new QuoteInsideMarketDistanceThrottle();
|
||||
#endregion
|
||||
|
||||
#region PreHedge
|
||||
|
||||
@@ -7,8 +7,15 @@ namespace mbts.ui.spark {
|
||||
/// Interaction logic for SyntheticManagementView.xaml
|
||||
/// </summary>
|
||||
partial class SyntheticManagementView : UserControl {
|
||||
public enum Columns {
|
||||
Name,
|
||||
Description,
|
||||
n_legs,
|
||||
n_working_orders,
|
||||
SecurityKey,
|
||||
User
|
||||
}
|
||||
|
||||
public enum Columns { Name, Description, n_legs, n_working_orders, SecurityKey}
|
||||
public SyntheticManagementView() =>
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
@@ -27,7 +28,7 @@ namespace mbts.spark {
|
||||
readonly IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
|
||||
|
||||
public SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state, WindowSettingsManager<GridSettings> settings_manager)
|
||||
public SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state, IWindowSettingsManager<GridSettings> settings_manager)
|
||||
: base("Synthetic Management", bus, factory, SparkCommands.ShowSyntheticManagement.icon) {
|
||||
_orders_model = orders_model;
|
||||
_state = state;
|
||||
@@ -66,8 +67,8 @@ namespace mbts.spark {
|
||||
), SparkCommand.create("Clone", Icons.CloneSynthetic, SparkTags.ShowSyntheticCloneTag).complete(
|
||||
current_row_connectable_builder.create(observe_row_strategy),
|
||||
rs => rs.HasValue, select_row_strategy_tuple
|
||||
), SparkCommands.ShowLadder(SparkCommands.KeyGestures.ShowLadder).complete(
|
||||
vm_connectable_builder.create(vm => vm.current_security_md), md => md.HasValue, ConvertUtils.ValueOrDefault
|
||||
), SparkCommands.ShowLadderAutoLookup(SparkCommands.KeyGestures.ShowLadder).complete(
|
||||
vm_connectable_builder.create(vm => vm.current_security_observable.Select(s => s.HasValue ? ImmutableHashSet<uint>.Empty.Add(s.Value.Id) : null)), id => id != null
|
||||
), SparkCommands.ShowOrders.complete(() => {
|
||||
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
|
||||
ob_settings.filters = new bts.shell.orderbook.FilterStates {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using bts.api;
|
||||
using bts.settings;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
@@ -7,6 +8,7 @@ using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using static mbts.ui.controls.DIYGrid;
|
||||
@@ -33,13 +35,8 @@ namespace bts.shell {
|
||||
public async void show(SparkTags.Nothing nothing) =>
|
||||
_bus.post(SparkTags.ShowWindow, await load(new StartupData()));
|
||||
|
||||
IShellWindow _create(StartupData state) {
|
||||
var settings_manager = new WindowSettingsManager<GridSettings>(_settings_hub,
|
||||
state.grid,
|
||||
get: us => us.synthetic_management_grid,
|
||||
set: (us, gs) => us.with(provided_synthetic_management_gridsettings: gs));
|
||||
return new SyntheticManagementWindow(this, _bus, _orders_model, state, settings_manager);
|
||||
}
|
||||
IShellWindow _create(StartupData state) =>
|
||||
new SyntheticManagementWindow(this, _bus, _orders_model, state, _settings_hub.create_synth_settings_manager(state.grid));
|
||||
|
||||
[ProtoContract]
|
||||
public class StartupData {
|
||||
@@ -67,6 +64,7 @@ namespace bts.shell {
|
||||
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_criterion {
|
||||
get {
|
||||
@@ -81,6 +79,7 @@ namespace bts.shell {
|
||||
yield return n_legs;
|
||||
yield return n_working_orders;
|
||||
yield return security_key;
|
||||
yield return user;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +100,15 @@ namespace bts.shell {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using bts.shell;
|
||||
using mbts.ui.spark;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class SyntheticEditorVM {
|
||||
class QuoteInsideMarketDistanceEnabledModel : RowModelBase<bool> {
|
||||
readonly QuoteInsideMarketDistanceTowardModel _toward;
|
||||
|
||||
public QuoteInsideMarketDistanceEnabledModel(QuoteInsideMarketDistanceTowardModel toward) =>
|
||||
_toward = toward;
|
||||
|
||||
public override string display_name { get; } = string.Empty;
|
||||
public override string help_text { get; } = string.Empty;
|
||||
public override string man_page { get; } = string.Empty;
|
||||
public override ParameterSection[] section_path { get; } = new[] { Sections.Quote, Subsections.InsideMarketDistance };
|
||||
|
||||
public override bool get_value(LegSpreaderParamsTuple leg_tup) =>
|
||||
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle != null;
|
||||
|
||||
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) {
|
||||
if (value) {
|
||||
if (leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle == null) {
|
||||
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle = SyntheticDataHelper.Defaults.QuoteInsideMarketDistanceThrottle;
|
||||
_toward.apply_value(leg_tup);
|
||||
}
|
||||
} else {
|
||||
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle = null;
|
||||
}
|
||||
_toward.set_enabled(leg_tup, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using bts.spark;
|
||||
using mbts.ui.spark;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class SyntheticEditorVM {
|
||||
class QuoteInsideMarketDistanceTowardModel : RowModelBase<uint> {
|
||||
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
|
||||
|
||||
public override string display_name { get; } = "Toward Distance";
|
||||
public override string help_text { get; } = string.Empty;
|
||||
public override string man_page { get; } = string.Empty;
|
||||
public override ParameterSection[] section_path { get; } = new[] { Sections.Quote, Subsections.InsideMarketDistance };
|
||||
|
||||
public void apply_value(LegSpreaderParamsTuple leg_tup) =>
|
||||
_get_inside_market_distance(leg_tup).OnTowardDistance = _value_from_store(leg_tup);
|
||||
|
||||
public override uint get_value(LegSpreaderParamsTuple leg_tup) =>
|
||||
_get_inside_market_distance(leg_tup) is QuoteInsideMarketDistanceThrottle inside_market_distance ?
|
||||
inside_market_distance.OnTowardDistance :
|
||||
_value_from_store(leg_tup);
|
||||
|
||||
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
|
||||
_value_store[leg_tup.SecurityKey] = value;
|
||||
if (_get_inside_market_distance(leg_tup) is QuoteInsideMarketDistanceThrottle inside_market_distance)
|
||||
inside_market_distance.OnTowardDistance = value;
|
||||
}
|
||||
|
||||
protected override bool _get_enabled(LegSpreaderParamsTuple leg_tup) =>
|
||||
_get_inside_market_distance(leg_tup) != null;
|
||||
|
||||
QuoteInsideMarketDistanceThrottle _get_inside_market_distance(LegSpreaderParamsTuple leg_tup) =>
|
||||
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle;
|
||||
|
||||
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
|
||||
_value_store.GetValueOrDefault(leg_tup.SecurityKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,11 @@ namespace mbts.spark {
|
||||
"dictates if this logic applies to the quoting market or to the synthetic spread " +
|
||||
"market");
|
||||
|
||||
public static ParameterSection InsideMarketDistance =>
|
||||
new ParameterSection("Inside Market Distance", true,
|
||||
"Reduces re-quoting by throttling quotes that are moving toward the inside market " +
|
||||
"but are more than 'Toward Distance' away from the inside market");
|
||||
|
||||
public static ParameterSection InsideMarketThrottleThresholds =>
|
||||
new ParameterSection("Inside Market Throttle Thresholds", true,
|
||||
"Reduces re-quoting for quotes that are moving toward the inside market. " +
|
||||
|
||||
@@ -190,6 +190,8 @@ namespace mbts.spark {
|
||||
var quote_inside_market_throttle_ticks = new QuoteInsideMarketThrottleTicksModel();
|
||||
var quote_inside_market_throttle_min_move = new QuoteInsideMarketThrottleMinMoveModel();
|
||||
|
||||
var quote_inside_market_distance_toward = new QuoteInsideMarketDistanceTowardModel();
|
||||
|
||||
var pre_hedge_payless_threshold = new PreHedgePaylessThresholdModel();
|
||||
var pre_hedge_payless_ticks = new PreHedgePaylessTicksModel();
|
||||
|
||||
@@ -227,6 +229,8 @@ namespace mbts.spark {
|
||||
//quote_inside_market_throttle_ticks,
|
||||
//quote_inside_market_throttle_min_move,
|
||||
//new MaxTicksInsideMarketModel(),
|
||||
new QuoteInsideMarketDistanceEnabledModel(quote_inside_market_distance_toward),
|
||||
quote_inside_market_distance_toward,
|
||||
new PreHedgePaylessEnabledModel(pre_hedge_payless_threshold, pre_hedge_payless_ticks),
|
||||
pre_hedge_payless_threshold,
|
||||
pre_hedge_payless_ticks,
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
using bts.api;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.collections;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.persist;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
@@ -17,43 +20,45 @@ using static mbts.ui.controls.DIYGrid;
|
||||
using static mbts.ui.spark.SyntheticManagementView;
|
||||
|
||||
namespace mbts.spark {
|
||||
class SyntheticManagementDialogVM : SecurityMDGrid<SyntheticManagementDialogVM.Row, (ExecutionStrategyImmutable? strategy, SecurityMD md)> {
|
||||
class SyntheticManagementDialogVM : SecurityGrid<SyntheticManagementDialogVM.Row, SyntheticTuple> {
|
||||
readonly IBus _bus;
|
||||
readonly OrdersModel _orders_model;
|
||||
IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
|
||||
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)
|
||||
: base(orders_model.spreader.strategy_tuple.Select(map => map.Values), sorting) {
|
||||
: base(orders_model.spreader.synthetic_tuples.Select(t => t.state.Values), sorting) {
|
||||
_bus = bus;
|
||||
_orders_model = orders_model;
|
||||
_settings_manager = settings_manager;
|
||||
// init filters
|
||||
user_filter = DiscreteValuesFilterVM.CreateTableUpdate<Row, UserTuple, uint>(r => r.user_tuple, UserTuple.key_fn, out var user_filter_items_observer, out var user_filter_items_view, nameof(Row.user_tuple));
|
||||
user_filter_items_view.CustomSort = Comparer<string>.Default.wrap<string, UserTuple>(ut => ut.name).to_non_generic();
|
||||
if (filters != null) {
|
||||
create_time_filter.load_state(filters.create_time);
|
||||
name_filter.load_state(filters.name);
|
||||
description_filter.load_state(filters.description);
|
||||
n_legs_filter.load_state(filters.n_legs);
|
||||
n_working_orders_filter.load_state(filters.n_working_orders);
|
||||
security_key_filter.load_state(filters.security_key);
|
||||
security_key_filter.load_state(filters.security_key);
|
||||
user_filter.load_state(filters.user);
|
||||
}
|
||||
_settings_manager = settings_manager;
|
||||
// columns
|
||||
add_col(Columns.Name, DefaultColumnHeaders.Name, r => r.name, nameof(Row.name), name_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.Security);
|
||||
add_col(Columns.Description, DefaultColumnHeaders.Description, r => r.description, nameof(Row.description), description_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.Description);
|
||||
add_col(Columns.n_legs, DefaultColumnHeaders.N_Legs, r => r.n_legs.ToString(), nameof(Row.n_legs), n_legs_filter, DataGridFilterKnownTemplates.IntMinMax, DefaultColumnWidths.N_Legs);
|
||||
columns.Add(new HyperlinkElementColumn<Row, Columns>(
|
||||
Columns.n_working_orders,
|
||||
new Binding(nameof(Row.n_working_orders)),
|
||||
Columns.n_working_orders,
|
||||
new Binding(nameof(Row.n_working_orders)),
|
||||
new Binding(nameof(Row.show_order_book)),
|
||||
n_working_orders_filter,
|
||||
DataGridFilterKnownTemplates.UIntMinMax) {
|
||||
id = col_id(Columns.n_working_orders),
|
||||
Header = DefaultColumnHeaders.N_Working_Orders,
|
||||
Width = DefaultColumnWidths.N_Working_Orders
|
||||
}
|
||||
);
|
||||
});
|
||||
add_col(Columns.SecurityKey, DefaultColumnHeaders.SecurityKey, r => r.security_key.ToString(), nameof(Row.security_key), security_key_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.SecurityKey);
|
||||
|
||||
add_col(Columns.User, DefaultColumnHeaders.User, r => r.user_name, nameof(Row.user_name), user_filter, DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.User);
|
||||
//
|
||||
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) {
|
||||
@@ -63,42 +68,39 @@ namespace mbts.spark {
|
||||
Width = width
|
||||
});
|
||||
//
|
||||
grid_settings_manager = new SimpleGridSettingsManager<Row>(columns, new Dictionary<string, GridColumnSettingsFeatureFlags> { }, row_vms);
|
||||
grid_settings_manager = new SimpleGridSettingsManager<Row>(columns, DictionaryHelper.empty<string, GridColumnSettingsFeatureFlags>(), row_vms);
|
||||
// this will initialize the immutable properties that correspond to the col_settings_flags above (e.g. Bid/Ask font_color_override, etc.)
|
||||
grid_settings_manager.load_from(settings_manager.current_settings());
|
||||
|
||||
_resources.AddRange(
|
||||
DataGridFilterHelper.hook_filters(rows_view, create_time_filter, name_filter, description_filter, n_legs_filter, n_working_orders_filter, security_key_filter),
|
||||
orders_model.admin_model.all_user_tuples.OnDispatcher().Subscribe(user_filter_items_observer),
|
||||
DataGridFilterHelper.hook_all<SyntheticManagementDialogVM, Row>(this, rows_view),
|
||||
settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager)
|
||||
);
|
||||
}
|
||||
|
||||
protected override Row row_factory((ExecutionStrategyImmutable? strategy, SecurityMD md) row_model) =>
|
||||
new Row(_bus, row_model.md, row_model.strategy, _orders_model);
|
||||
|
||||
protected override void on_row_removed(Row row) => row.Dispose();
|
||||
|
||||
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
|
||||
public ObservableCollection<ColumnBase> columns { get; } = new ObservableCollection<ColumnBase>();
|
||||
public SimpleGridSettingsManager<Row> grid_settings_manager { get; }
|
||||
|
||||
public GridSettings get_grid_save_state() {
|
||||
if (_settings_manager.instance_settings == null)
|
||||
return null;
|
||||
else
|
||||
return grid_settings_manager.create();
|
||||
}
|
||||
|
||||
public StartupData create() =>
|
||||
new StartupData(rows_view.get_sorting_criteria(columns), _get_filter_states(), get_grid_save_state());
|
||||
|
||||
|
||||
#region Filters
|
||||
public IDataGridColumnPersistentFilter<Row, TimeFilterTuple> create_time_filter { get; } = new TimeFilterVM<Row>(r => r.create_time, nameof(Row.create_time));
|
||||
public IDataGridColumnPersistentFilter<Row, string> name_filter { get; } = new RegexFilterVM<Row>(r => r.name, nameof(Row.name));
|
||||
public IDataGridColumnPersistentFilter<Row, string> description_filter { get; } = new RegexFilterVM<Row>(r => r.description, nameof(Row.description));
|
||||
public IDataGridColumnPersistentFilter<Row, IntMinMaxState> n_legs_filter { get; } = new IntMinMax<Row>(r => r.n_legs, nameof(Row.n_legs));
|
||||
public IDataGridColumnPersistentFilter<Row, UIntMinMaxState> n_working_orders_filter { get; } = new UIntMinMax<Row>(r => r.n_working_orders, nameof(Row.n_legs));
|
||||
public IDataGridColumnPersistentFilter<Row, string> security_key_filter { get; } = new RegexFilterVM<Row>(r => r.security_key.ToString(), nameof(Row.security_key));
|
||||
public IDataGridColumnPersistentFilter<Row, uint[]> user_filter { get; }
|
||||
|
||||
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());
|
||||
|
||||
protected override Row row_factory(SyntheticTuple row_model) =>
|
||||
new Row(_bus, row_model, _orders_model);
|
||||
|
||||
protected override void on_row_removed(Row row) =>
|
||||
row.Dispose();
|
||||
|
||||
FilterStates _get_filter_states() =>
|
||||
new FilterStates {
|
||||
@@ -107,19 +109,19 @@ namespace mbts.spark {
|
||||
name = name_filter.get_state(),
|
||||
n_legs = n_legs_filter.get_state(),
|
||||
n_working_orders = n_working_orders_filter.get_state(),
|
||||
security_key = security_key_filter.get_state()
|
||||
security_key = security_key_filter.get_state(),
|
||||
user = user_filter.get_state()
|
||||
};
|
||||
#endregion
|
||||
|
||||
public class Row : SecurityMDRow, IEquatable<Row>, IRowVM, IActionableRowVM {
|
||||
public readonly ExecutionStrategyImmutable? strategy;
|
||||
public class Row : SecurityRow, IEquatable<Row>, IRowVM, IActionableRowVM {
|
||||
public readonly SyntheticTuple synthetic_tuple;
|
||||
|
||||
readonly IBus _bus;
|
||||
readonly Property<uint> _n_working_orders;
|
||||
|
||||
public Row(IBus bus, SecurityMD security_md, ExecutionStrategyImmutable? strategy, OrdersModel orders_model) : base(security_md) {
|
||||
public Row(IBus bus, SyntheticTuple synthetic_tuple, OrdersModel orders_model) : base(synthetic_tuple.security) {
|
||||
_bus = bus;
|
||||
this.strategy = strategy;
|
||||
this.synthetic_tuple = synthetic_tuple;
|
||||
_n_working_orders = backing(nameof(n_working_orders),
|
||||
orders_model.on_security_orders(security_key)
|
||||
.Select(t => (uint)(t.state
|
||||
@@ -133,25 +135,23 @@ namespace mbts.spark {
|
||||
show_order_book = new DelegateCommand(_show_order_book);
|
||||
}
|
||||
|
||||
public event Action invalidated;
|
||||
|
||||
public DateTime create_time => DateTime.MinValue; //TODO
|
||||
public string name { get; }
|
||||
public string user_name => synthetic_tuple.user_name;
|
||||
public UserTuple user_tuple => new UserTuple(synthetic_tuple.security.Synthetic.UserId, user_name);
|
||||
public int n_legs { get; }
|
||||
public string description { get; }
|
||||
public ExecutionStrategyImmutable? strategy => synthetic_tuple.strategy;
|
||||
public bool strategy_present => strategy.HasValue;
|
||||
public uint n_working_orders => _n_working_orders.Value;
|
||||
public ICommand show_ladder { get; }
|
||||
public ICommand show_order_book { get; }
|
||||
public ICommand default_command => show_ladder;
|
||||
|
||||
|
||||
public ObservableCollection<RowAnimation> row_animations => new ObservableCollection<RowAnimation>();
|
||||
|
||||
public IReadOnlyList<IGridCellStore> cell_stores => new IGridCellStore[0];
|
||||
|
||||
|
||||
public event Action invalidated;
|
||||
|
||||
public void raise_invalidated() => invalidated?.Invoke();
|
||||
public void raise_invalidated() =>
|
||||
invalidated?.Invoke();
|
||||
|
||||
public bool Equals(Row other) =>
|
||||
other != null &&
|
||||
@@ -163,10 +163,8 @@ namespace mbts.spark {
|
||||
public override int GetHashCode() =>
|
||||
security_key.GetHashCode();
|
||||
|
||||
|
||||
|
||||
void _show_ladder() =>
|
||||
_bus.post(SparkTags.ShowLadderTag, md);
|
||||
_bus.post(SparkTags.ShowLadderAutoLookupTag, ImmutableHashSet<uint>.Empty.Add(security.Id));
|
||||
|
||||
void _show_order_book() {
|
||||
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
|
||||
@@ -177,4 +175,4 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,10 @@ namespace mbts.spark {
|
||||
_vm = new AlgoStudioVM(_model);
|
||||
_vm.load_from(_state);
|
||||
_view = new AlgoStudioView { DataContext = _vm };
|
||||
_resources = new CompositeDisposable();
|
||||
_resources = new CompositeDisposable(
|
||||
_bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => _view.Resources[ResourceKeys.Securities] = sec_map));
|
||||
// add definition
|
||||
_commands.Add(new SparkCommandCommand("Add Algo", Icons.AddBoxes, _vm.add_definition_cmd));
|
||||
// clone definition
|
||||
|
||||
@@ -27,7 +27,7 @@ namespace mbts.spark {
|
||||
_error_reporter = error_reporter;
|
||||
definitions =
|
||||
service.subscribei_algo_definitions()
|
||||
.Filter(r => r.Metadata.IsActive && r.Metadata.CreatorUserId == _admin_model.user.id)
|
||||
.Filter(r => r.Metadata.IsActive && _admin_model.user.can_access_user_data(r.Metadata.CreatorUserId))
|
||||
.SelectKeyValue(_definition_data, d => d.key)
|
||||
.Table()
|
||||
.Cache();
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace mbts.spark {
|
||||
string _template_key;
|
||||
ICommand _set_value_cmd;
|
||||
|
||||
public AlgoParamRowVM(IAlgoVM algo, IAlgoParamVM param, AlgoParamRowVM parent)
|
||||
: this(algo, param, parent, (param as ITemplateSelectorKey)?.key) { }
|
||||
|
||||
public AlgoParamRowVM(IAlgoVM algo, IAlgoParamVM param, AlgoParamRowVM parent, string template_key = null) {
|
||||
this.param = param;
|
||||
this.algo = algo;
|
||||
|
||||
+40
@@ -247,6 +247,31 @@
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static u:TemplateKeys.SecurityKey}">
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<!--Note: Ideally, this 'securities BindingProxy' would be a static resource, but it only seems to detect the dynamic securities when it is in the visual tree.-->
|
||||
<!--<Grid.Resources>
|
||||
<u:BindingProxy x:Key="SecuritiesBindingProxy" x:Shared="False" Value="{DynamicResource {x:Static u:ResourceKeys.Securities}}"/>
|
||||
</Grid.Resources>-->
|
||||
<u:BindingProxy x:Name="SecuritiesBindingProxy" Value="{DynamicResource {x:Static u:ResourceKeys.Securities}}"/>
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding>
|
||||
<MultiBinding.Converter>
|
||||
<cv:MultiValueConverterPipe>
|
||||
<cv:DictionaryLookupConverter/>
|
||||
<x:Static MemberType="cv:ValueConverters" Member="SecuritySelectorText"/>
|
||||
</cv:MultiValueConverterPipe>
|
||||
</MultiBinding.Converter>
|
||||
<Binding Path="Value" ElementName="SecuritiesBindingProxy"/>
|
||||
<Binding Path="param.value"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</usp:AlgoParamTemplateSelector.KeyedTemplates>
|
||||
<!--Bool-->
|
||||
<usp:AlgoParamTemplateSelector.BoolTemplate>
|
||||
@@ -320,6 +345,21 @@
|
||||
</usp:PriceEntryControl>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static u:TemplateKeys.SecurityKey}">
|
||||
<DataTemplate>
|
||||
<c:ContractPickerPopup
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding SelectedSecurity, RelativeSource={RelativeSource Self}, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
|
||||
IsEditMode="False"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource {x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurityKey="{Binding param.value}"
|
||||
ShowDialogSkirt="True"
|
||||
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityKeyHandler}"
|
||||
/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</usp:AlgoParamTemplateSelector.KeyedTemplates>
|
||||
<!--Bool-->
|
||||
<usp:AlgoParamTemplateSelector.BoolTemplate>
|
||||
|
||||
@@ -118,8 +118,8 @@ namespace mbts.spark {
|
||||
add_col_expl(Columns.Side, new SideColumn<Row, Columns>(Columns.Side, r => r.side, side_filter) { id = col_id(Columns.Side) }, side_filter);
|
||||
// all others (easily identified by the enum 'Columns.Foo')
|
||||
add_col(Columns.FillTime, DefaultColumnHeaders.FillTime, r => r.fill_time.ToString("M/d h:mm:ss tt"), new TimeFilterVM<FillTuple>(r => r.fill.Timestamp.ToDateTimeOffset().LocalDateTime), DataGridFilterKnownTemplates.TimeFilter, DefaultColumnWidths.Time);
|
||||
add_col(Columns.User, DefaultColumnHeaders.User, r => r.user, dv_filter_rx(_model.factory.subscribe_users().Select(d => d.Values), r => r.order?.user() ?? string.Empty), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.User);
|
||||
add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, dv_filter_rx(_model.factory.subscribe_accounts().Select(d => d.Values), r => r.account), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Account);
|
||||
add_col(Columns.User, DefaultColumnHeaders.User, r => r.user, dv_filter_rx(_model.factory.subscribe_users().Select(d => d.Values.Ordered()), r => r.order?.user() ?? string.Empty), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.User);
|
||||
add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, dv_filter_rx(_model.factory.subscribe_accounts().Select(d => d.Values.Ordered()), r => r.account), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Account);
|
||||
add_col(Columns.Exchange, DefaultColumnHeaders.Exchange, r => r.exchange.ToString(), dv_filter(EnumHelper.get_values<Venue>().ToArray(), r => r.security.exchange_or_default()), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Exchange);
|
||||
add_col(Columns.TimeInForce, DefaultColumnHeaders.TimeInForce, r => r.time_in_force.ToString(), dv_filter(EnumHelper.get_values<TimeInForce>().ToArray(), r => r.order?.time_in_force() ?? TimeInForce.Undefined), DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.TimeInForce);
|
||||
add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.size.ToString(), new DoubleMinMax<FillTuple>(r => r.fill.LastShares), DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size);
|
||||
@@ -147,6 +147,7 @@ namespace mbts.spark {
|
||||
});
|
||||
_dispose = new CompositeDisposable(
|
||||
ObservableAutoWrapper.connect(col_tups, columns, ct => ct.col),
|
||||
ObservableAutoWrapper.connect_item_hooks(col_tups, ct => ct.filter as IDisposable),
|
||||
RXCollection.connect(rows, fills_buffer.Filter(filter_src, FillTuple.key_fn), comp_src, ft => new Row(ft, model), r => r.fill_tuple),
|
||||
fills_source.OnDispatcher().Subscribe(t => {
|
||||
last_unfiltered_fills = t;
|
||||
@@ -284,7 +285,6 @@ namespace mbts.spark {
|
||||
public DateTime fill_time => _fill.Timestamp.to_local_time();
|
||||
public TimeInForce time_in_force => _order.time_in_force();
|
||||
public OrderType order_type => _order.order_type();
|
||||
//public OrderType root_algo_type => _or
|
||||
public string display_name => OrderUtils.display_name(_order, fill_tuple.algo);
|
||||
public OrderMetadata order_metadata => _order_metadata ?? (_order_metadata = _model.factory.get_metadata(_order.Def.CustomDef.CustomCase));
|
||||
public Brush badge_fill => _badge_fill ?? (_badge_fill = OrderUtils.badge_fill(fill_tuple.algo_meta, order_metadata));
|
||||
@@ -303,7 +303,7 @@ namespace mbts.spark {
|
||||
public OrderType root_algo_order_type => _root_algo.HasValue ? _root_algo.Value.value.CustomCase : OrderType.None;
|
||||
public OrderMetadata root_algo_order_metadata => _model.factory.get_metadata(root_algo_order_type);
|
||||
public Color root_algo_badge_color => _root_algo_badge_color ?? (_root_algo_badge_color = OrderUtils.badge_color(_root_algo_meta, root_algo_order_metadata)).Value;
|
||||
public string root_algo_display_name => _root_algo?.name ?? root_algo_order_type.display_name();
|
||||
public string root_algo_display_name => fill_tuple.root_order_security?.display_name() ?? OrderType.None.display_name();
|
||||
|
||||
public bool Equals(Row other) => other != null && other._fill.Equals(_fill) && other._order.Equals(_order);
|
||||
public override bool Equals(object obj) => Equals(obj as Row);
|
||||
|
||||
@@ -75,7 +75,7 @@ namespace bts.shell.fills {
|
||||
[DIYGridColumnID(nameof(Columns.Symbol))]
|
||||
public string[] symbol { get; set; }
|
||||
|
||||
[ProtoMember(16)]
|
||||
[ProtoMember(17)]
|
||||
[DIYGridColumnID(nameof(Columns.FillStatus))]
|
||||
public Fill.Types.FillStatus[] fill_status { get; set; }
|
||||
}
|
||||
|
||||
@@ -23,6 +23,10 @@ namespace mbts.spark {
|
||||
// TODO: somehow allow windows that are using the default settings to still have individual column widths.
|
||||
[ProtoContract]
|
||||
class LadderColumnSettings : IEquatable<LadderColumnSettings> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(LadderColumnSettings).GetHashCode();
|
||||
|
||||
public static LadderColumnSettings Default => new LadderColumnSettings {
|
||||
show = true,
|
||||
width = 50,
|
||||
@@ -74,14 +78,7 @@ namespace mbts.spark {
|
||||
other.show == show;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as LadderColumnSettings);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
width.GetHashCode() +
|
||||
background.GetHashCode() +
|
||||
foreground.GetHashCode() +
|
||||
font_family.GetHashCode() +
|
||||
font_size.GetHashCode() +
|
||||
font_weight.GetHashCode();
|
||||
public override int GetHashCode() => _type_hash;
|
||||
|
||||
public static bool operator ==(LadderColumnSettings x, LadderColumnSettings y) => x.safe_equals(y);
|
||||
public static bool operator !=(LadderColumnSettings x, LadderColumnSettings y) => !(x == y);
|
||||
@@ -89,6 +86,9 @@ namespace mbts.spark {
|
||||
|
||||
[ProtoContract]
|
||||
class HistogramBarAnimation : IEquatable<HistogramBarAnimation> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(HistogramBarAnimation).GetHashCode();
|
||||
[ProtoMember(1)]
|
||||
public ColorShowProto bars { get; set; }
|
||||
[ProtoMember(2)]
|
||||
@@ -102,14 +102,14 @@ namespace mbts.spark {
|
||||
show_numbers == other.show_numbers;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as HistogramBarAnimation);
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
bars.GetHashCode(),
|
||||
show_numbers.GetHashCode());
|
||||
public override int GetHashCode() => _type_hash;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class VolumeColumnSettings : IEquatable<VolumeColumnSettings> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(VolumeColumnSettings).GetHashCode();
|
||||
public static VolumeColumnSettings Default {
|
||||
get {
|
||||
var gen = LadderColumnSettings.Default;
|
||||
@@ -149,19 +149,14 @@ namespace mbts.spark {
|
||||
show_ltq.Equals(other.show_ltq);
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as VolumeColumnSettings);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
general.GetHashCode(),
|
||||
buy.GetHashCode(),
|
||||
sell.GetHashCode(),
|
||||
passive.GetHashCode(),
|
||||
total.GetHashCode(),
|
||||
show_ltq.GetHashCode());
|
||||
public override int GetHashCode() => _type_hash;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class LadderMarketBreakLine : IEquatable<LadderMarketBreakLine> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(LadderMarketBreakLine).GetHashCode();
|
||||
[ProtoMember(1)]
|
||||
public bool show { get; set; }
|
||||
[ProtoMember(2)]
|
||||
@@ -184,8 +179,7 @@ namespace mbts.spark {
|
||||
Equals(obj as LadderMarketBreakLine);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
show.GetHashCode() +
|
||||
(int)color;
|
||||
_type_hash;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
@@ -255,6 +249,11 @@ namespace mbts.spark {
|
||||
const double _default_order_entry_panel_width = 110.0;
|
||||
const double _default_zoom_factor = 1.0;
|
||||
const bool _default_show_warn_on_cross = true;
|
||||
public static readonly IEqualityComparer<LadderSettings> profile_equality_comparer =
|
||||
EqualityComparer.Create<LadderSettings>(profile_equals);
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(LadderSettings).GetHashCode();
|
||||
|
||||
// Default proto constructor
|
||||
// Added this so that when clients upgrade to a version with new settings,
|
||||
@@ -497,6 +496,9 @@ namespace mbts.spark {
|
||||
[ProtoMember(55)]
|
||||
public LadderMiniOrderBookSettings mini_ob { get; set; }
|
||||
|
||||
[ProtoMember(57)]
|
||||
public double left_panel_scroll_position { get; set; }
|
||||
|
||||
public void sanitize() =>
|
||||
sanitize(this);
|
||||
|
||||
@@ -533,112 +535,81 @@ namespace mbts.spark {
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not the specified LadderSettings are 'profile equivalent.'
|
||||
/// This means that 2 ladders, each with a different one of the specified settings, could share the same profile.
|
||||
/// </summary>
|
||||
///
|
||||
/// Note: the impetus for this version of equality is to avoid disconnecting a ladder from its profile due to 'trivial' settings changes.
|
||||
/// Consider the '<see cref="default_algo"/>' property:
|
||||
/// If a ladder is using a settings profile with Limit as the default algo, and the user decides to send a 'Market' order,
|
||||
/// the ladder would suddenly be disconnected from the profile, because it's settings state would no longer be equal to the profile's settings,
|
||||
/// since they changed the selected algo type to 'Market.'
|
||||
/// To avoid such travesties, a separate equals logic is needed: this is called 'profile-equivalent' and is implemented in <see cref="profile_equals)"/>.
|
||||
/// Devs: be sure to account for properties that are excluded in the 'profile-equals' logic in the [regular]'equals' logic.
|
||||
public static bool profile_equals(LadderSettings a, LadderSettings b) =>
|
||||
a == null ? b == null :
|
||||
b != null &&
|
||||
a.row_height == b.row_height &&
|
||||
a.grid_line_color == b.grid_line_color &&
|
||||
a.orders_column.safe_equals(b.orders_column) &&
|
||||
a.bid_column.safe_equals(b.bid_column) &&
|
||||
a.ask_column.safe_equals(b.ask_column) &&
|
||||
a.price_column.safe_equals(b.price_column) &&
|
||||
a.last_column.safe_equals(b.last_column) &&
|
||||
a.piq_column.safe_equals(b.piq_column) &&
|
||||
a.calculator_buttons.safe_equals(b.calculator_buttons, Enumerable.SequenceEqual) &&
|
||||
a.scroll_bar_visible == b.scroll_bar_visible &&
|
||||
a.default_account == b.default_account &&
|
||||
a.default_time_in_force == b.default_time_in_force &&
|
||||
a.size == b.size &&
|
||||
a.animate_recenter == b.animate_recenter &&
|
||||
a.badge_style == b.badge_style &&
|
||||
a.auto_clear == b.auto_clear &&
|
||||
a.bid_break_line.safe_equals(b.bid_break_line) &&
|
||||
a.ask_break_line.safe_equals(b.ask_break_line) &&
|
||||
a.mid_break_line.safe_equals(b.mid_break_line) &&
|
||||
a.alternate_order_size == b.alternate_order_size &&
|
||||
a.session_low_line.safe_equals(b.session_low_line) &&
|
||||
a.session_high_line.safe_equals(b.session_high_line) &&
|
||||
a.session_range_fill.safe_equals(b.session_range_fill) &&
|
||||
a.volume_column.safe_equals(b.volume_column) &&
|
||||
a.size_display.safe_equals(b.size_display) &&
|
||||
a.day_visible == b.day_visible &&
|
||||
a.high_visible == b.high_visible &&
|
||||
a.low_visible == b.low_visible &&
|
||||
a.open_visible == b.open_visible &&
|
||||
a.settle_visible == b.settle_visible &&
|
||||
a.volume_visible == b.volume_visible &&
|
||||
a.account_visible == b.account_visible &&
|
||||
a.tif_visible == b.tif_visible &&
|
||||
a.order_type_visible == b.order_type_visible &&
|
||||
a.account_position_visible == b.account_position_visible &&
|
||||
a.day_position_visible == b.day_position_visible &&
|
||||
a.raw_day_position_visible == b.raw_day_position_visible &&
|
||||
a.size_presets_visible == b.size_presets_visible &&
|
||||
a.cancel_buys_visible == b.cancel_buys_visible &&
|
||||
a.cancel_sells_visible == b.cancel_sells_visible &&
|
||||
a.cancel_all_visible == b.cancel_all_visible &&
|
||||
a.auto_clear_visible == b.auto_clear_visible &&
|
||||
a.clear_visible == b.clear_visible &&
|
||||
a.order_entry_panel_initial_width == b.order_entry_panel_initial_width &&
|
||||
a.zoom_slider_visible == b.zoom_slider_visible &&
|
||||
a.zoom_factor == b.zoom_factor &&
|
||||
a.use_defaults == b.use_defaults &&
|
||||
a.favorite_algos == b.favorite_algos &&
|
||||
a.net_change == b.net_change &&
|
||||
a.show_warn_on_cross == b.show_warn_on_cross &&
|
||||
a.mini_ob == b.mini_ob;
|
||||
|
||||
public bool Equals(LadderSettings other) =>
|
||||
other != null &&
|
||||
row_height == other.row_height &&
|
||||
grid_line_color == other.grid_line_color &&
|
||||
orders_column.safe_equals(other.orders_column) &&
|
||||
bid_column.safe_equals(other.bid_column) &&
|
||||
ask_column.safe_equals(other.ask_column) &&
|
||||
price_column.safe_equals(other.price_column) &&
|
||||
last_column.safe_equals(other.last_column) &&
|
||||
piq_column.safe_equals(other.piq_column) &&
|
||||
calculator_buttons.safe_equals(other.calculator_buttons, (a, b) => a.SequenceEqual(b)) &&
|
||||
scroll_bar_visible == other.scroll_bar_visible &&
|
||||
default_account == other.default_account &&
|
||||
profile_equals(this, other) &&
|
||||
default_algo == other.default_algo &&
|
||||
default_time_in_force == other.default_time_in_force &&
|
||||
size == other.size &&
|
||||
animate_recenter == other.animate_recenter &&
|
||||
badge_style == other.badge_style &&
|
||||
auto_clear == other.auto_clear &&
|
||||
bid_break_line.safe_equals(other.bid_break_line) &&
|
||||
ask_break_line.safe_equals(other.ask_break_line) &&
|
||||
mid_break_line.safe_equals(other.mid_break_line) &&
|
||||
alternate_order_size == other.alternate_order_size &&
|
||||
session_low_line.safe_equals(other.session_low_line) &&
|
||||
session_high_line.safe_equals(other.session_high_line) &&
|
||||
session_range_fill.safe_equals(other.session_range_fill) &&
|
||||
volume_column.safe_equals(other.volume_column) &&
|
||||
size_display.safe_equals(other.size_display) &&
|
||||
day_visible == other.day_visible &&
|
||||
high_visible == other.high_visible &&
|
||||
low_visible == other.low_visible &&
|
||||
open_visible == other.open_visible &&
|
||||
settle_visible == other.settle_visible &&
|
||||
volume_visible == other.volume_visible &&
|
||||
account_visible == other.account_visible &&
|
||||
tif_visible == other.tif_visible &&
|
||||
order_type_visible == other.order_type_visible &&
|
||||
account_position_visible == other.account_position_visible &&
|
||||
day_position_visible == other.day_position_visible &&
|
||||
raw_day_position_visible == other.raw_day_position_visible &&
|
||||
size_presets_visible == other.size_presets_visible &&
|
||||
cancel_buys_visible == other.cancel_buys_visible &&
|
||||
cancel_sells_visible == other.cancel_sells_visible &&
|
||||
cancel_all_visible == other.cancel_all_visible &&
|
||||
auto_clear_visible == other.auto_clear_visible &&
|
||||
clear_visible == other.clear_visible &&
|
||||
order_entry_panel_initial_width == other.order_entry_panel_initial_width &&
|
||||
zoom_slider_visible == other.zoom_slider_visible &&
|
||||
zoom_factor == other.zoom_factor &&
|
||||
use_defaults == other.use_defaults &&
|
||||
favorite_algos == other.favorite_algos &&
|
||||
net_change == other.net_change &&
|
||||
show_warn_on_cross == other.show_warn_on_cross &&
|
||||
mini_ob == other.mini_ob;
|
||||
left_panel_scroll_position == other.left_panel_scroll_position;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as LadderSettings);
|
||||
|
||||
// TODO: remove mutable props from hash logic (or conversely, make them immutable).
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
row_height.GetHashCode(),
|
||||
grid_line_color.GetHashCode(),
|
||||
EqualityComparer.safe_get_hash_code(orders_column),
|
||||
EqualityComparer.safe_get_hash_code(bid_column),
|
||||
EqualityComparer.safe_get_hash_code(ask_column),
|
||||
EqualityComparer.safe_get_hash_code(price_column),
|
||||
EqualityComparer.safe_get_hash_code(last_column),
|
||||
EqualityComparer.safe_get_hash_code(piq_column),
|
||||
EqualityComparer.safe_get_hash_code(calculator_buttons),
|
||||
scroll_bar_visible.GetHashCode(),
|
||||
default_time_in_force.GetHashCode(),
|
||||
size.GetHashCode(),
|
||||
animate_recenter.GetHashCode(),
|
||||
badge_style.GetHashCode(),
|
||||
EqualityComparer.safe_get_hash_code(bid_break_line),
|
||||
EqualityComparer.safe_get_hash_code(ask_break_line),
|
||||
EqualityComparer.safe_get_hash_code(mid_break_line),
|
||||
alternate_order_size.GetHashCode(),
|
||||
EqualityComparer.safe_get_hash_code(session_low_line),
|
||||
EqualityComparer.safe_get_hash_code(session_high_line),
|
||||
EqualityComparer.safe_get_hash_code(session_range_fill),
|
||||
EqualityComparer.safe_get_hash_code(volume_column),
|
||||
EqualityComparer.safe_get_hash_code(size_display),
|
||||
day_visible.GetHashCode(),
|
||||
high_visible.GetHashCode(),
|
||||
low_visible.GetHashCode(),
|
||||
open_visible.GetHashCode(),
|
||||
settle_visible.GetHashCode(),
|
||||
volume_visible.GetHashCode(),
|
||||
account_visible.GetHashCode(),
|
||||
tif_visible.GetHashCode(),
|
||||
order_type_visible.GetHashCode(),
|
||||
account_position_visible.GetHashCode(),
|
||||
size_presets_visible.GetHashCode(),
|
||||
cancel_buys_visible.GetHashCode(),
|
||||
cancel_sells_visible.GetHashCode(),
|
||||
cancel_all_visible.GetHashCode(),
|
||||
auto_clear_visible.GetHashCode(),
|
||||
clear_visible.GetHashCode(),
|
||||
order_entry_panel_initial_width.GetHashCode(),
|
||||
zoom_slider_visible.GetHashCode(),
|
||||
zoom_factor.GetHashCode(),
|
||||
use_defaults.GetHashCode(),
|
||||
favorite_algos.GetHashCode(),
|
||||
net_change.GetHashCode(),
|
||||
show_warn_on_cross.GetHashCode());
|
||||
public override int GetHashCode() => _type_hash;
|
||||
|
||||
public struct WindowSpecifics {
|
||||
public readonly double order_entry_panel_width, zoom_factor;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using bts.api;
|
||||
using bts.collections;
|
||||
using bts.md;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
@@ -50,6 +51,7 @@ namespace bts.shell {
|
||||
_mbo = mbo;
|
||||
_dispose = Disposable.Create(
|
||||
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(s => _securities = s),
|
||||
bus.subscribe(SparkTags.ShowLadderAutoLookupTag).Subscribe(_show_windows),
|
||||
bus.subscribe(SparkTags.ShowLadderTag).OnDispatcher().Subscribe(s => show_windows(new[] { s })),
|
||||
bus.subscribe(SparkTags.ShowLaddersTag).OnDispatcher().Subscribe(show_windows)
|
||||
);
|
||||
@@ -79,11 +81,9 @@ namespace bts.shell {
|
||||
if (!_settings_hub.current.ladder_profiles.TryGetFirst(lp => lp.id == id, out var result)) {
|
||||
// profile was deleted
|
||||
id = 0;
|
||||
} else {
|
||||
if (!s.Equals(result.value)) {
|
||||
// ladder's settings diverged from the profile (this happens when the user changes settings outside of the settings dialog, such as column widths, left panel width, etc.)
|
||||
id = 0;
|
||||
}
|
||||
} else if (!LadderSettings.profile_equals(s, result.value)) {
|
||||
// ladder's settings diverged from the profile (this happens when the user changes settings outside of the settings dialog, such as column widths, left panel width, etc.)
|
||||
id = 0;
|
||||
}
|
||||
}
|
||||
return new LadderSaveState { security_key = vm.security.Id, settings = s, settings_id = id };
|
||||
@@ -147,7 +147,7 @@ namespace bts.shell {
|
||||
var curr = vm.get_save_state();
|
||||
if (settings_id == 0) {
|
||||
last_value = curr;
|
||||
} else if (!curr.Equals(last_value)) {
|
||||
} else if (!LadderSettings.profile_equals(curr, last_value)) {
|
||||
settings_id = 0;
|
||||
last_value = curr;
|
||||
}
|
||||
@@ -161,7 +161,7 @@ namespace bts.shell {
|
||||
// not on a profile, don't respond to default settings changes
|
||||
return;
|
||||
} else if (us.ladder_profiles.TryGetFirst(p => p.id == settings_id, out var profile)) {
|
||||
if (!last_value.Equals(vm.get_save_state())) {
|
||||
if (!LadderSettings.profile_equals(last_value, vm.get_save_state())) {
|
||||
settings_id = 0;
|
||||
} else {
|
||||
settings_subject.OnNext(last_value = profile.value);
|
||||
@@ -209,6 +209,7 @@ namespace bts.shell {
|
||||
_ => _,
|
||||
vm.get_save_state(),
|
||||
UserSettings.FileExtensions.LADDER,
|
||||
LadderSettings.profile_equals,
|
||||
settings_id);
|
||||
dialog_vm.applied += apply;
|
||||
// dialog
|
||||
@@ -273,7 +274,21 @@ namespace bts.shell {
|
||||
});
|
||||
}
|
||||
|
||||
void show_windows(IReadOnlyCollection<SecurityMD> securities) {
|
||||
async void _show_windows(IImmutableSet<uint> securities) {
|
||||
if (securities == null)
|
||||
return;
|
||||
var start_time = DateTime.Now;
|
||||
do {
|
||||
var found = securities.KeyJoin(_securities).ToList();
|
||||
securities = securities.RemoveRange(found.Select(SecurityMD.key_fn));
|
||||
show_windows(found);
|
||||
if (securities.Count == 0)
|
||||
break;
|
||||
await Task.Delay(500);
|
||||
} while ((DateTime.Now - start_time) < _ladder_conf.security_timeout);
|
||||
}
|
||||
|
||||
void show_windows(IEnumerable<SecurityMD> securities) {
|
||||
int n_allowed = (int)_ladder_conf.max_open_ladders - _wm.list_windows().Where(w => w.window_type == _window_type).Count();
|
||||
int n_opened = 0;
|
||||
foreach (var s in securities) {
|
||||
|
||||
@@ -35,7 +35,14 @@ namespace mbts.spark {
|
||||
_show_cancel_sells, _show_cancel_all, _show_zoom;
|
||||
readonly PropertyAutoWrapper<LadderSettings, Brush> _bid_background, _bid_foreground, _ask_background, _ask_foreground;
|
||||
readonly HashSet<AlgoKey> _sticky_algos = new HashSet<AlgoKey>();
|
||||
double _position, _day_position, _raw_day_position;
|
||||
double
|
||||
_position,
|
||||
_day_position,
|
||||
_raw_day_position,
|
||||
_left_panel_extent_height,
|
||||
_left_panel_view_port_height,
|
||||
_left_panel_scroll_position,
|
||||
_left_panel_scroll_position_target;
|
||||
bool _show_account_position, _show_day_position, _show_raw_day_position;
|
||||
ulong _misc_time = 0;
|
||||
ulong _last_trade_time = 0;
|
||||
@@ -76,6 +83,7 @@ namespace mbts.spark {
|
||||
_volume_column_width = backing(nameof(volume_column_width), ws.volume_column_width);
|
||||
_left_panel_width = backing(nameof(left_panel_width), ws.order_entry_panel_width);
|
||||
_zoom_factor = backing(nameof(zoom_factor), ws.zoom_factor);
|
||||
_left_panel_scroll_position_target = init.left_panel_scroll_position;
|
||||
// remember: account_positions needs to subscribe to selected_account; both the sub and account_positions need to be disposed
|
||||
_volume = backing(nameof(volume), 0ul);
|
||||
_auto_clear = backing(nameof(auto_clear), init.auto_clear);
|
||||
@@ -333,6 +341,33 @@ namespace mbts.spark {
|
||||
public bool show_cancel_sells => _show_cancel_sells.view;
|
||||
public bool show_cancel_all => _show_cancel_all.view;
|
||||
public bool show_zoom => _show_zoom.view;
|
||||
// left panel scroll props
|
||||
public double left_panel_view_port_height {
|
||||
get => _left_panel_view_port_height;
|
||||
set {
|
||||
using (detect_property_changes(nameof(left_panel_scroll_position)))
|
||||
_left_panel_view_port_height = value;
|
||||
}
|
||||
}
|
||||
public double left_panel_scroll_position {
|
||||
get {
|
||||
if (!_left_panel_scroll_position_target.is_nan()) {
|
||||
if (_left_panel_scroll_position_target < _max_left_panel_scroll_position) {
|
||||
_left_panel_scroll_position = _left_panel_scroll_position_target;
|
||||
_left_panel_scroll_position_target = double.NaN;
|
||||
}
|
||||
}
|
||||
return Math.Min(_left_panel_scroll_position, _left_panel_extent_height);
|
||||
}
|
||||
set => Set(ref _left_panel_scroll_position, value);
|
||||
}
|
||||
public double left_panel_extent_height {
|
||||
get => _left_panel_extent_height;
|
||||
set {
|
||||
using (detect_property_changes(nameof(left_panel_scroll_position)))
|
||||
Set(ref _left_panel_extent_height, value);
|
||||
}
|
||||
}
|
||||
public double left_panel_width { get => _left_panel_width.Value; set => _left_panel_width.set(value); }
|
||||
public double zoom_factor { get => _zoom_factor.Value; set => _zoom_factor.set(value); }
|
||||
public string position_display_text {
|
||||
@@ -364,6 +399,10 @@ namespace mbts.spark {
|
||||
public bool show_warn_on_cross_from_settings => current_settings.show_warn_on_cross;
|
||||
internal LadderModel model => _model;
|
||||
internal IObservable<LadderSettings> settings { get; }
|
||||
|
||||
double _max_left_panel_scroll_position =>
|
||||
Math.Max(0, _left_panel_extent_height - _left_panel_view_port_height);
|
||||
|
||||
internal OrderRecordImmutable get_root_at_price((OrderRecordImmutable order, uint? _, AlgoData? __, AlgoDefinitionMetadata? meta) op) {
|
||||
var order = op.order;
|
||||
double p = order.price();
|
||||
@@ -394,6 +433,7 @@ namespace mbts.spark {
|
||||
zoom_factor,
|
||||
piq_column_width));
|
||||
rv.auto_clear = auto_clear;
|
||||
rv.left_panel_scroll_position = left_panel_scroll_position;
|
||||
if (order_properties_vm != null) {
|
||||
rv.default_time_in_force = order_properties_vm.selected_time_in_force;
|
||||
if (order_properties_vm.selected_account.HasValue)
|
||||
@@ -451,7 +491,7 @@ namespace mbts.spark {
|
||||
price = temp_price;
|
||||
}
|
||||
}
|
||||
await _model.order_gateway.send_order(order, price, size);
|
||||
await _model.order_gateway.send_order(security, order, price, size);
|
||||
if (auto_clear)
|
||||
size_calculator.value = 0;
|
||||
var fav_algo = order_properties_vm.selected_favorite_algo_definition;
|
||||
|
||||
@@ -301,7 +301,14 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="0" Grid.RowSpan="2" VerticalScrollBarVisibility="Hidden">
|
||||
<ScrollViewer
|
||||
x:Name="_left_panel_scroll"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
mbts:ScrollViewerBehavior.VerticalOffset="{Binding left_panel_scroll_position, Mode=TwoWay}"
|
||||
mbts:ScrollViewerBehavior.EnableTwoWayVerticalOffsetBinding="True"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
>
|
||||
<Grid RowSpan="2" Background="{mbts:BTSColor UI.spark_menu_background}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using bts;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
|
||||
@@ -80,6 +86,8 @@ namespace mbts.spark {
|
||||
_ladder.visible_columns_changed += () => _column_headers_presenter.Columns.SetItems(
|
||||
_ladder.visible_column_types.Select(t => ColumnFactory.create(_ladder, t)));
|
||||
this.vm = vm;
|
||||
this.ConnectDataContext<LadderVM>(lvm => lvm.bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(sec_map => Resources[ResourceKeys.Securities] = sec_map));
|
||||
vm.SubscribeDisposed(_connect_left_panel_scroll());
|
||||
DataContext = vm;
|
||||
DataContextChanged += datacontext_changed;
|
||||
_ui_scaler.MouseDoubleClick += _ui_scaler_MouseDoubleClick;
|
||||
@@ -87,7 +95,39 @@ namespace mbts.spark {
|
||||
this.PreviewMouseDown += LadderView_PreviewMouseDown;
|
||||
}
|
||||
|
||||
|
||||
IDisposable _connect_left_panel_scroll() {
|
||||
// extent height
|
||||
bool busy = false;
|
||||
var extent_height_hook = _left_panel_scroll.ObserveProperty(ScrollViewer.ExtentHeightProperty, (s, e) => {
|
||||
if (!busy) {
|
||||
double scroll_pos = vm.left_panel_scroll_position;
|
||||
busy = true;
|
||||
Dispatcher.InvokeAsync(() => {
|
||||
vm.left_panel_extent_height = _left_panel_scroll.ExtentHeight;
|
||||
if (_left_panel_scroll.VerticalOffset != scroll_pos)
|
||||
_left_panel_scroll.TryScrollToVerticalOffset(scroll_pos);
|
||||
busy = false;
|
||||
}, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
});
|
||||
// view port height
|
||||
_left_panel_scroll.Loaded += on_loaded;
|
||||
var dispose = Disposable.CreateDisposeOnce(dispose_impl);
|
||||
return Disposable.Create(dispose, vm.SubscribeDisposed(dispose));
|
||||
// local methods
|
||||
void on_loaded(object sender, RoutedEventArgs e) {
|
||||
if (_left_panel_scroll.descendants_of_type<ScrollBar>().TryGetFirst(sb => sb.Orientation == Orientation.Vertical, out var vscroll)) {
|
||||
vscroll.SetBinding(ScrollBar.ViewportSizeProperty, new Binding(nameof(LadderVM.left_panel_view_port_height)) {
|
||||
Converter = ValueConverter.create<double, Size>(d => new Size(0, d), sz => sz.Height),
|
||||
Mode = BindingMode.OneWayToSource
|
||||
});
|
||||
}
|
||||
}
|
||||
void dispose_impl() {
|
||||
Disposable.dispose(ref extent_height_hook);
|
||||
_left_panel_scroll.Loaded -= on_loaded;
|
||||
}
|
||||
}
|
||||
|
||||
// (Left|Right) CTRL + Wheel to zoom
|
||||
// This is neat b/c the user doesn't even need to have the slider visible to zoom
|
||||
|
||||
@@ -62,15 +62,15 @@
|
||||
Header="{Binding security}"
|
||||
IsEditMode="False"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.SecuritiesKey}}"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurity="{Binding security, UpdateSourceTrigger=Explicit}"
|
||||
ShowDialogSkirt="True"
|
||||
ToolTip="{Binding SelectedCriteria, RelativeSource={RelativeSource Self}, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}, Mode=OneWay}"
|
||||
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityHandler}"
|
||||
/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.AccountsKey}}" SelectedItem="{Binding account}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.UsersKey}}" SelectedItem="{Binding user}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.SidesKey}}" SelectedItem="{Binding side}" Width="48"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Accounts}}" SelectedItem="{Binding account}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Users}}" SelectedItem="{Binding user}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Sides}}" SelectedItem="{Binding side}" Width="48"/>
|
||||
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding size}"/>
|
||||
<usp:PriceEntryControl Grid.Row="2" Grid.Column="10" MinWidth="55" Value="{Binding price}" ContractSpec="{Binding spec}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -6,12 +6,6 @@ namespace mbts.ui.spark {
|
||||
/// Interaction logic for ManualFillRowEditor.xaml
|
||||
/// </summary>
|
||||
partial class ManualFillRowEditor : UserControl {
|
||||
public const string
|
||||
SecuritiesKey = nameof(SecuritiesKey),
|
||||
AccountsKey = nameof(AccountsKey),
|
||||
UsersKey = nameof(UsersKey),
|
||||
SidesKey = nameof(SidesKey);
|
||||
|
||||
public static readonly RoutedCommand
|
||||
CommitCommand = new RoutedCommand(),
|
||||
CancelCommand = new RoutedCommand();
|
||||
|
||||
@@ -70,7 +70,7 @@ namespace bts.shell.manual_fills {
|
||||
mfd => new ManualFillRow(mfd),
|
||||
mfr => mfr.data),
|
||||
_bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(m => {
|
||||
security_map = m;
|
||||
security_map = new FilteredDictionary<uint, SecurityMD>(m, smd => smd.security.ContractCase == Security.ContractOneofCase.Future);
|
||||
RaisePropertyChanged(nameof(security_map));
|
||||
}),
|
||||
ObservableAutoWrapper.connect_item_hooks(_persisted_rows, r => Disposable.Create(dirty_mon.join(r), selection_mon.join(r))),
|
||||
|
||||
@@ -50,11 +50,11 @@ namespace mbts.ui.spark {
|
||||
row_editor.CommandBindings.Add(new CommandBinding(ManualFillRowEditor.CommitCommand, commit_executed));
|
||||
row_editor.CommandBindings.Add(new CommandBinding(ManualFillRowEditor.CancelCommand, cancel_executed));
|
||||
// dynamic resources
|
||||
row_editor.Resources[ManualFillRowEditor.AccountsKey] = vm.accounts;
|
||||
row_editor.Resources[ManualFillRowEditor.UsersKey] = vm.users;
|
||||
row_editor.Resources[ManualFillRowEditor.SidesKey] = vm.sides;
|
||||
row_editor.Resources[ResourceKeys.Accounts] = vm.accounts;
|
||||
row_editor.Resources[ResourceKeys.Users] = vm.users;
|
||||
row_editor.Resources[ResourceKeys.Sides] = vm.sides;
|
||||
FocusManager.SetIsFocusScope(row_editor, true);
|
||||
var sec_hook = vm.as_demuxer().subscribe(nameof(ManualFillsVM.security_map)).OnDispatcher().Subscribe(v => row_editor.Resources[ManualFillRowEditor.SecuritiesKey] = v);
|
||||
var sec_hook = vm.as_demuxer().subscribe(nameof(ManualFillsVM.security_map)).OnDispatcher().Subscribe(v => row_editor.Resources[ResourceKeys.Securities] = v);
|
||||
// popup adorner
|
||||
var popup_adorner = new PopupAdorner(
|
||||
data_grid, data_grid,
|
||||
|
||||
@@ -121,7 +121,7 @@ namespace mbts.spark {
|
||||
r => r.user,
|
||||
DefaultColumnWidths.User,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, string>(
|
||||
model.factory.subscribe_users().Select(d => d.Values),
|
||||
model.factory.subscribe_users().Select(d => d.Values.Ordered()),
|
||||
ort => ort.order.order.user(),
|
||||
nameof(OrderBookRowVM.user)),
|
||||
DataGridFilterKnownTemplates.DiscreteValues,
|
||||
@@ -133,7 +133,7 @@ namespace mbts.spark {
|
||||
r => r.account,
|
||||
DefaultColumnWidths.Account,
|
||||
DiscreteValuesFilterVM.Create<OrderSecurityRootPair, string, string[]>(
|
||||
model.factory.subscribe_accounts().Select(d => d.Values),
|
||||
model.factory.subscribe_accounts().Select(d => d.Values.Ordered()),
|
||||
ort => ort.order.order.account(),
|
||||
vals => vals.ToArray(),
|
||||
vals => Observable.Return(vals),
|
||||
@@ -750,6 +750,7 @@ namespace mbts.spark {
|
||||
};
|
||||
header_cb.SetBinding(CheckBox.IsCheckedProperty, new Binding(nameof(OrderBookVM.all_rows_selected)));
|
||||
this.Header = header_cb;
|
||||
is_visible = true;
|
||||
}
|
||||
|
||||
public override FrameworkElement view_factory() {
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace mbts.spark {
|
||||
public CustomOrderDefinition.CustomOneofCase root_algo_order_type => order_tuple.root.algo.value.CustomCase;
|
||||
public OrderMetadata root_algo_order_metadata => _owner.get_metadata(root_algo_order_type);
|
||||
public Brush root_algo_badge_fill => _root_algo_badge_fill ?? (_root_algo_badge_fill = new SolidColorBrush(_root_algo_badge_color));
|
||||
public string root_algo_display_name => order_tuple.root.algo.name;
|
||||
public string root_algo_display_name => order_tuple.root.display_name();
|
||||
|
||||
// Tree
|
||||
public bool is_parent { get => _is_parent; set => Set(ref _is_parent, value); }
|
||||
|
||||
@@ -239,7 +239,7 @@ namespace mbts.spark {
|
||||
grid_settings_manager,
|
||||
order_mod_vm.algo_vm.connect_data(Observable.Return(model)),
|
||||
order_mod_vm.algo_vm.connect_data(_selected_params_mon.subscribe_spreader_security()),
|
||||
order_mod_vm.algo_vm,
|
||||
order_mod_vm,
|
||||
attach_vm,
|
||||
_show_fills.observe_current(true)
|
||||
.Subscribe(is_visible => {
|
||||
@@ -288,7 +288,14 @@ namespace mbts.spark {
|
||||
double price = attach_vm.price;
|
||||
uint? size = attach_vm.size;
|
||||
uint algo_template_key = attach_vm.algo_vm.algo_root.id.is_persisted ? attach_vm.algo_vm.algo_root.id.persisted : 0u;
|
||||
var order_tups = selected_rows.Select(r => r.order_tuple).ToList();
|
||||
// Don't pass any order tuples that don't have a security defined.
|
||||
// The actual security is passed to the OrdersModel - instead of just the security key -
|
||||
// so it can prevent GlbxIceberg orders from being sent to a non-glbx exchange (such as Ice).
|
||||
/// Impl details: <see cref="OrdersModel.send_order(SecurityImmutable, OrderDefinitionImmutable?, double, uint)"/>
|
||||
var order_tups = selected_rows
|
||||
.Select(r => r.order_tuple)
|
||||
.Where(ot => ot.order.security.HasValue)
|
||||
.ToList();
|
||||
var mod_algo_vm = attach_vm.algo_vm.algo_root;
|
||||
await Task.WhenAll(order_tups.Select(t => {
|
||||
var algo = t.order.order.Def.CustomDef.actual().CloneToMutable();
|
||||
@@ -302,6 +309,7 @@ namespace mbts.spark {
|
||||
};
|
||||
}
|
||||
return _model.send_order(
|
||||
t.order.security.Value,
|
||||
t.order.order.Def.with(
|
||||
attach_target_order_key: t.order.order.Key,
|
||||
custom_def: algo
|
||||
@@ -727,17 +735,13 @@ namespace mbts.spark {
|
||||
_size_permissions = AlgoParamPermissions.ReadOnly | AlgoParamPermissions.Write;
|
||||
SecurityMD? _security_md;
|
||||
|
||||
public OrderParamsVM(OrderBookVM owner, AlgoFormats init_format, AlgoEditingPermissions init_permissions)
|
||||
: this(owner,
|
||||
AlgoDefinitionVM.create(
|
||||
owner._model,
|
||||
AlgoData.create_unpersisted(OrderDataHelper.Defaults.CustomOrderDefinitionImmutable(CustomOrderDefinition.CustomOneofCase.Iceberg), CustomOrderDefinition.CustomOneofCase.Iceberg.display_name()),
|
||||
init_format: init_format,
|
||||
editing_permissions: init_permissions)) { }
|
||||
|
||||
OrderParamsVM(OrderBookVM owner, AlgoDefinitionVM algo_vm) {
|
||||
public OrderParamsVM(OrderBookVM owner, AlgoFormats init_format, AlgoEditingPermissions init_permissions) {
|
||||
_owner = owner;
|
||||
this.algo_vm = algo_vm;
|
||||
algo_vm = AlgoDefinitionVM.create(
|
||||
owner._model,
|
||||
AlgoData.create_unpersisted(OrderDataHelper.Defaults.CustomOrderDefinitionImmutable(CustomOrderDefinition.CustomOneofCase.Iceberg), CustomOrderDefinition.CustomOneofCase.Iceberg.display_name()),
|
||||
init_format: init_format,
|
||||
editing_permissions: init_permissions);
|
||||
_accounts = backing(
|
||||
nameof(selected_account),
|
||||
_model.factory.subscribe_account_tuples(),
|
||||
@@ -776,7 +780,8 @@ namespace mbts.spark {
|
||||
appended_param_rows.insert_front(new AlgoParamRowVM(root, _price_param, null, AlgoParamTemplateKeys.Price));
|
||||
}
|
||||
}),
|
||||
ObservableAutoWrapper.connect_item_hooks(param_rows, dirty_params_mon.join));
|
||||
ObservableAutoWrapper.connect_item_hooks(param_rows, dirty_params_mon.join),
|
||||
algo_vm);
|
||||
// payup methods
|
||||
void payup_bid(int ticks) => payup(bbo.bid, -ticks);
|
||||
void payup_ask(int ticks) => payup(bbo.ask, ticks);
|
||||
|
||||
@@ -66,6 +66,10 @@ namespace mbts.spark {
|
||||
_vm = new OrderBookVM(_bus, _notification_service, _model, _settings_manager);
|
||||
_vm.load_from(_state);
|
||||
_view = new OrderBookView(_vm);
|
||||
_vm.SubscribeDisposed(
|
||||
_bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => _view.Resources[ResourceKeys.Securities] = sec_map));
|
||||
|
||||
var show_fills_command = SparkCommands.ShowFills(SparkCommands.KeyGestures.ShowFills).complete(() => new FillsSettings(_vm.selected_orders.Select(r => r.Key).ToArray()));
|
||||
|
||||
|
||||
@@ -501,6 +501,12 @@ namespace bts.shell.pnl {
|
||||
account_name == other.account_name &&
|
||||
sod == other.sod &&
|
||||
day == other.day &&
|
||||
bid_price.Equals(other.bid_price) &&
|
||||
ask_price.Equals(other.ask_price) &&
|
||||
last_price.Equals(other.last_price) &&
|
||||
mid_price.Equals(other.mid_price) &&
|
||||
settlement_price.Equals(other.settlement_price) &&
|
||||
calc_type.Equals(other.calc_type) &&
|
||||
raw_day == other.raw_day &&
|
||||
working.summary == other.working.summary;
|
||||
|
||||
|
||||
@@ -97,22 +97,22 @@ namespace bts.shell.pnl {
|
||||
if (security.HasValue && security.Value.ContractCase != Security.ContractOneofCase.Future) {
|
||||
day_long = day_short = sod_long = sod_short = 0;
|
||||
} else {
|
||||
day_long = (decimal)DoubleUtil.nan_to_zero(data.day.long_pos);
|
||||
day_short = (decimal)DoubleUtil.nan_to_zero(data.day.short_pos);
|
||||
sod_long = (decimal)DoubleUtil.nan_to_zero(data.sod.long_pos);
|
||||
sod_short = (decimal)DoubleUtil.nan_to_zero(data.sod.short_pos);
|
||||
day_long = DoubleUtil.round_to_decimal(data.day.long_pos);
|
||||
day_short = DoubleUtil.round_to_decimal(data.day.short_pos);
|
||||
sod_long = DoubleUtil.round_to_decimal(data.sod.long_pos);
|
||||
sod_short = DoubleUtil.round_to_decimal(data.sod.short_pos);
|
||||
}
|
||||
var nd = new PNLNormalizedData(
|
||||
(decimal)DoubleUtil.nan_to_zero(day_pnl.value),
|
||||
DoubleUtil.round_to_decimal(day_pnl.value),
|
||||
day_long,
|
||||
day_short,
|
||||
(decimal)DoubleUtil.nan_to_zero(data.raw_day.long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.raw_day.short_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(sod_pnl.value),
|
||||
DoubleUtil.round_to_decimal(data.raw_day.long_pos),
|
||||
DoubleUtil.round_to_decimal(data.raw_day.short_pos),
|
||||
DoubleUtil.round_to_decimal(sod_pnl.value),
|
||||
sod_long,
|
||||
sod_short,
|
||||
(decimal)DoubleUtil.nan_to_zero(data.working_long_pos),
|
||||
(decimal)DoubleUtil.nan_to_zero(data.working_short_pos));
|
||||
DoubleUtil.round_to_decimal(data.working_long_pos),
|
||||
DoubleUtil.round_to_decimal(data.working_short_pos));
|
||||
if (_normalized_data != nd) {
|
||||
_normalized_data = nd;
|
||||
updated?.Invoke(nd);
|
||||
|
||||
@@ -103,7 +103,7 @@ namespace bts.shell.pnl {
|
||||
load_from(settings);
|
||||
_dispose = Disposable.Create(
|
||||
pnl_source.Operate(
|
||||
_calculation_type.observe_current(),
|
||||
_calculation_type.observe_current(true),
|
||||
(data, calc) => data.with(calc_type: calc)
|
||||
).FilterWithRaw(filter_src, PNLRowData.key_fn, null)
|
||||
.OnDispatcher()
|
||||
|
||||
@@ -373,7 +373,9 @@ namespace bts.shell.pnl {
|
||||
var less_last = data - last_data;
|
||||
var plus_new = less_last + new_data;
|
||||
data += new_data - last_data;
|
||||
Log.assert(plus_new == data);
|
||||
if (plus_new != data) {
|
||||
Log.Ui.warn($"Detected PNL arithmetic rounding error in the group sum for '{string.Join(" ", GroupNamePath)}' (this bug is under investigation).");
|
||||
}
|
||||
last_data = new_data;
|
||||
}
|
||||
void on_is_selected_changed(bool is_selected) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,8 +7,9 @@ using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace mbts.spark {
|
||||
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged {
|
||||
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged, ITemplateSelectorKey {
|
||||
readonly IEqualityComparer<T> _comparer;
|
||||
readonly string _template_key;
|
||||
AlgoParamPermissions _permissions;
|
||||
bool _is_enabled, _has_value;
|
||||
T _committed_value, _value;
|
||||
@@ -15,15 +17,16 @@ namespace mbts.spark {
|
||||
public AlgoParamVM(string id, string display_name)
|
||||
: this(id, display_name, default, EqualityComparer<T>.Default) { }
|
||||
|
||||
public AlgoParamVM(string id, string display_name, T value)
|
||||
: this(id, display_name, value, EqualityComparer<T>.Default) { }
|
||||
public AlgoParamVM(string id, string display_name, T value, string template_key = null)
|
||||
: this(id, display_name, value, EqualityComparer<T>.Default, template_key) { }
|
||||
|
||||
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer) {
|
||||
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null) {
|
||||
this.id = id;
|
||||
this.display_name = display_name;
|
||||
_value = value;
|
||||
_committed_value = value;
|
||||
_comparer = comparer;
|
||||
_template_key = template_key;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
@@ -50,7 +53,9 @@ namespace mbts.spark {
|
||||
public bool is_enabled { get => _is_enabled; set => _set(ref _is_enabled, value); }
|
||||
public bool has_value { get => _has_value; set => _set(ref _has_value, value); }
|
||||
public bool is_dirty { get; private set; }
|
||||
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
|
||||
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
|
||||
|
||||
string ITemplateSelectorKey.key => _template_key;
|
||||
|
||||
public void revert() {
|
||||
if (is_dirty) {
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace mbts.spark {
|
||||
public AlgoDefinitionMetadata? root_algo_meta =>
|
||||
_order_tuple.HasValue ? _order_tuple.Value.root.meta : null;
|
||||
|
||||
public OrderSecurityAlgo? root_order_security => _order_tuple?.root;
|
||||
|
||||
public string account {
|
||||
get {
|
||||
var account_string = string.Empty;
|
||||
@@ -85,4 +87,4 @@ namespace mbts.spark {
|
||||
public static string user(this FillTuple fill_tuple) =>
|
||||
fill_tuple.order.HasValue ? fill_tuple.order.Value.user() : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +53,7 @@ namespace mbts.spark {
|
||||
send_order = new DelegateCommand<Side>(side => {
|
||||
bool price_valid = _validate_price(), size_valid = size_calculator.validate();
|
||||
if (price_valid && size_valid && !HasErrors && !properties_vm.HasErrors) {
|
||||
model.send_order(properties_vm.create(side), price, size_calculator.value.Value).ignore();
|
||||
model.send_order(security, properties_vm.create(side), price, size_calculator.value.Value).ignore();
|
||||
RequestClose?.Invoke(this, new EventArgs());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -67,7 +67,6 @@ namespace mbts.spark.utilities {
|
||||
|
||||
public IObservable<IReadOnlyDictionary<uint, string>> subscribe_accounts() => _admin_model.on_accounts;
|
||||
public IObservable<IReadOnlyDictionary<uint, string>> subscribe_users() => _admin_model.on_users;
|
||||
public IObservable<IReadOnlyList<UserTuple>> subscribe_user_tuples() => _admin_model.on_user_tuples;
|
||||
public IObservable<IReadOnlyList<AccountTuple>> subscribe_account_tuples() => _admin_model.on_account_tuples;
|
||||
|
||||
#region Defaults
|
||||
|
||||
@@ -62,6 +62,7 @@ namespace mbts.spark {
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: param.AsInt32 = 0; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveOrderDef: param.AsOrderDef = new CustomOrderDefinition(); break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: param.AsString = string.Empty; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey:
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: param.AsUint32 = 0u; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: param.AsQuantity = new bts.spark.QuantityMeasure(); break;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using System.Reactive.Linq;
|
||||
namespace mbts.spark {
|
||||
interface IDynamicOrderParamVM {
|
||||
DynamicOrderParamDescriptor descriptor { get; }
|
||||
DynamicOrderParam param { get; set; }
|
||||
DynamicOrderParam param { get; }
|
||||
}
|
||||
|
||||
interface IDynamicOrderParamVM<T> : IDynamicOrderParamVM {
|
||||
@@ -21,26 +21,16 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
abstract class DynamicOrderParamVM : DisposableViewModelBase, IDynamicOrderParamVM {
|
||||
DynamicOrderParam _param;
|
||||
|
||||
protected DynamicOrderParamVM(DynamicOrderParamDescriptor descriptor)
|
||||
: this(descriptor, DynamicOrderHelper.init_param(descriptor)) { }
|
||||
|
||||
private DynamicOrderParamVM(DynamicOrderParamDescriptor descriptor, DynamicOrderParam param) {
|
||||
this.descriptor = descriptor;
|
||||
_param = param;
|
||||
this.param = param;
|
||||
}
|
||||
|
||||
public DynamicOrderParamDescriptor descriptor { get; set; }
|
||||
public DynamicOrderParam param {
|
||||
get => _param;
|
||||
set {
|
||||
_param = value;
|
||||
on_param_changed();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void on_param_changed();
|
||||
public DynamicOrderParam param { get; }
|
||||
}
|
||||
|
||||
class DynamicOrderParamVM<TView, TModel> : DynamicOrderParamVM, IDynamicOrderParamVM<TView> {
|
||||
@@ -53,14 +43,12 @@ namespace mbts.spark {
|
||||
_field_desc = field_descriptor;
|
||||
_convert = convert;
|
||||
_convert_back = convert_back;
|
||||
value = _convert(ConvertUtils.try_convert<TModel>(_field_desc.Accessor.GetValue(param)));
|
||||
}
|
||||
|
||||
public TView value {
|
||||
get => _value;
|
||||
set {
|
||||
_field_desc.Accessor.SetValue(param, _convert_back(_value = value));
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
set => Set(ref _value, value);
|
||||
}
|
||||
|
||||
public IObservable<TView> subscribe_value() =>
|
||||
@@ -70,11 +58,6 @@ namespace mbts.spark {
|
||||
void on_prop_changed(object sender, PropertyChangedEventArgs e) =>
|
||||
o.OnNext(value);
|
||||
});
|
||||
|
||||
protected override void on_param_changed() {
|
||||
_value = _convert(ConvertUtils.try_convert<TModel>(_field_desc.Accessor.GetValue(param)));
|
||||
RaisePropertyChanged(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicOrderEnumeratedVM : DynamicOrderParamVM, IDynamicOrderParamVM<string> {
|
||||
@@ -105,11 +88,6 @@ namespace mbts.spark {
|
||||
o.OnNext(selected_element);
|
||||
});
|
||||
|
||||
protected override void on_param_changed() {
|
||||
_sync_elements();
|
||||
RaisePropertyChanged(nameof(selected_element));
|
||||
}
|
||||
|
||||
static IEnumerable<string> _get_elements(DynamicOrderParamDescriptor descriptor) =>
|
||||
descriptor.ParamType?.DescriptorCase == DynamicOrderTypeDescriptor.DescriptorOneofCase.Enumerated ?
|
||||
descriptor.ParamType.Enumerated.Element : Enumerable.Empty<string>();
|
||||
@@ -148,10 +126,5 @@ namespace mbts.spark {
|
||||
o.OnNext(quantity);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void on_param_changed()
|
||||
{
|
||||
RaisePropertyChanged(nameof(quantity));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace mbts.ui.spark
|
||||
public DataTemplate StringTemplate { get; set; }
|
||||
public DataTemplate OrderDefTemplate { get; set; }
|
||||
public DataTemplate QuantityMeasureTemplate { get; set; }
|
||||
public DataTemplate SecurityKeyTemplate { get; set; }
|
||||
|
||||
public DataTemplate EnumTemplate { get; set; }
|
||||
|
||||
@@ -38,6 +39,7 @@ namespace mbts.ui.spark
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveDouble: return DoubleTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: return IntTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: return UIntTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey: return SecurityKeyTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: return StringTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveOrderDef: return OrderDefTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: return QuantityMeasureTemplate;
|
||||
|
||||
@@ -4,6 +4,7 @@ using bts.collections;
|
||||
using bts.spark;
|
||||
using mbts.collections;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
@@ -73,9 +74,9 @@ namespace mbts.spark {
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveDouble: return mk<double, double>(DynamicOrderParam.AsDoubleFieldNumber, v => v, v => v, double.NaN);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: return mk<int?, int>(DynamicOrderParam.AsInt32FieldNumber, v => v, v => v.GetValueOrDefault());
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: return mk<uint?, uint>(DynamicOrderParam.AsUint32FieldNumber, v => v, v => v.GetValueOrDefault());
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey: return mk<uint?, uint>(DynamicOrderParam.AsUint32FieldNumber, v => v, v => v.GetValueOrDefault(), template_key: TemplateKeys.SecurityKey);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: return mk<string, string>(DynamicOrderParam.AsStringFieldNumber, v => v, v => v);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity:
|
||||
{
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: {
|
||||
var qm = new DynamicOrderQuantityVM(descriptor);
|
||||
return attached(qm, DynamicOrderHelper.get_quantity_value, DynamicOrderHelper.set_quantity_value, new QuantityMeasure { Ratio = 1.0, Absolute = 1 });
|
||||
}
|
||||
@@ -88,16 +89,16 @@ namespace mbts.spark {
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(descriptor.ParamType.DescriptorCase, nameof(descriptor));
|
||||
}
|
||||
|
||||
DynamicOrderParamVM<TView, TModel> mk<TView, TModel>(int field_number, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default) {
|
||||
DynamicOrderParamVM<TView, TModel> mk<TView, TModel>(int field_number, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default, string template_key = null) {
|
||||
var fdesc = DynamicOrderParam.Descriptor.FindFieldByNumber(field_number);
|
||||
var pvm = new DynamicOrderParamVM<TView, TModel>(descriptor, fdesc, convert, convert_back);
|
||||
return attached<TView, DynamicOrderParamVM<TView, TModel>>(pvm, get_from_param, assign_to_param, default_value);
|
||||
return attached<TView, DynamicOrderParamVM<TView, TModel>>(pvm, get_from_param, assign_to_param, default_value, template_key);
|
||||
|
||||
TView get_from_param(DynamicOrderParam param) => convert(ConvertUtils.try_convert<TModel>(fdesc.Accessor.GetValue(param)));
|
||||
void assign_to_param(DynamicOrderParam param, TView value) => fdesc.Accessor.SetValue(param, convert_back(value));
|
||||
}
|
||||
|
||||
TParam attached<T, TParam>(TParam pvm, Func<DynamicOrderParam, T> selector, Action<DynamicOrderParam, T> assign_to_param, T default_value = default) where TParam : DynamicOrderParamVM, IDynamicOrderParamVM<T> {
|
||||
TParam attached<T, TParam>(TParam pvm, Func<DynamicOrderParam, T> selector, Action<DynamicOrderParam, T> assign_to_param, T default_value = default, string template_key = null) where TParam : DynamicOrderParamVM, IDynamicOrderParamVM<T> {
|
||||
add_property<T>(
|
||||
pvm.descriptor.ParamName,
|
||||
mdef => mdef.OrderParams.TryGetValue(pvm.descriptor.ParamName, out DynamicOrderParam p) ? selector(p) : default_value,
|
||||
@@ -105,7 +106,8 @@ namespace mbts.spark {
|
||||
() => pvm.value,
|
||||
v => pvm.value = v,
|
||||
pvm.subscribe_value(),
|
||||
default_value);
|
||||
default_value,
|
||||
template_key: template_key);
|
||||
return pvm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ namespace mbts.spark {
|
||||
case AlgoType.Dynamic: vm = dynamic(key, name, name, editing_permissions); break;
|
||||
case AlgoType.Splitter: vm = splitter(key,name,editing_permissions); break;
|
||||
case AlgoType.Composite: vm = composite(key, name, editing_permissions); break;
|
||||
case AlgoType.None: vm = new DefaultAlgoVM(); break;
|
||||
default: vm = generic(key, name, algo_type, editing_permissions); break;
|
||||
//default:
|
||||
// Log.Ui.debug_fatal($"Failed to create {typeof(IAlgoVM)}: unknown algo type '{algo_type}.'");
|
||||
@@ -166,7 +167,8 @@ namespace mbts.spark {
|
||||
};
|
||||
void subscribe_new_algo(IAlgoVM old_algo, IAlgoVM new_algo) {
|
||||
RaisePropertyChanged(sd.prop_name);
|
||||
Disposable.replace(ref def_sub, new CompositeDisposable());
|
||||
Disposable.dispose(ref def_sub);
|
||||
def_sub = new CompositeDisposable();
|
||||
if (assert(new_algo != null, sd.prop_name, "No algo specified.")) {
|
||||
def_sub.Add(new_algo.subscribe_definition().Subscribe(_ => _def_change_notifier.execute()));
|
||||
if (new_algo is INotifyDataErrorInfo ndei)
|
||||
@@ -255,14 +257,14 @@ namespace mbts.spark {
|
||||
_security_changed += o.OnNext;
|
||||
return Disposable.Create(() => _security_changed -= o.OnNext);
|
||||
});
|
||||
Disposable.replace(ref _pending_load, Disposable.Create(
|
||||
Disposable.dispose(ref _pending_load);
|
||||
_pending_load = Disposable.Create(
|
||||
// load children
|
||||
ObservableAutoWrapper.connect_item_hooks(
|
||||
_child_algo_selector_data.AsCollection(),
|
||||
sd => security_src.SubscribeEach(_ => ObservableAutoWrapper.connect_item_actions(_child_algo_selector_data.AsCollection(), selector_load_impl))),
|
||||
// load regular props
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load(algom))));
|
||||
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load(algom)));
|
||||
}
|
||||
|
||||
void selector_load_impl(AlgoSelectorData sd) {
|
||||
@@ -298,11 +300,12 @@ namespace mbts.spark {
|
||||
_security_changed += o.OnNext;
|
||||
return Disposable.Create(() => _security_changed -= o.OnNext);
|
||||
});
|
||||
Disposable.replace(ref _pending_load, Disposable.Create(
|
||||
Disposable.dispose(ref _pending_load);
|
||||
_pending_load = Disposable.Create(
|
||||
ObservableAutoWrapper.connect_item_hooks(
|
||||
_child_algo_selector_data.AsCollection(),
|
||||
sd => security_src.Subscribe(_ => ObservableAutoWrapper.connect_item_actions(_child_algo_selector_data.AsCollection(), selector_load_impl))),
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load_many(algos))));
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load_many(algos)));
|
||||
|
||||
void selector_load_impl(AlgoSelectorData sd) {
|
||||
// **assume that algos has more than 1 element
|
||||
@@ -542,8 +545,19 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
protected void add_property<TValue>(string prop_name, Func<TMutable, TValue> get_from_def, Action<TMutable, TValue> assign_to_def, Func<TValue> get_local, Action<TValue> set_local, IObservable<TValue> subscribe_local, TValue non_uniform_value = default, AlgoParamPermissions permissions = AlgoParamPermissions.ReadWrite) {
|
||||
var param = new AlgoParamVM<TValue>(prop_name, DisplayNameHelper.snake_to_title_case(prop_name), get_local()) { permissions = permissions };
|
||||
protected void add_property<TValue>(
|
||||
string prop_name,
|
||||
Func<TMutable, TValue> get_from_def,
|
||||
Action<TMutable, TValue> assign_to_def,
|
||||
Func<TValue> get_local,
|
||||
Action<TValue> set_local,
|
||||
IObservable<TValue> subscribe_local,
|
||||
TValue non_uniform_value = default,
|
||||
AlgoParamPermissions permissions = AlgoParamPermissions.ReadWrite,
|
||||
string template_key = null) {
|
||||
var param = new AlgoParamVM<TValue>(prop_name, DisplayNameHelper.snake_to_title_case(prop_name), get_local(), template_key) {
|
||||
permissions = permissions
|
||||
};
|
||||
_algo_params.Add(param);
|
||||
_auto_props[prop_name] = new AutoProp(prop_name,
|
||||
def => {
|
||||
@@ -816,12 +830,11 @@ namespace mbts.spark {
|
||||
var default_algo = AlgoKey.from_known_type(OrderDataHelper.get_default_algo(OrderDataHelper.get_algo_format(security.Value.ContractCase)));
|
||||
foreach (var sd in _child_algo_selectors)
|
||||
sd.load_algo_data_recursive(algo_data, default_algo);
|
||||
Disposable.replace(
|
||||
ref _selector_data_hooks,
|
||||
ObservableAutoWrapper.connect_item_hooks(child_algo_selectors, s => {
|
||||
s.load_algo_data_recursive(algo_data, default_algo);
|
||||
return Disposable.empty;
|
||||
}));
|
||||
Disposable.dispose(ref _selector_data_hooks);
|
||||
_selector_data_hooks = ObservableAutoWrapper.connect_item_hooks(child_algo_selectors, s => {
|
||||
s.load_algo_data_recursive(algo_data, default_algo);
|
||||
return Disposable.empty;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,9 @@ namespace mbts.spark {
|
||||
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) {
|
||||
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) {
|
||||
@@ -200,6 +202,13 @@ namespace mbts.spark {
|
||||
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, uint 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(
|
||||
@@ -243,7 +252,7 @@ namespace mbts.spark {
|
||||
order_type.ToString().ToUpper();
|
||||
|
||||
public static string display_name(this OrderSecurityAlgo order_sec_algo) =>
|
||||
display_name(order_sec_algo.order, order_sec_algo.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();
|
||||
|
||||
@@ -59,23 +59,30 @@ namespace mbts.spark {
|
||||
this.md_auxiliary_model = md_auxiliary_model;
|
||||
var scheduler = ThreadPoolScheduler.Instance;
|
||||
var interval = TimeSpan.FromMilliseconds(100);
|
||||
on_orders =
|
||||
_service
|
||||
.subscribei_orders()
|
||||
.Naggle(interval, scheduler)
|
||||
.PermissionFilter(admin_model.on_admin_data)
|
||||
.join(sstore, order => order.security_key())
|
||||
on_orders = admin_model
|
||||
.permission_filter(
|
||||
_service.subscribei_orders()
|
||||
.Naggle(interval, scheduler),
|
||||
o => o.account_id()
|
||||
).join(sstore, order => order.security_key())
|
||||
.Table()
|
||||
.Cache();
|
||||
|
||||
on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).PermissionFilter(admin_model.on_admin_data).join(sstore, pos => pos.SecurityId).Table().Cache();
|
||||
on_pnl = _position_service.subscribei_pnl_data()
|
||||
.Naggle(interval, scheduler)
|
||||
on_positions = admin_model
|
||||
.permission_filter(
|
||||
_position_service.subscribei_position_data().Naggle(interval, scheduler),
|
||||
p => p.AccountId)
|
||||
.join(sstore, pos => pos.SecurityId)
|
||||
.Table()
|
||||
.Cache();
|
||||
on_pnl = admin_model
|
||||
.permission_filter(
|
||||
_position_service.subscribei_pnl_data().Naggle(interval, scheduler),
|
||||
p => p.Pos.AccountId)
|
||||
.join(sstore, p => p.Pos.SecurityId)
|
||||
.TableCheckedAutoCorrect(EqualityComparer<(SecurityImmutable? sec, PNLUnitImmutable data)>.Default)
|
||||
.Where(t => t.has_changes())
|
||||
.Cache();
|
||||
on_account_pnl = _position_service.subscribei_pnl_account_data().Naggle(interval, scheduler).Table().Cache();
|
||||
var working_orders = on_orders.Filter(tup => tup.order.is_active());
|
||||
this.working_orders =
|
||||
on_orders.Scan(
|
||||
@@ -174,10 +181,11 @@ namespace mbts.spark {
|
||||
.Where(t => t.has_changes())
|
||||
.Cache();
|
||||
// fill tuples
|
||||
var on_fills =
|
||||
_service.subscribei_fills()
|
||||
.Naggle(interval, scheduler)
|
||||
.PermissionFilter(admin_model.on_admin_data)
|
||||
var on_fills = admin_model
|
||||
.permission_filter(
|
||||
_service.subscribei_fills()
|
||||
.Naggle(interval, scheduler),
|
||||
f => f.Account)
|
||||
.join(sstore, f => f.SecurityKey)
|
||||
.Table().Cache();
|
||||
var fill_order_tuples_join = on_fills.LeftJoin(
|
||||
@@ -202,6 +210,7 @@ namespace mbts.spark {
|
||||
// manual fills
|
||||
on_manual_fills_securities = _position_service.subscribei_manual_fills()
|
||||
.Naggle(interval, scheduler)
|
||||
.Filter(f => f.FillType == ManualFill.Types.FillType.Local)
|
||||
.join(sstore, mf => mf.SecurityKey)
|
||||
.Table().Cache();
|
||||
var manual_fill_tuples_account_join = on_manual_fills_securities.LeftJoin(
|
||||
@@ -241,7 +250,6 @@ namespace mbts.spark {
|
||||
.Filter(os => os.order.is_active() && os.order.State.DisplayStatus == OrderState.Types.DisplayStatus.Visible, key => key.order.Key)
|
||||
.Subscribe(_security_working_orders_with_piq_demuxer),
|
||||
on_positions.Subscribe(_security_positions_demuxer),
|
||||
_bus.subscribe(SparkTags.CreateOrder).Subscribe(a => send_order(a.definition, a.price, a.size).ignore()),
|
||||
_bus.subscribe(SparkTags.RecreateOrder).Subscribe(a => a.ForEach(x => send_order(x.definition, x.price, x.size).ignore())),
|
||||
_bus.subscribe(SparkTags.CancelOrders).Subscribe(x => cancel_orders(x).ignore()),
|
||||
_bus.subscribe(SparkTags.DetachOrders).Subscribe(x => detach_orders(x).ignore()),
|
||||
@@ -266,7 +274,6 @@ namespace mbts.spark {
|
||||
public IObservable<IReadOnlyDictionary<uint, OrderSecurity>> working_orders { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PNLUnitImmutable), int>> on_pnl { get; }
|
||||
public IObservable<TableUpdate<PNLRowData, PNLKey>> security_account_pnl { get; }
|
||||
public IObservable<TableUpdate<AccountPNLImmutable, uint>> on_account_pnl { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, PositionUnitImmutable), int>> on_positions { get; }
|
||||
public IObservable<TableUpdate<(AlgoData algo, AlgoDefinitionMetadata? meta), AlgoKey>> studio_algos_with_meta { get; }
|
||||
public IObservable<TableUpdate<(SecurityImmutable?, ManualFillImmutable), int>> on_manual_fills_securities { get; }
|
||||
@@ -287,13 +294,33 @@ namespace mbts.spark {
|
||||
#endregion
|
||||
|
||||
#region Input
|
||||
public async Task<uint?> send_order(OrderDefinitionImmutable? order, double price, uint size) {
|
||||
// the security arg is needed for a temporary hack to prevent GlbxIceberg orders from going out in non-glbx exchanges (like Ice)
|
||||
// eventually, we'll need an algo type model to handle this stuff.
|
||||
// This hack leverages the fact that every place that calls 'send_order' already has the security handy (not just the sec id).
|
||||
public Task<uint?> send_order(SecurityImmutable security, OrderDefinitionImmutable? order, double price, uint size) {
|
||||
if (order.HasValue) {
|
||||
var result = (await send_order_get_error(order.Value, price, size));
|
||||
_activity.on_send_order(order.Value, price, size, result.key, result.error);
|
||||
return result.key;
|
||||
#if DEBUG
|
||||
if (security.Exchange != Venue.Glbx && security.Exchange != Venue.Sim) {
|
||||
#else
|
||||
if (security.Exchange != Venue.Glbx) {
|
||||
#endif
|
||||
var algo_structure = OrderDataHelper.enumerate_algo_structure(order.Value.CustomDef);
|
||||
if (algo_structure.Any(t => t.type == CustomOrderDefinition.CustomOneofCase.GlbxIceberg)) {
|
||||
Log.Ui.info($"OrdersModel prevented a GlbxIceberg order from going out to a non-glbx exchange ({OrderUtils.short_description(security, order.Value.Side, price, size)}).");
|
||||
return Task.FromResult<uint?>(null);
|
||||
}
|
||||
}
|
||||
return send_order(order.Value, price, size);
|
||||
}
|
||||
return 0;
|
||||
return Task.FromResult<uint?>(null);
|
||||
}
|
||||
|
||||
/// Note: this method is private to force outside callers to use this overload <see cref="send_order(SecurityImmutable, OrderDefinitionImmutable?, double, uint)"/>.
|
||||
/// This allows us to control which orders can be sent to which security/exchange (e.g., GlbxIceberg orders are only permitted for securities on the Glbx exchange).
|
||||
async Task<uint?> send_order(OrderDefinitionImmutable order, double price, uint size) {
|
||||
var result = (await send_order_get_error(order, price, size));
|
||||
_activity.on_send_order(order, price, size, result.key, result.error);
|
||||
return result.key;
|
||||
}
|
||||
|
||||
async Task<(uint? key, string error)> send_order_get_error(OrderDefinitionImmutable order, double price, uint size) {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.shell;
|
||||
using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace mbts {
|
||||
static class OrderSentEventPredicateVMFactory {
|
||||
public static Dictionary<OrderSentEventPredicate.PredicateTypeOneofCase, ITemplatedPropertyWrapperVM<OrderSentEventPredicate>> get_oneofs(IBus bus) =>
|
||||
new Dictionary<OrderSentEventPredicate.PredicateTypeOneofCase, ITemplatedPropertyWrapperVM<OrderSentEventPredicate>> {
|
||||
{ OrderSentEventPredicate.PredicateTypeOneofCase.AnyOrder, PredicateVMWrapper.create<OrderSentEventPredicate>(p => p.security = null) },
|
||||
{ OrderSentEventPredicate.PredicateTypeOneofCase.SecurityOrder, SecurityCriteriaVMFactory.create<OrderSentEventPredicate>(bus, o => o.security?.is_value ?? new SecurityCriteria(),
|
||||
(p, s) => {
|
||||
if (p.security == null) {
|
||||
p.security = new SecurityPredicate();
|
||||
}
|
||||
p.security.is_value = s;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class OrderSentEventPredicate : IEquatable<OrderSentEventPredicate> {
|
||||
static readonly int _type_hash = typeof(OrderSentEventPredicate).GetHashCode();
|
||||
// arbitrary short time period (can override in config)
|
||||
static readonly TimeSpan _stale_order_threshold = TimeSpan.FromSeconds(config.Configuration.get<bts.config.OrderAlerts>().stale_order_threshold_seconds);
|
||||
|
||||
public enum PredicateTypeOneofCase {
|
||||
[Description("(any order)")]
|
||||
AnyOrder,
|
||||
[Description("choose securities")]
|
||||
SecurityOrder
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public SecurityPredicate security { get; set; }
|
||||
|
||||
public PredicateTypeOneofCase predicate_type_case {
|
||||
get {
|
||||
if (security != null)
|
||||
return PredicateTypeOneofCase.SecurityOrder;
|
||||
return PredicateTypeOneofCase.AnyOrder;
|
||||
}
|
||||
}
|
||||
|
||||
public IPredicate<OrderSecurity> create(object obj) =>
|
||||
create_predicate((INotificationServiceDataCache)obj);
|
||||
|
||||
public IPredicate<OrderSecurity> create_predicate(INotificationServiceDataCache data_cache) {
|
||||
var sec_pred = security?.create();
|
||||
return PredicateBuilder.create<OrderSecurity>(os =>
|
||||
// verify security
|
||||
(sec_pred == null || (os.security.HasValue && sec_pred.trigger(os.security.Value))) &&
|
||||
// verify not stale
|
||||
((DateTime.Now - os.order.created()) < _stale_order_threshold) &&
|
||||
// verify order hasn't been added yet
|
||||
// Note: the notification service should offer a way to connect predicates so that they only receive items in TableUpdate.added,
|
||||
// however, the notification service predated TableUpdate.Changes, so for now, just check that the history.Count == 1)
|
||||
(data_cache.order_history.TryGetValue(os.order.Key, out var hist) && hist.Count == 1));
|
||||
}
|
||||
|
||||
public INotificationPredicate<OrderSecurity> create_toast(INotificationServiceDataCache data_cache) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public bool Equals(OrderSentEventPredicate other) =>
|
||||
other != null && other.security.safe_equals(other.security);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as OrderSentEventPredicate);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase(_type_hash, security?.GetHashCode() ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,9 @@ using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using OrderType = bts.api.orderman.CustomOrderDefinition.CustomOneofCase;
|
||||
using PTOC = mbts.OrderRecordTuplePredicate.PredicateTypeOneofCase;
|
||||
|
||||
namespace mbts {
|
||||
@@ -20,6 +15,7 @@ namespace mbts {
|
||||
// TODO: uncomment the remaining cases when ready
|
||||
public static Dictionary<PTOC, ITemplatedPropertyWrapperVM<OrderRecordTuplePredicate>> get_oneofs(IBus bus, OrdersModel orders_model) {
|
||||
return new Dictionary<PTOC, ITemplatedPropertyWrapperVM<OrderRecordTuplePredicate>> {
|
||||
{ PTOC.SentEvent, wrap(rp => rp.sent_event, (rp, p) => rp.sent_event = p, p => p.predicate_type_case, OrderSentEventPredicateVMFactory.get_oneofs(bus)) },
|
||||
{ PTOC.FilledEvent, wrap(rp => rp.filled_event, (rp, p) => rp.filled_event = p, p => p.predicate_type_case, OrderFilledEventPredicateVMFactory.get_oneofs(bus)) },
|
||||
{ PTOC.CompletedEvent, wrap(rp => rp.completed_event, (rp, p) => rp.completed_event = p, p => p.predicate_type_case, OrderCompletedEventPredicateVMFactory.get_oneofs(bus)) },
|
||||
{ PTOC.CanceledEvent, wrap(rp => rp.canceled_event, (rp, p) => rp.canceled_event = p, p => p.predicate_type_case, OrderCanceledEventPredicateVMFactory.get_oneofs(bus)) },
|
||||
@@ -77,6 +73,7 @@ namespace mbts {
|
||||
OrderFilledEventPredicate _filled_event;
|
||||
OrderCompletedEventPredicate _completed_event;
|
||||
OrderCanceledEventPredicate _canceled_event;
|
||||
OrderSentEventPredicate _sent_event;
|
||||
|
||||
public enum PredicateTypeOneofCase {
|
||||
None,
|
||||
@@ -120,6 +117,8 @@ namespace mbts {
|
||||
OrderKey,
|
||||
[PredicateCase(is_toastable:false)]
|
||||
ParentKey,
|
||||
[PredicateCase("When order is sent", PredicateCategory.Event)]
|
||||
SentEvent,
|
||||
[PredicateCase("When order gets a fill", PredicateCategory.Event)]
|
||||
FilledEvent,
|
||||
[PredicateCase("When order is completed", PredicateCategory.Event)]
|
||||
@@ -176,6 +175,8 @@ namespace mbts {
|
||||
public OrderCompletedEventPredicate completed_event { get => _completed_event; set => _set(ref _completed_event, value); }
|
||||
[ProtoMember(23)]
|
||||
public OrderCanceledEventPredicate canceled_event { get => _canceled_event; set => _set(ref _canceled_event, value); }
|
||||
[ProtoMember(26)]
|
||||
public OrderSentEventPredicate sent_event { get => _sent_event; set => _set(ref _sent_event, value); }
|
||||
[ProtoMember(24)]
|
||||
public bool is_toast_enabled { get; set; }
|
||||
[ProtoMember(25)]
|
||||
@@ -206,6 +207,7 @@ namespace mbts {
|
||||
if (_filled_event != null) return PTOC.FilledEvent;
|
||||
if (_completed_event != null) return PTOC.CompletedEvent;
|
||||
if (_canceled_event != null) return PTOC.CanceledEvent;
|
||||
if (_sent_event != null) return PTOC.SentEvent;
|
||||
if (_attached_target_order_key != null) return PTOC.AttachedTargetOrderKey;
|
||||
return PTOC.None;
|
||||
}
|
||||
@@ -236,6 +238,7 @@ namespace mbts {
|
||||
case PTOC.FilledEvent: return _filled_event.create_predicate(data_cache);
|
||||
case PTOC.CompletedEvent: return _completed_event.create_predicate(data_cache);
|
||||
case PTOC.CanceledEvent: return _canceled_event.create(data_cache);
|
||||
case PTOC.SentEvent: return _sent_event.create(data_cache);
|
||||
case PTOC.AttachedTargetOrderKey: return wrap(_attached_target_order_key.create(), r => r.order.attached_target_order_key());
|
||||
default: return null;
|
||||
}
|
||||
@@ -285,6 +288,7 @@ namespace mbts {
|
||||
case PTOC.FilledEvent: return _filled_event.Equals(other._filled_event);
|
||||
case PTOC.CompletedEvent: return _completed_event.Equals(other._completed_event);
|
||||
case PTOC.CanceledEvent: return _canceled_event.Equals(other._canceled_event);
|
||||
case PTOC.SentEvent: return _sent_event.Equals(other._sent_event);
|
||||
case PTOC.AttachedTargetOrderKey: return _attached_target_order_key.Equals(other._attached_target_order_key);
|
||||
case PTOC.None: return true;
|
||||
default: throw ExceptionHelper.unexpected_oneof_case(ptoc);
|
||||
@@ -320,6 +324,7 @@ namespace mbts {
|
||||
case PTOC.FilledEvent: raw_hash = _filled_event.GetHashCode(); break;
|
||||
case PTOC.CompletedEvent: raw_hash = _completed_event.GetHashCode(); break;
|
||||
case PTOC.CanceledEvent: raw_hash = _canceled_event.GetHashCode(); break;
|
||||
case PTOC.SentEvent: raw_hash = _sent_event.GetHashCode(); break;
|
||||
case PTOC.AttachedTargetOrderKey: raw_hash = _attached_target_order_key.GetHashCode(); break;
|
||||
case PTOC.None: raw_hash = 0; break;
|
||||
default: throw ExceptionHelper.unexpected_oneof_case(ptoc);
|
||||
@@ -355,6 +360,7 @@ namespace mbts {
|
||||
_filled_event = null;
|
||||
_completed_event = null;
|
||||
_canceled_event = null;
|
||||
_sent_event = null;
|
||||
_attached_target_order_key = null;
|
||||
}
|
||||
field = value;
|
||||
|
||||
@@ -339,7 +339,7 @@
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="Padding" Value="3,3,1,3"/>
|
||||
<Setter Property="Padding" Value="6,3,1,3"/>
|
||||
<Setter Property="ScrollViewer.CanContentScroll" Value="true"/>
|
||||
<Setter Property="ScrollViewer.PanningMode" Value="Both"/>
|
||||
<Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
|
||||
|
||||
@@ -8,6 +8,7 @@ using mbts.spark;
|
||||
using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace bts.spark {
|
||||
@@ -41,6 +42,9 @@ namespace bts.spark {
|
||||
SparkCommand.create("P&L (2.0)", Icons.Accounting, SparkTags.ShowPNLTag);
|
||||
|
||||
// Ladder
|
||||
public static PartialCommand<IImmutableSet<uint>> ShowLadderAutoLookup(KeyGesture key_gesture = null) =>
|
||||
SparkCommand.create("Ladder", Icons.Ladder, SparkTags.ShowLadderAutoLookupTag, key_gesture);
|
||||
|
||||
public static PartialCommand<SecurityMD> ShowLadder(KeyGesture key_gesture = null) =>
|
||||
SparkCommand.create("Ladder", Icons.Ladder, SparkTags.ShowLadderTag, key_gesture);
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ using bts.spark;
|
||||
using mbts.ui;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark {
|
||||
@@ -17,7 +18,6 @@ namespace mbts.spark {
|
||||
#endregion
|
||||
|
||||
#region Order
|
||||
public static ITag<(OrderDefinitionImmutable definition, double price, uint size)> CreateOrder { get; } = new SparkTag<(OrderDefinitionImmutable, double, uint)>();
|
||||
public static ITag<IEnumerable<(OrderDefinitionImmutable definition, double price, uint size)>> RecreateOrder { get; } = new SparkTag<IEnumerable<(OrderDefinitionImmutable, double, uint)>>();
|
||||
|
||||
public static ITag<IEnumerable<uint>> CancelOrders { get; } = new SparkTag<IEnumerable<uint>>();
|
||||
@@ -56,6 +56,8 @@ namespace mbts.spark {
|
||||
// The owners of the various SparkWindows may remove dependencies on the former at their leasure.
|
||||
// For now, both tags go through the same logic in the LadderWindowFactory anyways.
|
||||
public static ITag<SecurityMD> ShowLadderTag { get; } = new SparkTag<SecurityMD>("Show Ladder");
|
||||
// TODO: get rid of the SecurityMD ShowLadderTag. This has created burdensome dependency issues.
|
||||
public static ITag<IImmutableSet<uint>> ShowLadderAutoLookupTag { get; } = new SparkTag<IImmutableSet<uint>>("Show Ladder Auto Lookup");
|
||||
public static ITag<IReadOnlyList<SecurityMD>> ShowLaddersTag { get; } = new SparkTag<IReadOnlyList<SecurityMD>>(nameof(ShowLaddersTag));
|
||||
public static ITag<IObservable<SecurityMD>> ShowLinkedLadder { get; } = new SparkTag<IObservable<SecurityMD>>("Linked Ladder");
|
||||
public static ITag<SecurityCriteria> ShowMarketViewTag { get; } = new SparkTag<SecurityCriteria>("Show MarketView");
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts {
|
||||
public static class DoubleHelper {
|
||||
public static bool any_nan_or_infinity(params double[] values) {
|
||||
for (int i = 0; i < values.Length; i++) {
|
||||
if (double.IsNaN(values[i]) || double.IsInfinity(values[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool are_close(double v1, double v2, double threshold) =>
|
||||
!any_nan_or_infinity(v1, v2, threshold) && Math.Abs(threshold) > Math.Abs(v1 - v2);
|
||||
|
||||
// bc double.IsNaN gets annoying write, and a lazy programmer is a happy programmer ;)
|
||||
public static bool is_nan(this double value) =>
|
||||
double.IsNaN(value);
|
||||
|
||||
/// <summary>
|
||||
/// If the specified value is a <see cref="double"/>, <see cref="int"/>, or <see cref="uint"/>, returns the equivalent <see cref="double"/>; otherwise, returns <see cref="double.NaN"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to convert to a <see cref="double"/>.</param>
|
||||
/// <returns>The <see cref="double"/> equivalent value if the cast succeeds, otherwise <see cref="double.NaN"/>.</returns>
|
||||
public static double nan_cast(object value) {
|
||||
// devs: add numeric type checks as needed
|
||||
if (value is double d) return d;
|
||||
if (value is int i) return i;
|
||||
if (value is uint u) return u;
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
public static bool IsNaNOrInfinity(double d) => double.IsNaN(d) || double.IsInfinity(d);
|
||||
public static bool IsNaNOrInfinityOr(double d, params double[] values) => IsNaNOrInfinity(d) || values.Contains(d);
|
||||
public static bool IsOneOf(double d, params double[] values) => values.Contains(d);
|
||||
|
||||
@@ -11,6 +11,7 @@ using bts.api.persistence;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Reactive.Linq;
|
||||
using bts.spark;
|
||||
using bts;
|
||||
|
||||
namespace mbts.persist
|
||||
{
|
||||
@@ -146,16 +147,18 @@ namespace mbts.persist
|
||||
}
|
||||
|
||||
[Export(typeof(IUserPersist))]
|
||||
public class UserUISettings : UISettingsBase
|
||||
{
|
||||
public class UserUISettings : UISettingsBase {
|
||||
string _user;
|
||||
|
||||
[ImportingConstructor]
|
||||
public UserUISettings([Import("userid")]uint user, [Import("settings")]PersistenceService persist, ErrorReporter err)
|
||||
public UserUISettings(uint user_id, [Import("settings")]PersistenceService persist, ErrorReporter err)
|
||||
: base(persist,err) {
|
||||
_user = $"User {user}";
|
||||
_user = $"User {user_id}";
|
||||
}
|
||||
|
||||
[ImportingConstructor]
|
||||
UserUISettings(UserTuple user, [Import("settings")]PersistenceService persist, ErrorReporter err)
|
||||
: this(user.id, persist, err) { }
|
||||
|
||||
protected override string user() { return _user; }
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ namespace bts.settings {
|
||||
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
|
||||
interface ISyntheticManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
|
||||
static class SettingsMigrationHelper {
|
||||
public static bool apply<T>(T settings, IEnumerable<ISettingsMigrationRule<T>> migration_rules, out T new_settings) {
|
||||
|
||||
@@ -25,7 +25,8 @@ namespace mbts.persist {
|
||||
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)),
|
||||
_pandl_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.pnl_grid, (us, gs) => us.with(provided_pnl_gridsettings: gs));
|
||||
_pandl_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.pnl_grid, (us, gs) => us.with(provided_pnl_gridsettings: gs)),
|
||||
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs));
|
||||
|
||||
static readonly IImmutableProperty<UserSettings, PNLSettings>
|
||||
_pnl_propi = ImmutableProperty.create<UserSettings, PNLSettings>(us => us.pnl, (us, pnl) => us.with(pnl: pnl));
|
||||
@@ -36,6 +37,7 @@ namespace mbts.persist {
|
||||
readonly IEnumerable<IMarketViewGridSettingsMigrationRule> _mview_migration_rules;
|
||||
readonly IEnumerable<IPAndLGridSettingsMigrationRule> _pandl_migration_rules;
|
||||
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
|
||||
readonly IEnumerable<ISyntheticManagementMigrationRule> _synth_migration_rules;
|
||||
readonly IUserPersist _persist;
|
||||
|
||||
UserSettings _default_settings;
|
||||
@@ -46,13 +48,15 @@ namespace mbts.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<IPNLSettingsMigrationRule> pnl_migration_rules,
|
||||
[ImportMany]IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules) {
|
||||
//
|
||||
_persist = persist;
|
||||
_fills_migration_rules = fills_migration_rules ?? Enumerable.Empty<IFillsGridSettingsMigrationRule>();
|
||||
_mview_migration_rules = mview_migration_rules ?? Enumerable.Empty<IMarketViewGridSettingsMigrationRule>();
|
||||
_pandl_migration_rules = pandl_migration_rules ?? Enumerable.Empty<IPAndLGridSettingsMigrationRule>();
|
||||
_pnl_migration_rules = pnl_migration_rules ?? Enumerable.Empty<IPNLSettingsMigrationRule>();
|
||||
_synth_migration_rules = synth_migration_rules ?? Enumerable.Empty<ISyntheticManagementMigrationRule>();
|
||||
init();
|
||||
async void init() {
|
||||
UserSettings s;
|
||||
@@ -62,7 +66,9 @@ namespace mbts.persist {
|
||||
var wrapped_rules = new[] {
|
||||
_fills_migration_rules.merge().wrap(_fills_propi),
|
||||
_mview_migration_rules.merge().wrap(_mview_propi),
|
||||
_pandl_migration_rules.merge().wrap(_pandl_propi)
|
||||
_pandl_migration_rules.merge().wrap(_pandl_propi),
|
||||
_pnl_migration_rules.merge().wrap(_pnl_propi),
|
||||
_synth_migration_rules.merge().wrap(_synth_propi)
|
||||
};
|
||||
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
|
||||
// TODO: move ladder sanitization logic into ISettingsMigrationRule
|
||||
@@ -136,6 +142,12 @@ namespace mbts.persist {
|
||||
_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 async Task save_default(UserSettings value) {
|
||||
// notify observers before persisting so the changes are reflected immediately in the ui
|
||||
lock (_default_settings_lock) {
|
||||
|
||||
@@ -15,12 +15,8 @@ namespace bts.utils {
|
||||
}
|
||||
|
||||
static class ObjectSnapshot {
|
||||
public static IEnumerable<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>() where TSource : class where TValue : class {
|
||||
var tval = typeof(TValue);
|
||||
return typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => tval.IsAssignableFrom(p.PropertyType))
|
||||
.Select(_prop_getter<TSource, TValue>);
|
||||
}
|
||||
public static IReadOnlyList<Func<TSource, TValue>> property_delegates_of_type<TSource, TValue>() =>
|
||||
PropertyGetterStore<TSource, TValue>.value;
|
||||
|
||||
public static IObservable<TableUpdate<(string name, object current, object original), string>> property_table<T>(T instance, params string[] prop_names) where T : INotifyPropertyChanged =>
|
||||
_property_table(instance, Array.ConvertAll(prop_names, PropertyInfoStore<T>.type.GetProperty));
|
||||
@@ -162,6 +158,18 @@ namespace bts.utils {
|
||||
}
|
||||
}
|
||||
|
||||
static class PropertyGetterStore<TSource, TValue> {
|
||||
public static readonly IReadOnlyList<Func<TSource, TValue>> value;
|
||||
|
||||
static PropertyGetterStore() {
|
||||
var tval = typeof(TValue);
|
||||
value = typeof(TSource).GetProperties(BindingFlags.Public | BindingFlags.Instance)
|
||||
.Where(p => tval.IsAssignableFrom(p.PropertyType))
|
||||
.Select(_prop_getter<TSource, TValue>)
|
||||
.ToReadOnlyList();
|
||||
}
|
||||
}
|
||||
|
||||
class PropNode {
|
||||
public readonly PropertyInfo prop;
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using bts.api;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
namespace bts {
|
||||
struct SyntheticTuple : IEquatable<SyntheticTuple>, IComparable<SyntheticTuple>, IComparable {
|
||||
public static readonly Func<SyntheticTuple, uint> key_fn = a => a.security.Id;
|
||||
public static readonly Func<SyntheticTuple?, uint> nullable_key_fn = a => a.HasValue ? a.Value.security.Id : 0u;
|
||||
|
||||
public readonly SecurityImmutable security;
|
||||
public readonly string user_name;
|
||||
public readonly ExecutionStrategyImmutable? strategy;
|
||||
public readonly int? num_working_orders;
|
||||
|
||||
public SyntheticTuple(SecurityImmutable security, string user_name, ExecutionStrategyImmutable? strategy, int? num_working_orders = null) {
|
||||
this.security = security;
|
||||
this.user_name = user_name;
|
||||
this.strategy = strategy;
|
||||
this.num_working_orders = num_working_orders;
|
||||
}
|
||||
|
||||
public int CompareTo(SyntheticTuple other) =>
|
||||
SecurityHelper.Comparer.Compare(security, other.security);
|
||||
|
||||
int IComparable.CompareTo(object obj) =>
|
||||
obj is SyntheticTuple other ? CompareTo(other) : 1;
|
||||
|
||||
public bool Equals(SyntheticTuple other) =>
|
||||
security == other.security &&
|
||||
user_name == other.user_name &&
|
||||
EqualityComparer.nullable_equals(strategy, other.strategy);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
EqualityComparer.equals_obj(this, obj);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
security.Id.GetHashCode();
|
||||
|
||||
public override string ToString() =>
|
||||
user_name == null ? security.ToString() : $"{user_name}:{security}";
|
||||
|
||||
public static bool operator ==(SyntheticTuple a, SyntheticTuple b) => a.Equals(b);
|
||||
public static bool operator !=(SyntheticTuple a, SyntheticTuple b) => !a.Equals(b);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.ui.controls
|
||||
{
|
||||
internal class DoubleUtil
|
||||
{
|
||||
namespace mbts.ui.controls {
|
||||
static class DoubleUtil {
|
||||
public const int API_DECIMAL_PRECISION = 9;
|
||||
// arbitrary small number that can be used to determine whether or not scroll changes are significant
|
||||
public const double MIN_SCROLL_DELTA = 0.000015;
|
||||
/* smallest such that 1.0+DBL_EPSILON != 1.0 */
|
||||
private const double EPSILON = 2.2204460492503131e-016;
|
||||
private const double BIG_EPSILON = EPSILON * 10;
|
||||
@@ -36,6 +37,9 @@ namespace mbts.ui.controls
|
||||
(double.IsNaN(right) ? double.NaN : -right) :
|
||||
(double.IsNaN(right) ? left : left - right);
|
||||
|
||||
public static decimal round_to_decimal(double value, int decimals = API_DECIMAL_PRECISION) =>
|
||||
double.IsNaN(value) ? 0 : (decimal)Math.Round(value, decimals, MidpointRounding.AwayFromZero);
|
||||
|
||||
internal static bool AreClose(double v1, double v2) {
|
||||
if (v1 == v2) return true;
|
||||
double delta = Math.Abs(v1 - v2);
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
using System;
|
||||
using bts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.spark.filters {
|
||||
static class DataGridFilterHelper {
|
||||
public static IDisposable hook_filters<TRow>(ICollectionView rows, params IDataGridColumnFilter<TRow>[] filters) {
|
||||
public static IDisposable hook_all<TSource, TRow>(TSource source, ICollectionView rows) =>
|
||||
hook_filters(rows,
|
||||
ObjectSnapshot.property_delegates_of_type<TSource, IDataGridColumnFilter<TRow>>()
|
||||
.Select(f => f(source))
|
||||
.ExceptNull()
|
||||
.ToList());
|
||||
|
||||
public static IDisposable hook_filters<TRow>(ICollectionView rows, params IDataGridColumnFilter<TRow>[] filters) =>
|
||||
hook_filters(rows, filters.AsEnumerable());
|
||||
|
||||
public static IDisposable hook_filters<TRow>(ICollectionView rows, IEnumerable<IDataGridColumnFilter<TRow>> filters) {
|
||||
var icvls = (ICollectionViewLiveShaping)rows;
|
||||
icvls.IsLiveFiltering = true;
|
||||
return _hook_filters(set_filter, filters);
|
||||
@@ -22,7 +33,7 @@ namespace mbts.spark.filters {
|
||||
}
|
||||
}
|
||||
|
||||
static IDisposable _hook_filters<TRow>(Action set_filter, params IDataGridColumnFilter<TRow>[] filters) {
|
||||
static IDisposable _hook_filters<TRow>(Action set_filter, IEnumerable<IDataGridColumnFilter<TRow>> filters) {
|
||||
foreach (var f in filters)
|
||||
f.filter_changed += set_filter;
|
||||
set_filter();
|
||||
@@ -34,7 +45,7 @@ namespace mbts.spark.filters {
|
||||
});
|
||||
}
|
||||
|
||||
static List<IDataGridColumnFilter<TRow>> _get_active_filters<TRow>(IDataGridColumnFilter<TRow>[] filters) =>
|
||||
static List<IDataGridColumnFilter<TRow>> _get_active_filters<TRow>(IEnumerable<IDataGridColumnFilter<TRow>> filters) =>
|
||||
filters.Where(f => f.active.HasValue && f.active.Value).ToList();
|
||||
}
|
||||
|
||||
|
||||
@@ -68,10 +68,11 @@ namespace mbts.spark.filters {
|
||||
Func<TRow, TValue> factory,
|
||||
Func<TValue, TKey> key,
|
||||
out IObserver<TableUpdate<TValue, TKey>> items_observer,
|
||||
out ListCollectionView<TValue> items_view)
|
||||
out ListCollectionView<TValue> items_view,
|
||||
string prop_name = null)
|
||||
where TKey : struct, IEquatable<TKey> =>
|
||||
//
|
||||
CreateTableUpdate(factory, key, keys => keys, keys => keys.ToArray(), out items_observer, out items_view);
|
||||
CreateTableUpdate(factory, key, keys => keys, keys => keys.ToArray(), out items_observer, out items_view, prop_name);
|
||||
|
||||
public static IDataGridColumnPersistentFilter<TRow, TState> CreateTableUpdate<TRow, TValue, TKey, TState>(
|
||||
Func<TRow, TValue> factory,
|
||||
@@ -79,10 +80,11 @@ namespace mbts.spark.filters {
|
||||
Func<TState, IEnumerable<TKey>> load_state,
|
||||
Func<IEnumerable<TKey>, TState> get_state,
|
||||
out IObserver<TableUpdate<TValue, TKey>> items_observer,
|
||||
out ListCollectionView<TValue> items_view)
|
||||
out ListCollectionView<TValue> items_view,
|
||||
string prop_name = null)
|
||||
where TKey : struct, IEquatable<TKey> {
|
||||
//
|
||||
var filter = new TableUpdateItemsFilter<TRow, TValue, TKey, TState>(factory, key, load_state, get_state);
|
||||
var filter = new TableUpdateItemsFilter<TRow, TValue, TKey, TState>(factory, key, load_state, get_state, prop_name);
|
||||
items_observer = filter;
|
||||
items_view = filter.items_view;
|
||||
return filter;
|
||||
@@ -291,8 +293,8 @@ namespace mbts.spark.filters {
|
||||
readonly HashSet<TKey> _selected_keys = new HashSet<TKey>(), _desired_keys = new HashSet<TKey>();
|
||||
ListCollectionView<TValue> _items_view;
|
||||
|
||||
public TableUpdateItemsFilter(Func<TRow, TValue> factory, Func<TValue, TKey> key, Func<TState, IEnumerable<TKey>> load_state, Func<IEnumerable<TKey>, TState> get_state)
|
||||
: base(factory, null) {
|
||||
public TableUpdateItemsFilter(Func<TRow, TValue> factory, Func<TValue, TKey> key, Func<TState, IEnumerable<TKey>> load_state, Func<IEnumerable<TKey>, TState> get_state, string prop_name)
|
||||
: base(factory, prop_name) {
|
||||
_factory = factory;
|
||||
_key = key;
|
||||
_load_state = load_state;
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace mbts.ui {
|
||||
public TimeTextTuple end => _end;
|
||||
public bool is_active =>
|
||||
_mode == TimeFilterMode.Today ||
|
||||
_mode == TimeFilterMode.CMEOpen ||
|
||||
(_mode == TimeFilterMode.Period && (_begin.result.HasValue || _end.result.HasValue));
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
using bts.support;
|
||||
using Google.Protobuf;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
@@ -18,6 +12,13 @@ using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
using bts.support;
|
||||
using Google.Protobuf;
|
||||
using mbts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
|
||||
namespace bts.view.controls {
|
||||
partial class SupportRequestWindow : Window {
|
||||
@@ -50,28 +51,40 @@ namespace bts.view.controls {
|
||||
_base = report_base;
|
||||
send_command = new DelegateCommand(send_report, _ => !string.IsNullOrWhiteSpace(_text.Value));
|
||||
_text = backing(nameof(user_text), "");
|
||||
_text.Changed += (_,__) => send_command.TryRaiseCanExecuteChanged();
|
||||
_text.Changed += (_, __) => send_command.TryRaiseCanExecuteChanged();
|
||||
_contact = backing(nameof(user_contact), "");
|
||||
_contact.Changed += (_,__) => send_command.TryRaiseCanExecuteChanged();
|
||||
_contact.Changed += (_, __) => send_command.TryRaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
async void send_report(object _) {
|
||||
ByteString screenshot = null;
|
||||
if (include_screenshot) {
|
||||
var stream = ScreenCapture.capture_encoded("jpeg");
|
||||
screenshot = await ByteString.FromStreamAsync(stream);
|
||||
}
|
||||
var reply = await _service.send_report_async(_base.with(
|
||||
user_description: _text.Value,
|
||||
contact: user_contact,
|
||||
log: include_logs ? LogUtil.read_logs() : "",
|
||||
image: screenshot
|
||||
));
|
||||
if (reply.success) {
|
||||
BTSMessageBox.Show("Your message has been received.", "Thank You", BTSMessageBoxButtons.OK, MessageBoxImage.Information, is_modal: false, top_most:true);
|
||||
}
|
||||
else {
|
||||
BTSMessageBox.Show($"There was an error submitting your message. '{reply.error_message}'", "Error", BTSMessageBoxButtons.OK, MessageBoxImage.Error, is_modal: false, top_most: true);
|
||||
using (var d = new BusyDialog { Text = "Sending your error report...", Topmost = true }) {
|
||||
d.Show();
|
||||
ByteString screenshot = null;
|
||||
if (include_screenshot) {
|
||||
var stream = ScreenCapture.capture_encoded("jpeg");
|
||||
screenshot = await ByteString.FromStreamAsync(stream);
|
||||
}
|
||||
var request = await Task.Run(() => {
|
||||
var result = _base.with(
|
||||
user_description: _text.Value,
|
||||
contact: user_contact,
|
||||
image: screenshot);
|
||||
if (include_logs) {
|
||||
result = spark.ErrorReporter.append_logs(result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
|
||||
var reply = await _service.send_report_async(request);
|
||||
if (reply.success) {
|
||||
Log.Ui.info("Support request message has been received.");
|
||||
BTSMessageBox.Show("Your message has been received.", "Thank You", BTSMessageBoxButtons.OK, MessageBoxImage.Information, is_modal: false, top_most: true);
|
||||
}
|
||||
else {
|
||||
var errmsg = $"There was an error submitting your message. '{reply.error_message}'";
|
||||
Log.Ui.error(errmsg);
|
||||
BTSMessageBox.Show(errmsg, "Error", BTSMessageBoxButtons.OK, MessageBoxImage.Error, is_modal: false, top_most: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1145,6 +1145,30 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.UIntTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.SecurityKeyTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"/>
|
||||
<c:ContractPickerPopup
|
||||
Grid.Column="2"
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding SelectedSecurity, RelativeSource={RelativeSource Self}, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
|
||||
IsEditMode="False"
|
||||
IsEnabled="{Binding DataContext.editing_permissions, RelativeSource={RelativeSource AncestorType=ItemsControl}, Converter={StaticResource CanEditValuesConverter}}"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurityKey="{Binding value}"
|
||||
ShowDialogSkirt="True"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.SecurityKeyTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.StringTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
@@ -1181,30 +1205,35 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.EnumTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"/>
|
||||
<uag:QuantityMeasureControl
|
||||
Grid.Column="2" Focusable="False" HorizontalAlignment="Stretch"
|
||||
QuantityMeasure="{Binding quantity, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl">
|
||||
<uag:QuantityMeasureControl.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
|
||||
</uag:QuantityMeasureControl.CommandBindings>
|
||||
<uag:QuantityMeasureControl.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
|
||||
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
|
||||
</uag:QuantityMeasureControl.InputBindings>
|
||||
</uag:QuantityMeasureControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"
|
||||
/>
|
||||
<uag:QuantityMeasureControl
|
||||
Grid.Column="2"
|
||||
Focusable="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
QuantityMeasure="{Binding quantity, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl"
|
||||
>
|
||||
<uag:QuantityMeasureControl.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
|
||||
</uag:QuantityMeasureControl.CommandBindings>
|
||||
<uag:QuantityMeasureControl.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
|
||||
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
|
||||
</uag:QuantityMeasureControl.InputBindings>
|
||||
</uag:QuantityMeasureControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector>
|
||||
</ItemsControl.ItemTemplateSelector>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -457,15 +457,15 @@
|
||||
Name="PART_TextBox"
|
||||
u:TextBoxBehavior.EnableArrowKeyTraverse="True"
|
||||
BorderThickness="0">
|
||||
<TextBox.Style>
|
||||
<TextBox.Style>
|
||||
<Style TargetType="{x:Type TextBox}">
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="FocusManager.FocusedElement" Value="{Binding RelativeSource={RelativeSource Self}}"></Setter>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</Style>
|
||||
</TextBox.Style>
|
||||
</TextBox>
|
||||
</c:TickUpDown>
|
||||
</ControlTemplate>
|
||||
|
||||
@@ -93,7 +93,6 @@
|
||||
u:TextBoxBehavior.EnableArrowKeyTraverse="True"
|
||||
u:Behavior.DisablePaste="True"
|
||||
Height="20"
|
||||
BorderThickness="0"
|
||||
>
|
||||
<TextBox.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static c:DateTimePicker.TickTimeComponentCommand}" CommandParameter="{u:Int 1}"/>
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace mbts.ui.converters {
|
||||
public static IValueConverter create<TIn, TOut>(Func<TIn, TOut> convert) =>
|
||||
new AnonymousValueConverter<TIn, TOut>(convert);
|
||||
|
||||
public static IValueConverter create<TIn, TOut>(Func<TIn, TOut> convert, Func<TOut, TIn> convert_back) =>
|
||||
new AnonymousValueConverter<TIn, TOut>(convert, convert_back);
|
||||
|
||||
public static IMultiValueConverter create<TIn1, TIn2, TOut>(Func<TIn1, TIn2, TOut> convert) =>
|
||||
new AnonymousMultiValueConverter<TIn1, TIn2, TOut>(convert);
|
||||
|
||||
@@ -28,9 +31,17 @@ namespace mbts.ui.converters {
|
||||
IntTextConverter = new IntTextConverter(),
|
||||
UIntTextConverter = new UIntTextConverter(),
|
||||
QuantityMeasureTextConverter = ValueConverter.create<QuantityMeasureImmutable?, string>(fmt),
|
||||
SecuritySelectorText,
|
||||
SimpleString = new AnonymousValueConverter(obj => obj?.ToString()),
|
||||
ThicknessToDouble = new ThicknessToDoubleConverter();
|
||||
|
||||
static ValueConverters() {
|
||||
SecuritySelectorText = new NullSubstitutionConverterWrapper {
|
||||
Converter = SimpleString,
|
||||
ValueForNull = "(None)"
|
||||
};
|
||||
}
|
||||
|
||||
public static readonly IMultiValueConverter
|
||||
AreEqualConverter = new AreEqualConverter();
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
class DictionaryLookupConverter : IMultiValueConverter {
|
||||
[ThreadStatic]
|
||||
Dictionary<Type, Lookup> _type_impls_backing;
|
||||
Dictionary<Type, Lookup> _type_impls => _type_impls_backing ?? (_type_impls_backing = new Dictionary<Type, Lookup>());
|
||||
|
||||
delegate bool Lookup(object source, object key, out object result);
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (values.Length == 2 && values[0] != null && values[1] != null) {
|
||||
var lookup_type = values[0].GetType();
|
||||
var lookup = _type_impls.GetOrAdd(lookup_type, TypeImplResolver.resolve);
|
||||
if (lookup(values[0], values[1], out var result))
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
static class TypeImplResolver {
|
||||
public static Lookup resolve(Type lookup_type) =>
|
||||
_prioritized_impls(lookup_type)
|
||||
.OrderBy(tup => tup.priority)
|
||||
.Select(tup => tup.impl)
|
||||
.FirstOrDefault() ?? _lookup_fail;
|
||||
|
||||
static bool _lookup_fail(object source, object key, out object result) {
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static IEnumerable<(int priority, Lookup impl)> _prioritized_impls(Type lookup_type) {
|
||||
foreach (var itype in lookup_type.GetInterfaces()) {
|
||||
if (itype.IsGenericType) {
|
||||
var gen_def = itype.GetGenericTypeDefinition();
|
||||
if (gen_def == typeof(IDictionary<,>))
|
||||
yield return (1, _get_generic_impl(nameof(_GenericImpls<int, int>.idictionary), itype));
|
||||
if (gen_def == typeof(IReadOnlyDictionary<,>))
|
||||
yield return (2, _get_generic_impl(nameof(_GenericImpls<int, int>.ireadonly_dictionary), itype));
|
||||
if (gen_def == typeof(ILookup<,>))
|
||||
yield return (3, _get_generic_impl(nameof(_GenericImpls<int, int>.ilookup), itype));
|
||||
}
|
||||
if (itype == typeof(IDictionary))
|
||||
yield return (4, _NonGenericImpls.idictionary);
|
||||
}
|
||||
}
|
||||
|
||||
static Lookup _get_generic_impl(string impl_name, Type impl_type) =>
|
||||
_get_generic_impl(impl_name, impl_type.GetGenericArguments());
|
||||
|
||||
static Lookup _get_generic_impl(string impl_name, Type[] key_value_types) =>
|
||||
(Lookup)typeof(_GenericImpls<,>)
|
||||
.MakeGenericType(key_value_types)
|
||||
.GetField(nameof(_GenericImpls<int, int>.idictionary))
|
||||
.GetValue(null);
|
||||
|
||||
static class _GenericImpls<TKey, TValue> {
|
||||
public static readonly Lookup
|
||||
idictionary = _idictionary,
|
||||
ireadonly_dictionary = _ireadonly_dictionary,
|
||||
ilookup = _ilookup;
|
||||
|
||||
static bool _idictionary(object lookup, object key, out object result) {
|
||||
if (key is TKey k && lookup is IDictionary<TKey, TValue> d && d.TryGetValue(k, out var value)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _ireadonly_dictionary(object lookup, object key, out object result) {
|
||||
if (key is TKey k && lookup is IReadOnlyDictionary<TKey, TValue> rod && rod.TryGetValue(k, out var value)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _ilookup(object lookup, object key, out object result) {
|
||||
if ((key is TKey k) && lookup is ILookup<TKey, TValue> l && l.Contains(k)) {
|
||||
result = l[k];
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class _NonGenericImpls {
|
||||
public static readonly Lookup
|
||||
idictionary = _idictionary;
|
||||
|
||||
static bool _idictionary(object lookup, object key, out object result) {
|
||||
var dict = (IDictionary)lookup;
|
||||
if (dict.Contains(key)) {
|
||||
result = dict[key];
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,29 @@ using CompositeDisposable = System.Reactive.Disposables.CompositeDisposable;
|
||||
|
||||
namespace mbts.ui.extensions {
|
||||
static class FrameworkElementHelper {
|
||||
/// <summary>
|
||||
/// Invokes the specified action immediately if the element is already loaded, otherwise hooks up to the <see cref="FrameworkElement.Loaded"/> event,
|
||||
/// and [conditionally]invokes the specified action if <paramref name="should_still_execute"/> returns true.
|
||||
/// </summary>
|
||||
/// <param name="element">The element that must be loaded before the specified action is invoked.</param>
|
||||
/// <param name="action">The action to invoke when the element is loaded.</param>
|
||||
/// <param name="should_still_execute">If the element is not already loaded, this delegate determines whether or not to invoke the action in the Loaded event callback.</param>
|
||||
/// <returns></returns>
|
||||
public static void InvokeOnLoaded(this FrameworkElement element, Action action, Func<bool> should_still_execute) {
|
||||
if (action == null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
if (element.IsLoaded) {
|
||||
action();
|
||||
} else {
|
||||
element.Loaded += on_loaded;
|
||||
void on_loaded(object sender, RoutedEventArgs e) {
|
||||
element.Loaded -= on_loaded;
|
||||
if (should_still_execute())
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Point GetCenter(this FrameworkElement element) =>
|
||||
new Point(element.ActualWidth * 0.5, element.ActualHeight * 0.5);
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.extensions;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace mbts.ui {
|
||||
static class ScrollViewerBehavior {
|
||||
public static readonly DependencyProperty VerticalOffsetProperty =
|
||||
DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), typeof(ScrollViewerBehavior), new PropertyMetadata(double.NaN, OnVerticalOffsetChanged));
|
||||
|
||||
static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (d is ScrollViewer scroll) {
|
||||
double voff = DoubleHelper.nan_cast(e.NewValue);
|
||||
// Ignore small deltas to prevent strange layout rounding issues/loops.
|
||||
// Be mindful that small deltas are not accumulated into a single larger delta.
|
||||
if (!double.IsNaN(voff) && !DoubleHelper.are_close(DoubleHelper.nan_cast(e.OldValue), voff, DoubleUtil.MIN_SCROLL_DELTA)) {
|
||||
scroll.InvokeOnLoaded(() => scroll.ScrollToVerticalOffset(voff),
|
||||
() => scroll.GetValue(VerticalOffsetProperty) is double curr_voff && curr_voff == voff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static double GetVerticalOffset(DependencyObject d) => (double)d.GetValue(VerticalOffsetProperty);
|
||||
public static void SetVerticalOffset(DependencyObject d, double value) => d.SetValue(VerticalOffsetProperty, value);
|
||||
|
||||
public static readonly DependencyProperty EnableTwoWayVerticalOffsetBindingProperty =
|
||||
DependencyProperty.RegisterAttached("EnableTwoWayVerticalOffsetBinding", typeof(bool), typeof(ScrollViewerBehavior), new PropertyMetadata(false, OnEnableTwoWayVerticalOffsetBindingChanged));
|
||||
|
||||
static void OnEnableTwoWayVerticalOffsetBindingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (d is ScrollViewer scroll) {
|
||||
if ((bool)e.NewValue) {
|
||||
scroll.ScrollChanged += EnableTwoWayVerticalOffsetBindingUtils.on_scroll_changed;
|
||||
} else {
|
||||
scroll.ScrollChanged -= EnableTwoWayVerticalOffsetBindingUtils.on_scroll_changed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool GetEnableTwoWayVerticalOffsetBinding(DependencyObject d) => (bool)d.GetValue(EnableTwoWayVerticalOffsetBindingProperty);
|
||||
public static void SetEnableTwoWayVerticalOffsetBinding(DependencyObject d, bool value) => d.SetValue(EnableTwoWayVerticalOffsetBindingProperty, value);
|
||||
|
||||
public static readonly DependencyProperty VerticalOffsetMirrorBusyProperty =
|
||||
DependencyProperty.RegisterAttached("VerticalOffsetMirrorBusy", typeof(bool), typeof(ScrollViewerBehavior), new PropertyMetadata(false));
|
||||
|
||||
static class EnableTwoWayVerticalOffsetBindingUtils {
|
||||
public static void on_scroll_changed(object sender, ScrollChangedEventArgs e) {
|
||||
if (e.VerticalChange != 0 && sender is ScrollViewer scroll) {
|
||||
if (!(bool)scroll.GetValue(VerticalOffsetMirrorBusyProperty)) {
|
||||
scroll.SetValue(VerticalOffsetMirrorBusyProperty, true);
|
||||
// scroll changes could get noisy while layouts are being processed;
|
||||
// let everything settle down before updating the VerticalOffset binding sources/targets.
|
||||
scroll.Dispatcher.InvokeAsync(() => {
|
||||
scroll.SetCurrentValue(VerticalOffsetProperty, scroll.VerticalOffset);
|
||||
scroll.SetValue(VerticalOffsetMirrorBusyProperty, false);
|
||||
}, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +236,14 @@ namespace mbts.ui.extensions {
|
||||
target.ScrollChanged += callback;
|
||||
return Disposable.Create(() => target.ScrollChanged -= callback);
|
||||
}
|
||||
|
||||
public static bool TryScrollToVerticalOffset(this ScrollViewer scroll_viewer, double vertical_offset) {
|
||||
if (vertical_offset >= 0 && scroll_viewer.IsLoaded && vertical_offset < scroll_viewer.ExtentHeight - scroll_viewer.ViewportHeight) {
|
||||
scroll_viewer.ScrollToVerticalOffset(vertical_offset);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TextBoxExtensions {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace mbts.ui {
|
||||
static class ResourceKeys {
|
||||
public const string
|
||||
Securities = nameof(Securities),
|
||||
Accounts = nameof(Accounts),
|
||||
Users = nameof(Users),
|
||||
Sides = nameof(Sides);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace mbts.ui {
|
||||
static class TemplateKeys {
|
||||
public const string
|
||||
SecurityKey = nameof(SecurityKey);
|
||||
}
|
||||
}
|
||||
@@ -15,8 +15,7 @@ namespace mbts.ui.shell {
|
||||
// The LoginContainer participates in the startup Import/Export dependency system
|
||||
// Since the login form should not depend on *anything* it will be shown before this is created
|
||||
// Once this is created it access the global LoginModel and retrieves the username
|
||||
internal class LoginContainer
|
||||
{
|
||||
class LoginContainer {
|
||||
[ImportingConstructor]
|
||||
public LoginContainer(ServiceContext context) {
|
||||
var model = LoginModel.instance;
|
||||
@@ -28,10 +27,8 @@ namespace mbts.ui.shell {
|
||||
model.Dispose();
|
||||
}
|
||||
|
||||
[Export(Exports.UserName)]
|
||||
public string UserName { get; private set; }
|
||||
|
||||
[Export(Exports.UserID)]
|
||||
public uint UserId { get; private set; }
|
||||
|
||||
[Export]
|
||||
@@ -42,13 +39,6 @@ namespace mbts.ui.shell {
|
||||
|
||||
[Export("workspace")]
|
||||
public string Workspace { get; private set; }
|
||||
|
||||
public static class Exports {
|
||||
public const string
|
||||
UserName = "username",
|
||||
UserID = "userid",
|
||||
UserTuple = "usertuple";
|
||||
}
|
||||
}
|
||||
|
||||
public struct User {
|
||||
|
||||
@@ -130,8 +130,10 @@ namespace mbts.spark {
|
||||
DialogSkirtItem _apply_item, _ok_item, _cancel_item;
|
||||
MenuItemVM _import_item, _export_item, _load_factory_item;
|
||||
|
||||
public ProfiledShellSettingsDialogVM(ISettingsProfileService<T> profile_service, Func<UserSettings, T> get_fallback, Func<T, TVM> vm_factory, Func<T, T> normalize, T init, string file_ext, uint settings_id = 0) {
|
||||
public ProfiledShellSettingsDialogVM(ISettingsProfileService<T> profile_service, Func<UserSettings, T> get_fallback, Func<T, TVM> vm_factory, Func<T, T> normalize, T init, string file_ext, Func<T, T, bool> profile_equals = null, uint settings_id = 0) {
|
||||
_vm_factory = vm_factory;
|
||||
if (profile_equals == null)
|
||||
profile_equals = EqualityComparer<T>.Default.Equals;
|
||||
// profile collections
|
||||
var skip_local = profiles.WrapSkip(1);
|
||||
var skip_local_wrapper = skip_local.WrapSelector(
|
||||
@@ -185,12 +187,19 @@ namespace mbts.spark {
|
||||
continue;
|
||||
i = ~i;
|
||||
var temp = ns.with(value: normalize(ns.value));
|
||||
skip_local_wrapper.Insert(i, temp);
|
||||
if (settings_id == temp.id) {
|
||||
if (temp.value.Equals(init))
|
||||
selected_profile = skip_local[i];
|
||||
settings_id = 0;
|
||||
if (profile_equals(temp.value, init)) {
|
||||
if (!temp.value.Equals(init)) {
|
||||
// they aren't exactly equal (don't overwrite any props in 'init' - use it to create the vm)
|
||||
temp = temp.with(value: init);
|
||||
}
|
||||
skip_local_wrapper.Insert(i, temp);
|
||||
selected_profile = skip_local[i];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
skip_local_wrapper.Insert(i, temp);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -114,10 +114,16 @@ namespace mbts.ui
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
foreach (IShellModule module in _modules.OrderBy(m => m.priority).Reverse()) {
|
||||
foreach (var module in _modules.OrderBy(m => m.priority).Reverse()) {
|
||||
module.Dispose();
|
||||
}
|
||||
_modules.Clear();
|
||||
foreach (var daemon in _services) {
|
||||
if (daemon is IDisposable d) {
|
||||
d.Dispose();
|
||||
}
|
||||
}
|
||||
_services.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,7 @@ using System.Windows.Input;
|
||||
|
||||
namespace mbts.ui.shell {
|
||||
[Export(typeof(ILauncher))]
|
||||
public class LauncherVM : ViewModelBase, ILauncher
|
||||
{
|
||||
public class LauncherVM : ViewModelBase, ILauncher {
|
||||
Window _window;
|
||||
string _user;
|
||||
readonly IBus _bus;
|
||||
@@ -23,9 +22,9 @@ namespace mbts.ui.shell {
|
||||
[ImportingConstructor]
|
||||
public LauncherVM(
|
||||
ObservableCollection<IShellModule> modules,
|
||||
[Import(LoginContainer.Exports.UserName)]string username,
|
||||
UserTuple user,
|
||||
ErrorReporter errors, IBus bus) {
|
||||
_user = username;
|
||||
_user = user.name;
|
||||
_modules = new ObservableAutoWrapper<ShellModuleVM, IShellModule>(modules, m => new ShellModuleVM(m, bus));
|
||||
_error_service = errors;
|
||||
_bus = bus;
|
||||
@@ -123,7 +122,7 @@ namespace mbts.ui.shell {
|
||||
_shown = true;
|
||||
if (_window != null) {
|
||||
Application.Current.ShutdownMode = ShutdownMode.OnMainWindowClose;
|
||||
if (shown != null) shown();
|
||||
shown?.Invoke();
|
||||
_window.Show();
|
||||
}
|
||||
}
|
||||
@@ -140,4 +139,4 @@ namespace mbts.ui.shell {
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -42,8 +42,8 @@ namespace mbts.ui.shell
|
||||
try {
|
||||
Dispatcher.Run();
|
||||
}
|
||||
catch (Exception e) {
|
||||
Log.assert(false, e.ToString());
|
||||
catch (Exception ex) {
|
||||
Log.assert(false, $"Exception on dispatcher thread '{name}': {ex}.");
|
||||
}
|
||||
finally {
|
||||
Log.Ui.info("Disposing slow {0} and cleaning up background thread.".fmt(name));
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace mbts.spark {
|
||||
readonly IRaiseCanExecuteChangedCommand _add_criteria_command, _remove_criteria_command, _ok_button_command;
|
||||
|
||||
bool _allow_empty, _is_edit_mode;
|
||||
SecurityImmutable? _pending_sec;
|
||||
(SecurityCriteria criteria, SecurityImmutable? sec, SecurityMD? md)? _snapshot;
|
||||
uint? _pending_sec_key;
|
||||
(SecurityCriteria criteria, uint? sec, SecurityMD? md)? _snapshot;
|
||||
|
||||
public ContractPickerModel(IBus bus, bool edit_mode, SecurityCriteria existing_criteria, SecurityCriteria world_view_filter = null)
|
||||
: this(bus.subscribe(SecuritiesTag), edit_mode, existing_criteria, world_view_filter) { }
|
||||
@@ -209,9 +209,9 @@ namespace mbts.spark {
|
||||
_update_security_types();
|
||||
_update_symbols();
|
||||
_update_securities();
|
||||
if (_pending_sec.HasValue && changed_securities.ContainsKey(_pending_sec.Value.Id)) {
|
||||
select_security_request?.Invoke(_pending_sec.Value);
|
||||
_pending_sec = null;
|
||||
if (_pending_sec_key.HasValue && changed_securities.TryGetValue(_pending_sec_key.Value, out var smd)) {
|
||||
select_security_request?.Invoke(smd.security);
|
||||
_pending_sec_key = null;
|
||||
}
|
||||
}
|
||||
internal void criteria_selection_changed(IEnumerable<SecurityCriteriaClause> selected_clauses) {
|
||||
@@ -229,6 +229,8 @@ namespace mbts.spark {
|
||||
security_criteria_clause_changed?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public bool is_editing => _snapshot.HasValue;
|
||||
|
||||
public ObservableCollection<Venue> all_exchanges { get; } = new ObservableCollection<Venue>();
|
||||
|
||||
public List<Venue> selected_exchanges = new List<Venue>();
|
||||
@@ -298,7 +300,22 @@ namespace mbts.spark {
|
||||
if (all_securities.ContainsKey(value.Value.security.Id)) {
|
||||
select_security_request?.Invoke(value.Value.security);
|
||||
} else {
|
||||
_pending_sec = value.Value.security;
|
||||
_pending_sec_key = value.Value.security.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint? selected_security_key {
|
||||
get => selected_securities.Count == 1 && all_securities.TryGetValue(selected_securities[0], out var smd) ? smd.security.Id : (uint?)null;
|
||||
set {
|
||||
if (value != selected_security_key) {
|
||||
if (value.HasValue) {
|
||||
if (all_securities.TryGetValue(value.Value, out var smd)) {
|
||||
select_security_request?.Invoke(smd.security);
|
||||
} else {
|
||||
_pending_sec_key = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,7 +330,7 @@ namespace mbts.spark {
|
||||
if (all_securities.ContainsKey(value.Value.Id)) {
|
||||
select_security_request?.Invoke(value.Value);
|
||||
} else {
|
||||
_pending_sec = value.Value;
|
||||
_pending_sec_key = value.Value.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,18 +353,19 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
#region IEditableObject
|
||||
public void BeginEdit() => _snapshot = (result_criteria, _pending_sec ?? selected_security, selected_security_md);
|
||||
public void BeginEdit() => _snapshot = (result_criteria, _pending_sec_key ?? selected_security_key, selected_security_md);
|
||||
public void EndEdit() => _snapshot = null;
|
||||
public void CancelEdit() {
|
||||
if (_snapshot.HasValue) {
|
||||
var (criteria, sec, md) = _snapshot.Value;
|
||||
security_criteria_clauses.SetItems(criteria.clauses);
|
||||
selected_security = sec;
|
||||
selected_security_key = sec;
|
||||
/// A <see cref="_pending_sec"/> may have been used to create the snapshot when <see cref="BeginEdit"/> was called (this happens when securities are loaded lazily).
|
||||
/// If this is the case, pending sec was assigned to the <see cref="_snapshot"/> security;
|
||||
/// check if the security is now present in <see cref="all_securities"/>, if so, use that value instead of the null.
|
||||
selected_security_md = sec.HasValue && !md.HasValue && all_securities.TryGetValue(sec.Value.Id, out var md_match) ? md_match : md;
|
||||
selected_security_md = sec.HasValue && !md.HasValue && all_securities.TryGetValue(sec.Value, out var md_match) ? md_match : md;
|
||||
_snapshot = null;
|
||||
RaisePropertyChanged(nameof(selected_security));
|
||||
_add_criteria_command.RaiseCanExecuteChanged();
|
||||
_remove_criteria_command.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
@@ -16,8 +17,11 @@ namespace mbts.ui.controls {
|
||||
partial class ContractPickerPopup : PopupControl {
|
||||
public static readonly EventHandler
|
||||
FlushSelectedCriteriaHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedCriteriaProperty)?.UpdateSource(),
|
||||
FlushSelectedSecurityKeyHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedSecurityKeyProperty)?.UpdateSource(),
|
||||
FlushSelectedSecurityHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedSecurityProperty)?.UpdateSource();
|
||||
|
||||
readonly PropertyUpdateGate _selection_sync_gate = new PropertyUpdateGate();
|
||||
|
||||
IReadOnlyDictionary<uint, SecurityMD> _cached_security_map = DictionaryHelper.empty<uint, SecurityMD>();
|
||||
ContractPickerPopupController _controller;
|
||||
bool _allow_empty, _is_lazy;
|
||||
@@ -97,12 +101,55 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SelectedSecurityKey
|
||||
public static readonly DependencyProperty SelectedSecurityKeyProperty =
|
||||
WPFHelper.create_dp<uint?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurityKey),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security_key = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(o.SelectedSecurityMD, o.SelectedSecurity, v, v));
|
||||
});
|
||||
//(o, base_value) => base_value is uint u && u != 0 ? u : (uint?)null);
|
||||
|
||||
public uint? SelectedSecurityKey {
|
||||
get => GetValue(SelectedSecurityKeyProperty) as uint?;
|
||||
set => SetValue(SelectedSecurityKeyProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
enum SecurityProp {
|
||||
Security,
|
||||
SecurityMD,
|
||||
ID
|
||||
}
|
||||
|
||||
void _sync_selection_props(SecurityMD? curr_sec_md, SecurityImmutable? curr_sec, uint? curr_sec_key, uint? target_sec_key) {
|
||||
if (curr_sec_key != target_sec_key) {
|
||||
SetCurrentValue(SelectedSecurityKeyProperty, target_sec_key);
|
||||
}
|
||||
bool sec_md_dirty = target_sec_key != (curr_sec_md?.security)?.Id;
|
||||
bool sec_dirty = target_sec_key != curr_sec?.Id;
|
||||
if (sec_md_dirty || sec_dirty) {
|
||||
curr_sec_md = target_sec_key.HasValue && _cached_security_map.TryGetValue(target_sec_key.Value, out var smd) ? smd : (SecurityMD?)null;
|
||||
if (sec_md_dirty)
|
||||
SetCurrentValue(SelectedSecurityMDProperty, curr_sec_md);
|
||||
if (sec_dirty)
|
||||
SetCurrentValue(SelectedSecurityProperty, curr_sec_md?.security);
|
||||
}
|
||||
if (!IsEditMode)
|
||||
SetCurrentValue(SelectedCriteriaProperty, target_sec_key.HasValue ? SecurityCriteria.from_keys(new[] { target_sec_key.Value }) : null);
|
||||
}
|
||||
|
||||
#region SelectedSecurity
|
||||
public static readonly DependencyProperty SelectedSecurityProperty =
|
||||
WPFHelper.create_dp<SecurityImmutable?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurity),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => o.Controller.model.selected_security = v);
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(o.SelectedSecurityMD, v, o.SelectedSecurityKey, v?.Id));
|
||||
});
|
||||
|
||||
public SecurityImmutable? SelectedSecurity {
|
||||
get => GetValue(SelectedCriteriaProperty) as SecurityImmutable?;
|
||||
@@ -115,7 +162,10 @@ namespace mbts.ui.controls {
|
||||
WPFHelper.create_dp<SecurityMD?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurityMD),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => o.Controller.model.selected_security_md = v);
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security_md = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(v, o.SelectedSecurity, o.SelectedSecurityKey, (v?.security)?.Id));
|
||||
});
|
||||
|
||||
public SecurityMD? SelectedSecurityMD {
|
||||
get => GetValue(SelectedCriteriaProperty) as SecurityMD?;
|
||||
@@ -248,12 +298,10 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
void on_cancel() {
|
||||
model.CancelEdit();
|
||||
propagate_to_owner();
|
||||
owner.CancelClick?.Invoke(owner, EventArgs.Empty);
|
||||
}
|
||||
void propagate_to_owner() {
|
||||
owner.set_current_if_changed(SelectedSecurityMDProperty, owner.Controller.model.selected_security_md);
|
||||
owner.set_current_if_changed(SelectedSecurityProperty, owner.Controller.model.selected_security);
|
||||
owner.set_current_if_changed(SelectedSecurityKeyProperty, owner.Controller.model.selected_security_key);
|
||||
owner.set_current_if_changed(SelectedCriteriaProperty, owner.Controller.model.result_criteria);
|
||||
}
|
||||
}
|
||||
@@ -262,4 +310,4 @@ namespace mbts.ui.controls {
|
||||
public SecurityCriteria selected_criteria => _selected_criteria.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace mbts.ui {
|
||||
class BindingProxy : FrameworkElement {
|
||||
public static readonly DependencyProperty ValueProperty =
|
||||
DependencyProperty.Register(nameof(Value), typeof(object), typeof(BindingProxy));
|
||||
|
||||
public object Value {
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ using System.Linq;
|
||||
using System.Reactive.Concurrency;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Text;
|
||||
|
||||
namespace bts.api.core {
|
||||
@@ -94,38 +93,38 @@ namespace bts.api.core {
|
||||
new Update<T, TKey>(new T[0], new T[0], new Change<T>[0], new T[0], key_extractor);
|
||||
|
||||
public Update<T,TKey> merge(Update<T, TKey> next) {
|
||||
var comp = new RowEqualityComparer<T, TKey>(_key);
|
||||
var added = this.added as HashSet<T> ?? new HashSet<T>(this.added, comp);
|
||||
var added = this.added as KeyedValueSet<TKey, T> ?? new KeyedValueSet<TKey, T>(this.added, _key);
|
||||
var changes = this.changed as ChangeDict<T,TKey> ?? new ChangeDict<T,TKey>(this.changes, _key);
|
||||
var removed = this.removed as HashSet<T> ?? new HashSet<T>(this.removed, comp);
|
||||
var removed = this.removed as KeyedValueSet<TKey, T> ?? new KeyedValueSet<TKey, T>(this.removed, _key);
|
||||
|
||||
// Add
|
||||
added.UnionWith(next.added);
|
||||
foreach (var v in next.added) {
|
||||
var k = _key(v);
|
||||
if (removed.Remove(k, out var oldval)) {
|
||||
changes.AddOrMerge(new Change<T>(oldval, v));
|
||||
} else {
|
||||
added[k] = v;
|
||||
}
|
||||
}
|
||||
|
||||
// Change
|
||||
foreach (var c in next.changes) {
|
||||
if (added.Contains(c.Old)) {
|
||||
added.Remove(c.Old);
|
||||
added.Add(c.New);
|
||||
}
|
||||
else {
|
||||
var k = _key(c.Old);
|
||||
if (!added.Replace(k, c.New))
|
||||
changes.AddOrMerge(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove
|
||||
foreach (var r in next.removed) {
|
||||
if (!added.Remove(r)) {
|
||||
removed.Add(r);
|
||||
}
|
||||
changes.Remove(_key(r));
|
||||
var k = _key(r);
|
||||
if (!added.Remove(k))
|
||||
removed[k] = changes.TryRemove(k, out var ch) ? ch.Old : r;
|
||||
}
|
||||
|
||||
return new Update<T, TKey>(
|
||||
next.all,
|
||||
added, changes, removed,
|
||||
_key
|
||||
);
|
||||
_key);
|
||||
}
|
||||
|
||||
public IEnumerable<T> all { get; }
|
||||
@@ -148,6 +147,39 @@ namespace bts.api.core {
|
||||
#endif
|
||||
}
|
||||
|
||||
class KeyedValueSet<TKey, TValue> : IEnumerable<TValue> {
|
||||
readonly Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
|
||||
|
||||
public KeyedValueSet(IEnumerable<TValue> value, Func<TValue, TKey> key) {
|
||||
foreach (var v in value)
|
||||
_cache[key(v)] = v;
|
||||
}
|
||||
|
||||
public TValue this[TKey key] {
|
||||
set => _cache[key] = value;
|
||||
}
|
||||
|
||||
public bool Remove(TKey key) =>
|
||||
_cache.Remove(key);
|
||||
|
||||
public bool Remove(TKey key, out TValue value) =>
|
||||
_cache.TryGetValue(key, out value) && _cache.Remove(key);
|
||||
|
||||
public bool Replace(TKey key, TValue value) {
|
||||
if (_cache.ContainsKey(key)) {
|
||||
_cache[key] = value;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public IEnumerator<TValue> GetEnumerator() =>
|
||||
_cache.Values.GetEnumerator();
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() =>
|
||||
GetEnumerator();
|
||||
}
|
||||
|
||||
class ChangeDict<T, TKey> : IEnumerable<Change<T>> {
|
||||
Dictionary<TKey, Change<T>> _changes;
|
||||
Func<Change<T>, TKey> _key;
|
||||
@@ -167,9 +199,11 @@ namespace bts.api.core {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void Remove(TKey key) {
|
||||
public bool Remove(TKey key) =>
|
||||
_changes.Remove(key);
|
||||
}
|
||||
|
||||
public bool TryRemove(TKey key, out Change<T> result) =>
|
||||
_changes.TryGetValue(key, out result) && _changes.Remove(key);
|
||||
|
||||
#region IEnumerable
|
||||
public IEnumerator<Change<T>> GetEnumerator() {
|
||||
@@ -181,31 +215,6 @@ namespace bts.api.core {
|
||||
#endregion
|
||||
}
|
||||
|
||||
struct RowEqualityComparer<T,TKey> : IEqualityComparer<T>, IEqualityComparer<Change<T>>
|
||||
where TKey : IEquatable<TKey> {
|
||||
readonly Func<T,TKey> _key;
|
||||
|
||||
public RowEqualityComparer(Func<T,TKey> key) {
|
||||
_key = key;
|
||||
}
|
||||
|
||||
public bool Equals(T x, T y) {
|
||||
return _key(x).Equals(_key(y));
|
||||
}
|
||||
|
||||
public bool Equals(Change<T> x, Change<T> y) {
|
||||
return Equals(x.New, y.New);
|
||||
}
|
||||
|
||||
public int GetHashCode(T obj) {
|
||||
return _key(obj).GetHashCode();
|
||||
}
|
||||
|
||||
public int GetHashCode(Change<T> c) {
|
||||
return GetHashCode(c.New);
|
||||
}
|
||||
}
|
||||
|
||||
public class TableUpdate<T, TKey> where TKey : IEquatable<TKey> {
|
||||
// warning: this does not have a key_extractor
|
||||
public static readonly TableUpdate<T, TKey> empty = new TableUpdate<T, TKey>(ImmutableDictionary<TKey, T>.Empty, Array.Empty<T>(), Array.Empty<Change<T>>(), Array.Empty<T>(), null);
|
||||
@@ -736,13 +745,19 @@ namespace bts.api.core {
|
||||
return new CompositeDisposable(source.Subscribe(aggregator.on_left), right.Subscribe(aggregator.on_right));
|
||||
});
|
||||
|
||||
public static IObservable<TResult> AggregateLatest<TLeft, TRight, TAggregate, TResult>(this IObservable<TLeft> source, IObservable<TRight> right,
|
||||
Func<TAggregate, Change<TLeft>, TRight, TAggregate> update_left, Func<TAggregate, TLeft, Change<TRight>, TAggregate> update_right, TAggregate seed,
|
||||
public static IObservable<TResult> AggregateLatest<TLeft, TRight, TAggregate, TResult>(
|
||||
this IObservable<TLeft> source,
|
||||
IObservable<TRight> right,
|
||||
Func<TAggregate, Change<TLeft>, TRight, TAggregate> update_left,
|
||||
Func<TAggregate, TLeft, Change<TRight>, TAggregate> update_right,
|
||||
Func<TAggregate> seed_factory,
|
||||
Func<TAggregate, TResult> result_selector,
|
||||
TLeft init_left = default, TRight init_right = default) =>
|
||||
TLeft init_left = default,
|
||||
TRight init_right = default) =>
|
||||
//
|
||||
Observable.Create<TResult>(o => {
|
||||
Log.DataFlow.diag($"Create AggregateLatest (Left: {typeof(TLeft)}, Right: {typeof(TRight)}).");
|
||||
var aggregator = new ConcurrentAggregator<TLeft, TRight, TAggregate>(update_left, update_right, agg => o.OnNext(result_selector(agg)), seed, init_left, init_right);
|
||||
var aggregator = new ConcurrentAggregator<TLeft, TRight, TAggregate>(update_left, update_right, agg => o.OnNext(result_selector(agg)), seed_factory(), init_left, init_right);
|
||||
return new CompositeDisposable(source.Subscribe(aggregator.on_left), right.Subscribe(aggregator.on_right));
|
||||
});
|
||||
|
||||
@@ -922,4 +937,4 @@ namespace bts.api.core {
|
||||
$"T is '{typeof(T)}', TKey is '{typeof(TKey)}'";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ namespace bts.api.core {
|
||||
return left_source.AggregateLatest<TableUpdate<TLeft, TKey>, TableUpdate<TRight, TKey>, (TableUpdate<TResult, TKey> result, IReadOnlyDictionary<TKey, TLeft> left_state, IReadOnlyDictionary<TKey, TRight> right_state), TableUpdate<TResult, TKey>>(right_source,
|
||||
process_left,
|
||||
process_right,
|
||||
(TableUpdate<TResult, TKey>.mk_empty(result_key), new Dictionary<TKey, TLeft>(0), new Dictionary<TKey, TRight>(0)),
|
||||
() => (TableUpdate<TResult, TKey>.mk_empty(result_key), new Dictionary<TKey, TLeft>(0), new Dictionary<TKey, TRight>(0)),
|
||||
t => t.result,
|
||||
TableUpdate<TLeft, TKey>.mk_empty(left_key), TableUpdate<TRight, TKey>.mk_empty(right_key));
|
||||
// left
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace bts.api.core {
|
||||
right_source,
|
||||
process_left,
|
||||
process_right,
|
||||
(TableUpdate<TResult, TLeftKey>.mk_empty(result_key), new Dictionary<TRightKey, HashSet<TLeftKey>>()),
|
||||
() => (TableUpdate<TResult, TLeftKey>.mk_empty(result_key), new Dictionary<TRightKey, HashSet<TLeftKey>>()),
|
||||
state => state.result,
|
||||
TableUpdate<TLeft, TLeftKey>.mk_empty(left_key),
|
||||
TableUpdate<TRight, TRightKey>.mk_empty(right_key));
|
||||
|
||||
@@ -1,15 +1,28 @@
|
||||
using System;
|
||||
|
||||
namespace bts {
|
||||
struct UserTuple : IEquatable<UserTuple>, IComparable<UserTuple>, IComparable {
|
||||
public readonly struct UserTuple : IEquatable<UserTuple>, IComparable<UserTuple>, IComparable {
|
||||
const string _admin_name = "bts";
|
||||
|
||||
public static readonly Func<UserTuple, uint> key_fn = u => u.id;
|
||||
public static UserTuple admin => new UserTuple(0, _admin_name, Permissions.Admin);
|
||||
|
||||
public readonly uint id;
|
||||
public readonly string name;
|
||||
public readonly Permissions permissions;
|
||||
|
||||
public UserTuple(uint id, string name) {
|
||||
public UserTuple(uint id, string name)
|
||||
: this(id, name, name == _admin_name ? Permissions.Admin : Permissions.Regular) { }
|
||||
|
||||
public UserTuple(uint id, string name, Permissions permissions) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.permissions = permissions;
|
||||
}
|
||||
|
||||
public enum Permissions {
|
||||
Regular,
|
||||
Admin
|
||||
}
|
||||
|
||||
public int CompareTo(UserTuple other) {
|
||||
@@ -28,4 +41,9 @@ namespace bts {
|
||||
public static bool operator ==(UserTuple a, UserTuple b) => a.Equals(b);
|
||||
public static bool operator !=(UserTuple a, UserTuple b) => !a.Equals(b);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UserTupleExtensions {
|
||||
public static bool can_access_user_data(this UserTuple user, uint user_id) =>
|
||||
user.id == user_id || user.permissions == UserTuple.Permissions.Admin;
|
||||
}
|
||||
}
|
||||
@@ -13,84 +13,78 @@ namespace bts.md {
|
||||
enum MDSourceOptions {
|
||||
DeferInit = 1,
|
||||
}
|
||||
class MDSource : IObserver<List<SecurityAndHandle>>, IDisposable
|
||||
{
|
||||
|
||||
class MDSource : IObserver<List<SecurityAndHandle>>, IDisposable {
|
||||
const uint _no_user_id = 0;
|
||||
|
||||
protected readonly MDConsumer _consumer = new MDConsumer();
|
||||
protected readonly Co2RelayControl _relay;
|
||||
protected DateTime _version = DateTime.MinValue;
|
||||
protected readonly ConcurrentDictionary<SecurityKey, SecurityMD> _exchange_sec_map = new ConcurrentDictionary<SecurityKey, SecurityMD>();
|
||||
protected List<IDisposable> _dispose = new List<IDisposable>();
|
||||
protected readonly MDSourceOptions _options;
|
||||
readonly uint _user_id;
|
||||
protected DateTime _version = DateTime.MinValue;
|
||||
protected List<IDisposable> _dispose = new List<IDisposable>();
|
||||
|
||||
public MDSource(uint naggle_ms, MDSourceOptions options = 0, uint user_id = _no_user_id)
|
||||
{
|
||||
readonly UserTuple _user;
|
||||
readonly BehaviorSubject<ConcurrentDictionary<SecurityKey, SecurityMD>> _on_securities =
|
||||
new BehaviorSubject<ConcurrentDictionary<SecurityKey, SecurityMD>>(new ConcurrentDictionary<SecurityKey, SecurityMD>());
|
||||
|
||||
public MDSource(uint naggle_ms, MDSourceOptions options = 0)
|
||||
: this(naggle_ms, options, UserTuple.admin) { }
|
||||
|
||||
public MDSource(uint naggle_ms, MDSourceOptions options, UserTuple user) {
|
||||
_options = options;
|
||||
_user_id = user_id;
|
||||
_user = user;
|
||||
_relay = new Co2RelayControl(_consumer, naggle_ms);
|
||||
_relay.preload_securities((co2.MDRequest.Types.Flags)0xff).ignore();
|
||||
_dispose.Add(_consumer.on_security.Subscribe(this));
|
||||
if(!options.HasFlag(MDSourceOptions.DeferInit))
|
||||
if (!options.HasFlag(MDSourceOptions.DeferInit))
|
||||
initialize();
|
||||
}
|
||||
|
||||
readonly BehaviorSubject<ConcurrentDictionary<SecurityKey, SecurityMD>> _on_securities =
|
||||
new BehaviorSubject<ConcurrentDictionary<SecurityKey, SecurityMD>>( new ConcurrentDictionary<SecurityKey, SecurityMD>() );
|
||||
public IObservable<ConcurrentDictionary<SecurityKey, SecurityMD>> on_securities => _on_securities;
|
||||
|
||||
virtual public void initialize()
|
||||
{
|
||||
public virtual void initialize() =>
|
||||
Task.Run(run);
|
||||
}
|
||||
|
||||
public void set_token(AuthTokenImmutable token)
|
||||
{
|
||||
public void set_token(AuthTokenImmutable token) =>
|
||||
_relay.set_token(token);
|
||||
}
|
||||
|
||||
public void OnCompleted() { }
|
||||
public void OnError(Exception error) => Log.Md.error(error.ToString());
|
||||
|
||||
public void OnError(Exception error) =>
|
||||
Log.Md.error(error.ToString());
|
||||
|
||||
public void OnNext(List<SecurityAndHandle> values) {
|
||||
if (values.Count > 0) {
|
||||
foreach (var v in values) {
|
||||
if (_user_id != _no_user_id &&
|
||||
v.security.ContractCase == Security.ContractOneofCase.Synthetic &&
|
||||
v.security.Synthetic.UserId != _user_id &&
|
||||
v.security.Synthetic.UserId != _no_user_id) {
|
||||
continue;
|
||||
if (v.security.ContractCase != Security.ContractOneofCase.Synthetic || _user.can_access_user_data(v.security.Synthetic.UserId)) {
|
||||
var smd = new SecurityMD(new SecurityImmutable(v.security), v.handle);
|
||||
_exchange_sec_map.AddOrUpdate(smd.security.Id, smd, (_, __) => smd);
|
||||
}
|
||||
var smd = new SecurityMD(new SecurityImmutable(v.security), v.handle);
|
||||
_exchange_sec_map.AddOrUpdate(smd.security.Id, smd, (_, __) => smd);
|
||||
}
|
||||
_version = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => Disposable.dispose(_dispose);
|
||||
async protected Task run()
|
||||
{
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(_dispose);
|
||||
|
||||
protected async Task run() {
|
||||
DateTime run_version = DateTime.MinValue;
|
||||
while (true)
|
||||
{
|
||||
while (true) {
|
||||
var exch = _exchange_sec_map;
|
||||
var current_version = _version;
|
||||
if (current_version > run_version)
|
||||
{
|
||||
if (current_version > run_version) {
|
||||
run_version = current_version;
|
||||
try
|
||||
{
|
||||
try {
|
||||
Log.Md.info($"MDSource publishing {exch.Count} securities");
|
||||
_on_securities.OnNext(exch);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
} catch (Exception e) {
|
||||
Log.Md.error($"Error in publish_securities: {e}");
|
||||
}
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromSeconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,11 @@
|
||||
using bts.api;
|
||||
using mbts.co2;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts {
|
||||
struct SecurityMD : IEquatable<SecurityMD> {
|
||||
readonly struct SecurityMD : IEquatable<SecurityMD> {
|
||||
public static readonly Func<SecurityMD, uint> key_fn = smd => smd.security.Id;
|
||||
|
||||
public SecurityImmutable security { get; }
|
||||
public MDConsumerHandle md { get; }
|
||||
|
||||
@@ -36,4 +34,4 @@ namespace mbts {
|
||||
return !(left == right);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -278,6 +278,9 @@ namespace System.Collections.Generic {
|
||||
public static IComparer<KeyValuePair<TKey, TValue>> key_comparer<TKey, TValue>() =>
|
||||
Impl<TKey, TValue>.key_comparer;
|
||||
|
||||
public static IReadOnlyDictionary<TKey, TValue> wrap_value<TKey, TValue>(TKey key, TValue value) =>
|
||||
new Impl<TKey, TValue>.ValueWrapper(key, value);
|
||||
|
||||
static class Impl<TKey, TValue> {
|
||||
public static readonly IReadOnlyDictionary<TKey, TValue> empty =
|
||||
new ReadOnlyDictionary<TKey, TValue>(new Dictionary<TKey, TValue>(0));
|
||||
@@ -297,6 +300,57 @@ namespace System.Collections.Generic {
|
||||
public int Compare(object x, object y) =>
|
||||
x is KeyValuePair<TKey, TValue> xkv ? (y is KeyValuePair<TKey, TValue> ykv ? Compare(xkv, ykv) : 1) : -1;
|
||||
}
|
||||
|
||||
public class ValueWrapper : IReadOnlyDictionary<TKey, TValue> {
|
||||
readonly TKey _key;
|
||||
readonly TValue _value;
|
||||
readonly IEqualityComparer<TKey> _key_comparer;
|
||||
|
||||
public ValueWrapper(TKey key, TValue value)
|
||||
: this(key, value, EqualityComparer<TKey>.Default) { }
|
||||
|
||||
public ValueWrapper(TKey key, TValue value, IEqualityComparer<TKey> key_comparer) {
|
||||
_key = key;
|
||||
_value = value;
|
||||
_key_comparer = key_comparer;
|
||||
}
|
||||
|
||||
public TValue this[TKey key] =>
|
||||
_key_comparer.Equals(key, _key) ? _value : throw new KeyNotFoundException();
|
||||
|
||||
public int Count => 1;
|
||||
|
||||
public IEnumerable<TKey> Keys {
|
||||
get {
|
||||
yield return _key;
|
||||
}
|
||||
}
|
||||
|
||||
public IEnumerable<TValue> Values {
|
||||
get {
|
||||
yield return _value;
|
||||
}
|
||||
}
|
||||
|
||||
public bool ContainsKey(TKey key) => _key_comparer.Equals(key, _key);
|
||||
|
||||
public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() {
|
||||
yield return new KeyValuePair<TKey, TValue>(_key, _value);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator() {
|
||||
yield return new KeyValuePair<TKey, TValue>(_key, _value);
|
||||
}
|
||||
|
||||
public bool TryGetValue(TKey key, out TValue value) {
|
||||
if (_key_comparer.Equals(key, _key)) {
|
||||
value = _value;
|
||||
return true;
|
||||
}
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loaded 100 of 190 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user