diff --git a/.gitignore b/.gitignore index 5fe2ccc4d..e0769acc0 100644 --- a/.gitignore +++ b/.gitignore @@ -24,5 +24,7 @@ project.lock.json src/server/sparkd/orderman/perf_analysis/data src/server/sparkd/orderman/perf_analysis/logs/ src/server/sparkd/orderman/perf_analysis/trace_data/ +.fake +*.fsx.lock src/lib/orderman/perf_analysis/logs/latency_data/ trace_data/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 106c3b541..40bb8992a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -42,7 +42,18 @@ "stopAtEntry": false, "externalConsole": true, "requireExactSource": false - }, + }, + { + "name": "Client Tests", + "type": "clr", + "request": "launch", + "program": "${workspaceRoot}/build/Debug/bin/net471/sparkcmd.exe", + "args": ["test","test_filter"], + "cwd": "${workspaceRoot}", + "stopAtEntry": false, + "externalConsole": true, + "requireExactSource": false + }, { "name": "Client Attach", "type": "clr", @@ -187,7 +198,7 @@ "type": "cppdbg", "request": "launch", "program": "/home/bts/spark/build/ninja_debug/src/server/sparkd/sparkd", - "args": ["test", "[md]"], + "args": ["test","Co2RelayTests"], "stopAtEntry": false, "cwd": "/home/bts/spark/build/ninja_debug/src/server/sparkd/", "environment": [], diff --git a/ops_notes.txt b/ops_notes.txt index 0ffdf4188..6d1493c4b 100644 --- a/ops_notes.txt +++ b/ops_notes.txt @@ -1,3 +1,6 @@ +1.3.5 +- Change in market data wire format. Old shell will get only direct market data instead of merged which may not match what the servers is driving from. + 1.3.4 - Changed default logging config for Windows. If Logging config is overridden for a system it should be adjusted to override only linux. diff --git a/spark_client.sln b/spark_client.sln index b09517f46..f8aa20aaf 100644 --- a/spark_client.sln +++ b/spark_client.sln @@ -4,7 +4,7 @@ VisualStudioVersion = 15.0.27130.2024 MinimumVisualStudioVersion = 15.0.26124.0 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_api", "src\client\spark_api\spark_api.csproj", "{DC9A8975-8211-4FC9-A9BA-C8B9D26AD481}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libspark", "build\src\lib\libspark.vcxproj", "{8530E0E8-3768-37EC-B1E1-8248C5A92FE3}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "libspark", "build\src\lib\libspark.vcxproj", "{69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spark", "src\client\spark\Spark.csproj", "{9A4D4D07-1AB6-4B2B-B111-A4F34D6C267E}" EndProject @@ -12,11 +12,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "sparkcmd", "src\client\spar EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_core", "src\client\spark_core\spark_core.csproj", "{6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}" EndProject -Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZERO_CHECK", "build\ZERO_CHECK.vcxproj", "{9AD1A867-93A9-301D-BF77-0113E18E9E39}" +Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ZERO_CHECK", "build\ZERO_CHECK.vcxproj", "{33E7E6C5-04E3-36A8-A751-639BE5E72B9B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_sdk", "src\client\spark_sdk\spark_sdk.csproj", "{6963C314-83C4-4667-B0D0-C2F7AE208C29}" ProjectSection(ProjectDependencies) = postProject - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3} = {8530E0E8-3768-37EC-B1E1-8248C5A92FE3} + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C} = {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C} EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "spark_test", "src\client\spark_test\spark_test.csproj", "{ECD78CC5-138F-4809-A764-2358C2AC2E82}" @@ -57,22 +57,22 @@ Global {DC9A8975-8211-4FC9-A9BA-C8B9D26AD481}.RelWithDebInfo|x64.Build.0 = Release|x64 {DC9A8975-8211-4FC9-A9BA-C8B9D26AD481}.RelWithDebInfo|x86.ActiveCfg = Release|x64 {DC9A8975-8211-4FC9-A9BA-C8B9D26AD481}.RelWithDebInfo|x86.Build.0 = Release|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.Debug|Any CPU.ActiveCfg = Debug|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.Debug|x64.ActiveCfg = Debug|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.Debug|x64.Build.0 = Debug|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.Debug|x86.ActiveCfg = Debug|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.MinSizeRel|Any CPU.ActiveCfg = MinSizeRel|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.MinSizeRel|x64.Build.0 = MinSizeRel|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.Release|Any CPU.ActiveCfg = Release|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.Release|x64.ActiveCfg = Release|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.Release|x64.Build.0 = Release|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.Release|x86.ActiveCfg = Release|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.RelWithDebInfo|Any CPU.ActiveCfg = RelWithDebInfo|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64 - {8530E0E8-3768-37EC-B1E1-8248C5A92FE3}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.Debug|Any CPU.ActiveCfg = Debug|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.Debug|x64.ActiveCfg = Debug|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.Debug|x64.Build.0 = Debug|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.Debug|x86.ActiveCfg = Debug|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.MinSizeRel|Any CPU.ActiveCfg = MinSizeRel|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.MinSizeRel|x64.Build.0 = MinSizeRel|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.Release|Any CPU.ActiveCfg = Release|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.Release|x64.ActiveCfg = Release|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.Release|x64.Build.0 = Release|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.Release|x86.ActiveCfg = Release|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.RelWithDebInfo|Any CPU.ActiveCfg = RelWithDebInfo|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64 + {69D73CF1-9D7A-38CA-9817-0D0C3E3BCB3C}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64 {9A4D4D07-1AB6-4B2B-B111-A4F34D6C267E}.Debug|Any CPU.ActiveCfg = Debug|x64 {9A4D4D07-1AB6-4B2B-B111-A4F34D6C267E}.Debug|x64.ActiveCfg = Debug|x64 {9A4D4D07-1AB6-4B2B-B111-A4F34D6C267E}.Debug|x64.Build.0 = Debug|x64 @@ -93,26 +93,30 @@ Global {9A4D4D07-1AB6-4B2B-B111-A4F34D6C267E}.RelWithDebInfo|x64.Build.0 = Release|x64 {9A4D4D07-1AB6-4B2B-B111-A4F34D6C267E}.RelWithDebInfo|x86.ActiveCfg = Release|x64 {9A4D4D07-1AB6-4B2B-B111-A4F34D6C267E}.RelWithDebInfo|x86.Build.0 = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|Any CPU.ActiveCfg = Debug|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|x64.ActiveCfg = Debug|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|x64.Build.0 = Debug|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|x86.ActiveCfg = Debug|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|Any CPU.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|Any CPU.Build.0 = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|x64.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|x64.Build.0 = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|x86.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|x86.Build.0 = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|Any CPU.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|x64.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|x64.Build.0 = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|x86.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|Any CPU.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|Any CPU.Build.0 = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|x64.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|x64.Build.0 = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|x86.ActiveCfg = Release|x64 - {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|x86.Build.0 = Release|x64 + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|Any CPU.Build.0 = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|x64.ActiveCfg = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|x64.Build.0 = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|x86.ActiveCfg = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Debug|x86.Build.0 = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|Any CPU.ActiveCfg = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|Any CPU.Build.0 = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|x64.ActiveCfg = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|x64.Build.0 = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|x86.ActiveCfg = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.MinSizeRel|x86.Build.0 = Debug|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|Any CPU.ActiveCfg = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|Any CPU.Build.0 = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|x64.ActiveCfg = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|x64.Build.0 = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|x86.ActiveCfg = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.Release|x86.Build.0 = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|Any CPU.ActiveCfg = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|Any CPU.Build.0 = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|x64.ActiveCfg = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|x64.Build.0 = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|x86.ActiveCfg = Release|Any CPU + {387566C6-C6F6-412C-A30C-9855BBCB4816}.RelWithDebInfo|x86.Build.0 = Release|Any CPU {6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.Debug|Any CPU.ActiveCfg = Debug|x64 {6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.Debug|x64.ActiveCfg = Debug|x64 {6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.Debug|x64.Build.0 = Debug|x64 @@ -133,22 +137,22 @@ Global {6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.RelWithDebInfo|x64.Build.0 = Release|x64 {6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.RelWithDebInfo|x86.ActiveCfg = Release|x64 {6E449EB6-20AC-4FA6-B8D9-10F4BB7E1FEE}.RelWithDebInfo|x86.Build.0 = Release|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.Debug|Any CPU.ActiveCfg = Debug|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.Debug|x64.ActiveCfg = Debug|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.Debug|x64.Build.0 = Debug|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.Debug|x86.ActiveCfg = Debug|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.MinSizeRel|Any CPU.ActiveCfg = MinSizeRel|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.MinSizeRel|x64.Build.0 = MinSizeRel|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.Release|Any CPU.ActiveCfg = Release|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.Release|x64.ActiveCfg = Release|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.Release|x64.Build.0 = Release|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.Release|x86.ActiveCfg = Release|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.RelWithDebInfo|Any CPU.ActiveCfg = RelWithDebInfo|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64 - {9AD1A867-93A9-301D-BF77-0113E18E9E39}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.Debug|Any CPU.ActiveCfg = Debug|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.Debug|x64.ActiveCfg = Debug|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.Debug|x64.Build.0 = Debug|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.Debug|x86.ActiveCfg = Debug|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.MinSizeRel|Any CPU.ActiveCfg = MinSizeRel|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.MinSizeRel|x64.ActiveCfg = MinSizeRel|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.MinSizeRel|x64.Build.0 = MinSizeRel|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.MinSizeRel|x86.ActiveCfg = MinSizeRel|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.Release|Any CPU.ActiveCfg = Release|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.Release|x64.ActiveCfg = Release|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.Release|x64.Build.0 = Release|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.Release|x86.ActiveCfg = Release|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.RelWithDebInfo|Any CPU.ActiveCfg = RelWithDebInfo|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.RelWithDebInfo|x64.ActiveCfg = RelWithDebInfo|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.RelWithDebInfo|x64.Build.0 = RelWithDebInfo|x64 + {33E7E6C5-04E3-36A8-A751-639BE5E72B9B}.RelWithDebInfo|x86.ActiveCfg = RelWithDebInfo|x64 {6963C314-83C4-4667-B0D0-C2F7AE208C29}.Debug|Any CPU.ActiveCfg = Debug|x64 {6963C314-83C4-4667-B0D0-C2F7AE208C29}.Debug|x64.ActiveCfg = Debug|x64 {6963C314-83C4-4667-B0D0-C2F7AE208C29}.Debug|x64.Build.0 = Debug|x64 diff --git a/src/client/spark/Admin/AdminModel.cs b/src/client/spark/Admin/AdminModel.cs index 52efce6db..55a314811 100644 --- a/src/client/spark/Admin/AdminModel.cs +++ b/src/client/spark/Admin/AdminModel.cs @@ -11,18 +11,22 @@ namespace mbts.spark { [Export] class AdminModel : IDisposable { readonly string _user_name; + public string user_name => _user_name; + readonly BehaviorSubject> _accounts_subject = new BehaviorSubject>(new Dictionary()), - _users_subject = new BehaviorSubject>(new Dictionary()); + _users_subject = new BehaviorSubject>(new Dictionary()); IReadOnlyDictionary _account_map = new Dictionary(), _user_map = new Dictionary(); - IDisposable _dispose; + readonly BehaviorSubject _wrapper_subject = new BehaviorSubject(null); + IDisposable _dispose; [ImportingConstructor] AdminModel(AdminService service, [Import("username")]string user_name) { _user_name = user_name; _dispose = service.subscribei_admin_data().Select(update => { + _wrapper_subject.OnNext(new AdminModelWrapper(update.all.FirstOrDefault(), _user_name)); IEnumerable active_users = from admin_data in update.all from bts_user in admin_data.Users @@ -52,9 +56,10 @@ namespace mbts.spark { } public IObservable> on_users => _users_subject.AsObservable(); - public IObservable> on_accounts => _accounts_subject.AsObservable(); + public IObservable> on_accounts => _accounts_subject.AsObservable(); public IObservable> on_user_tuples => on_users.Select(d => d.Select(kv => new UserTuple(kv.Key, kv.Value)).ToList()); public IObservable> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToList()); + public IObservable on_admin_data => _wrapper_subject.AsObservable(); public UserTuple? get_current_user() { foreach (var kv in _user_map) { diff --git a/src/client/spark/Admin/AdminModelWrapper.cs b/src/client/spark/Admin/AdminModelWrapper.cs new file mode 100644 index 000000000..bbc001d33 --- /dev/null +++ b/src/client/spark/Admin/AdminModelWrapper.cs @@ -0,0 +1,65 @@ +using bts; +using bts.api.admin; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace mbts.spark +{ + class AdminModelWrapper + { + readonly AdminDataImmutable? _admin_data; + readonly string _user_name; + readonly List _allowed_accounts; + readonly Dictionary _allowed_account_dict = new Dictionary(); + public AdminModelWrapper(AdminDataImmutable? admin_data = null, string user_name = null) + { + _admin_data = admin_data; + _user_name = user_name; + _allowed_accounts = search_admin(u => u.Name == _user_name).Select(x => x.Item1).ToList(); + foreach(var val in _allowed_accounts) + _allowed_account_dict[val.Name] = val.Name; + } + public IEnumerable> get_trading_permissions(UInt32 user_id) + { + return search_admin(u => u.Id == user_id); + } + public IEnumerable viewable_accounts() + { + return _allowed_accounts; + } + public bool can_view_account(string account) + { + return _allowed_account_dict.ContainsKey(account); + } + + IEnumerable> search_admin(Func predicate) + { + var rv = new List>(); + if (_admin_data.HasValue) + { + var users = _admin_data.Value.Users.Where(predicate); + if (users != null && users.Count() == 1) + { + var user = users.ElementAt(0); + if (user.HasUserData && user.UserData.HasAccountPermissions) + { + foreach (var kv in user.UserData.AccountPermissions.AccountPermissions) + { + var account_id = kv.Key; + var accounts = _admin_data.Value.Accounts.Where(a => a.Id == account_id); + if (accounts != null && accounts.Count() == 1) + { + var account = accounts.ElementAt(0); + var tp = kv.Value.TradingPermission; + var rv_val = new TradingPermissionUnitImmutable(tp); + rv.Add(Tuple.Create(account, rv_val)); + } + } + } + } + } + return rv; + } + } +} \ No newline at end of file diff --git a/src/client/spark/Admin/PermissionFilters.cs b/src/client/spark/Admin/PermissionFilters.cs new file mode 100644 index 000000000..115187a45 --- /dev/null +++ b/src/client/spark/Admin/PermissionFilters.cs @@ -0,0 +1,66 @@ +using bts; +using bts.api; +using bts.api.core; +using bts.api.orderman; +using bts.api.position; +using bts.spark; +using mbts.collections; +using mbts.spark; +using mbts.spark.utilities; +using mbts.utils; +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.Specialized; +using System.Linq; +using System.Reactive.Concurrency; +using System.Reactive.Linq; + +namespace bts.api.core +{ + + static class PermissionFilters + { + public static IObservable> PermissionFilter(this IObservable> source, AdminModel am) + { + var pred = Observable.Create>( observer => + { + return am.on_admin_data.Subscribe(x => + { + Func filter = (z => x.can_view_account(z.Account)); + observer.OnNext(filter); + }); + }); + + return source.Filter(pred); + } + + public static IObservable> PermissionFilter(this IObservable> source, AdminModel am) + { + var pred = Observable.Create>(observer => + { + return am.on_admin_data.Subscribe(x => + { + Func filter = (z => x.can_view_account(z.account())); + observer.OnNext(filter); + }); + }); + + return source.Filter(pred); + } + + public static IObservable> PermissionFilter(this IObservable> source, AdminModel am) + { + var pred = Observable.Create>(observer => + { + return am.on_admin_data.Subscribe(x => + { + Func filter = (z => x.can_view_account(z.Account)); + observer.OnNext(filter); + }); + }); + + return source.Filter(pred); + } + } +} \ No newline at end of file diff --git a/src/client/spark/Daemons/ShellServices.cs b/src/client/spark/Daemons/ShellServices.cs index 25a8e9f8e..a57ac2547 100644 --- a/src/client/spark/Daemons/ShellServices.cs +++ b/src/client/spark/Daemons/ShellServices.cs @@ -1,5 +1,6 @@ using bts.api.admin; using bts.api.core; +using bts.api.md.auxiliary; using bts.api.orderman; using bts.api.persistence; using bts.api.position; @@ -40,5 +41,8 @@ namespace Spark { [Export] public PositionService position_service => context.locate(); + + [Export] + public MarketDataAuxiliaryService md_auxiliary_service => context.locate(); } } \ No newline at end of file diff --git a/src/client/spark/Images/Icons.cs b/src/client/spark/Images/Icons.cs index adc9118a4..2eec0bce9 100644 --- a/src/client/spark/Images/Icons.cs +++ b/src/client/spark/Images/Icons.cs @@ -36,20 +36,20 @@ namespace mbts.ui { #pragma warning disable CS0618 // Type or member is obsolete public static ImageSource Logo { get { return load_image("logo.ico"); } } - public static ImageSource Admin { get { return load_resource_and_freeze("Admin"); } } - public static ImageSource Analytics { get { return load_resource_and_freeze("Analytics"); } } - public static ImageSource Sheets { get { return load_resource_and_freeze("Sheets"); } } - public static ImageSource Trades { get { return load_resource_and_freeze("Trades"); } } - public static ImageSource Depth { get { return load_resource_and_freeze("Depth"); } } - public static ImageSource Ladder { get { return load_resource_and_freeze("Ladder"); } } + public static ImageSource Admin { get { return (DrawingImage)_resources["Admin"]; } } + public static ImageSource Analytics { get { return (DrawingImage)_resources["Analytics"]; } } + public static ImageSource Sheets { get { return (DrawingImage)_resources["Sheets"]; } } + public static ImageSource Trades { get { return (DrawingImage)_resources["Trades"]; } } + public static ImageSource Depth { get { return (DrawingImage)_resources["Depth"]; } } + public static ImageSource Ladder { get { return (DrawingImage)_resources["Ladder"]; } } public static ImageSource SheetsRed { get { return load_image("SheetsRed.png"); } } public static ImageSource SheetsGreen { get { return load_image("SheetsGreen.png"); } } - public static ImageSource Spreads { get { return load_resource_and_freeze("Spreads"); } } + public static ImageSource Spreads { get { return (DrawingImage)_resources["Spreads"]; } } public static ImageSource BlueSpreads { get { return load_image("BlueSpread.ico"); } } - public static ImageSource Reports { get { return load_resource_and_freeze("Reports"); } } - public static ImageSource Risk { get { return load_resource_and_freeze("Risk"); } } - public static ImageSource Eye { get { return load_resource_and_freeze("Eye"); } } - public static ImageSource Coins { get { return load_resource_and_freeze("Coins"); } } + public static ImageSource Reports { get { return (DrawingImage)_resources["Reports"]; } } + public static ImageSource Risk { get { return (DrawingImage)_resources["Risk"]; } } + public static ImageSource Eye { get { return (DrawingImage)_resources["Eye"]; } } + public static ImageSource Coins { get { return (DrawingImage)_resources["Coins"]; } } public static ImageSource DataCopy { get { return load_image("DataCopy.ico"); } } @@ -60,10 +60,10 @@ namespace mbts.ui { public static ImageSource Date { get { return load_image("Date.png"); } } - public static ImageSource Cube3d { get { return load_resource_and_freeze("Cube"); } } + public static ImageSource Cube3d { get { return (DrawingImage)_resources["Cube"]; } } public static ImageSource Algo { get { return Cube3d; } } public static ImageSource Quote { get { return Math; } } - public static ImageSource Math { get { return load_resource_and_freeze("Surface"); } } + public static ImageSource Math { get { return (DrawingImage)_resources["Surface"]; } } public static ImageSource Abacus { get { return load_image("Abacus.png"); } } public static ImageSource Delete { get { return load_image("Delete.png"); } } @@ -75,57 +75,60 @@ namespace mbts.ui { #pragma warning restore CS0618 // Vector icons - public static ImageSource Bookmarks { get { return load_resource_and_freeze("Bookmarks"); } } - public static ImageSource Window { get { return load_resource_and_freeze("Window"); } } - public static ImageSource ActiveWindow { get { return load_resource_and_freeze("ActiveWindow"); } } - public static ImageSource Trash { get { return load_resource_and_freeze("Trash"); } } - public static ImageSource Trash2 { get { return load_resource_and_freeze("Trash2"); } } - public static ImageSource Crucible { get { return load_resource_and_freeze("Crucible"); } } + public static ImageSource Bookmarks { get { return (DrawingImage)_resources["Bookmarks"]; } } + public static ImageSource Window { get { return (DrawingImage)_resources["Window"]; } } + public static ImageSource ActiveWindow { get { return (DrawingImage)_resources["ActiveWindow"]; } } + public static ImageSource Trash { get { return (DrawingImage)_resources["Trash"]; } } + public static ImageSource Trash2 { get { return (DrawingImage)_resources["Trash2"]; } } + public static ImageSource Crucible { get { return (DrawingImage)_resources["Crucible"]; } } - public static ImageSource Accounting { get { return load_resource_and_freeze("Accounting"); } } - public static ImageSource Add { get { return load_resource_and_freeze("Add"); } } - public static ImageSource AddEncircled { get { return load_resource_and_freeze("AddEncircled"); } } - public static ImageSource Beaker { get { return load_resource_and_freeze("Beaker"); } } - public static ImageSource Book { get { return load_resource_and_freeze("Book"); } } + public static ImageSource Accounting { get { return (DrawingImage)_resources["Accounting"]; } } + public static ImageSource Add { get { return (DrawingImage)_resources["Add"]; } } + public static ImageSource AddEncircled { get { return (DrawingImage)_resources["AddEncircled"]; } } + public static ImageSource Beaker { get { return (DrawingImage)_resources["Beaker"]; } } + public static ImageSource Book { get { return (DrawingImage)_resources["Book"]; } } public static ImageSource Orders => create_icon(OrdersGeometryKey); - public static ImageSource AddOrders { get { return load_resource_and_freeze("AddOrders"); } } - public static ImageSource CancelOrders { get { return load_resource_and_freeze("CancelOrders"); } } - public static ImageSource CancelBuys { get { return load_resource_and_freeze("CancelBuys"); } } - public static ImageSource CancelSells { get { return load_resource_and_freeze("CancelSells"); } } - public static ImageSource CancelSelected { get { return load_resource_and_freeze("CancelSelected"); } } + public static ImageSource AddOrders { get { return (DrawingImage)_resources["AddOrders"]; } } + public static ImageSource CancelOrders { get { return (DrawingImage)_resources["CancelOrders"]; } } + public static ImageSource CancelBuys { get { return (DrawingImage)_resources["CancelBuys"]; } } + public static ImageSource CancelSells { get { return (DrawingImage)_resources["CancelSells"]; } } + public static ImageSource CancelSelected { get { return (DrawingImage)_resources["CancelSelected"]; } } - public static ImageSource LadderCancelOrders { get { return load_resource_and_freeze("LadderCancelOrders"); } } - public static ImageSource LadderCancelBuys { get { return load_resource_and_freeze("LadderCancelBuys"); } } - public static ImageSource LadderCancelSells { get { return load_resource_and_freeze("LadderCancelSells"); } } + public static ImageSource LadderCancelOrders { get { return (DrawingImage)_resources["LadderCancelOrders"]; } } + public static ImageSource LadderCancelBuys { get { return (DrawingImage)_resources["LadderCancelBuys"]; } } + public static ImageSource LadderCancelSells { get { return (DrawingImage)_resources["LadderCancelSells"]; } } - public static ImageSource Synthetics { get { return load_resource_and_freeze("Synthetics"); } } - public static ImageSource AddSynthetic { get { return load_resource_and_freeze("AddSynthetic"); } } - public static ImageSource RemoveSynthetic { get { return load_resource_and_freeze("RemoveSynthetic"); } } - public static ImageSource ModifySynthetic { get { return load_resource_and_freeze("ModifySynthetic"); } } - public static ImageSource CloneSynthetic { get { return load_resource_and_freeze("CloneSynthetic"); } } + public static ImageSource Synthetics { get { return (DrawingImage)_resources["Synthetics"]; } } + public static ImageSource AddSynthetic { get { return (DrawingImage)_resources["AddSynthetic"]; } } + public static ImageSource RemoveSynthetic { get { return (DrawingImage)_resources["RemoveSynthetic"]; } } + public static ImageSource ModifySynthetic { get { return (DrawingImage)_resources["ModifySynthetic"]; } } + public static ImageSource CloneSynthetic { get { return (DrawingImage)_resources["CloneSynthetic"]; } } - public static ImageSource ModifyOrder { get { return load_resource_and_freeze("ModifyOrder"); } } - public static ImageSource Clock { get { return load_resource_and_freeze("Clock"); } } - public static ImageSource Defrag { get { return load_resource_and_freeze("Defrag"); } } - public static ImageSource Display { get { return load_resource_and_freeze("Display"); } } + public static ImageSource ModifyOrder { get { return (DrawingImage)_resources["ModifyOrder"]; } } + public static ImageSource Clock { get { return (DrawingImage)_resources["Clock"]; } } + public static ImageSource Defrag { get { return (DrawingImage)_resources["Defrag"]; } } + public static ImageSource Display { get { return (DrawingImage)_resources["Display"]; } } public static ImageSource Draggable => create_icon(DraggableGeometryKey); public static ImageSource Export => load_resource_and_freeze("Export"); - public static ImageSource Filter { get { return load_resource_and_freeze("Filter"); } } - public static ImageSource FilterActive { get { return load_resource_and_freeze("FilterActive"); } } - public static ImageSource FilterError { get { return load_resource_and_freeze("FilterError"); } } + public static ImageSource Filter { get { return (DrawingImage)_resources["Filter"]; } } + public static ImageSource FilterActive { get { return (DrawingImage)_resources["FilterActive"]; } } + public static ImageSource FilterError { get { return (DrawingImage)_resources["FilterError"]; } } public static ImageSource Gear => create_icon(GearGeometryKey); public static ImageSource Import => load_resource_and_freeze("Import"); - public static ImageSource Knight { get { return load_resource_and_freeze("Knight"); } } + public static ImageSource Knight { get { return (DrawingImage)_resources["Knight"]; } } public static ImageSource Payment => create_icon(PaymentGeometryKey); - public static ImageSource Spawn { get { return load_resource_and_freeze("Spawn"); } } - public static ImageSource Target { get { return load_resource_and_freeze("Target"); } } - public static ImageSource Tetris { get { return load_resource_and_freeze("Tetris"); } } + public static ImageSource Spawn { get { return (DrawingImage)_resources["Spawn"]; } } + public static ImageSource Target { get { return (DrawingImage)_resources["Target"]; } } + public static ImageSource Tetris { get { return (DrawingImage)_resources["Tetris"]; } } public static ImageSource Tools => create_icon(ToolsGeometryKey); public static ImageSource ViewSettings => load_resource_and_freeze("ViewSettings"); - public static ImageSource Wrench { get { return load_resource_and_freeze("Wrench"); } } + public static ImageSource Wrench { get { return (DrawingImage)_resources["Wrench"]; } } public static ImageSource RightArrow { get { return (load_resource_and_freeze("RightArrow")); } } public static ImageSource DownArrow { get { return (load_resource_and_freeze("DownArrow")); } } + public static ImageSource Factory { get { return (load_resource_and_freeze("Factory")); } } + public static ImageSource DotsVertical { get { return (load_resource_and_freeze("DotsVertical")); } } + public static ImageSource HandPointingOverDocument => load_resource_and_freeze("HandPointingOverDocument"); public static ImageSource Support => load_resource_and_freeze("SupportIcon"); public static ImageSource create_icon(string geometric_path_key) => diff --git a/src/client/spark/Images/Vector/Icons.xaml b/src/client/spark/Images/Vector/Icons.xaml index 7460c5d49..d6341a5a2 100644 --- a/src/client/spark/Images/Vector/Icons.xaml +++ b/src/client/spark/Images/Vector/Icons.xaml @@ -813,8 +813,48 @@ M0,0L63.999001,0 63.999001,48.792999 0,48.792999z"> + + + + + + + + + - - - - - + + - - - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/client/spark/Modules/Spreader/SyntheticManagementView.xaml.cs b/src/client/spark/Modules/Spreader/SyntheticManagementView.xaml.cs index 8714268be..c3b7e6bea 100644 --- a/src/client/spark/Modules/Spreader/SyntheticManagementView.xaml.cs +++ b/src/client/spark/Modules/Spreader/SyntheticManagementView.xaml.cs @@ -1,12 +1,17 @@ -using mbts.spark.filters; +using mbts.spark; +using mbts.ui.controls; using System.Windows.Controls; namespace mbts.ui.spark { /// /// Interaction logic for SyntheticManagementView.xaml /// - internal partial class SyntheticManagementView : UserControl { + partial class SyntheticManagementView : UserControl { + + public enum Columns { Name, Description, n_legs, n_working_orders, SecurityKey} public SyntheticManagementView() => InitializeComponent(); } + + class SyntheticManagementRowView : DIYGrid.HighPerformanceRow { } } \ No newline at end of file diff --git a/src/client/spark/Modules/Spreader/SyntheticManagementWindow.cs b/src/client/spark/Modules/Spreader/SyntheticManagementWindow.cs index 8bccc80d9..d7efda3eb 100644 --- a/src/client/spark/Modules/Spreader/SyntheticManagementWindow.cs +++ b/src/client/spark/Modules/Spreader/SyntheticManagementWindow.cs @@ -2,8 +2,11 @@ using bts.api; using bts.shell; using bts.spark; +using mbts.persist; using mbts.ui; +using mbts.ui.settings; using mbts.ui.spark; +using mbts.ui.utility; using mbts.utils; using System; using System.Collections.Generic; @@ -13,27 +16,33 @@ using System.Windows; using static bts.shell.SyntheticManagementWindowFactory; namespace mbts.spark { - class SyntheticManagementWindow : SparkWindow { + class SyntheticManagementWindow : SparkWindow, ISlowWindow { const string _window_type = "SyntheticManagement"; SingleValuePump _vm = new SingleValuePump(); StartupData _state; OrdersModel _orders_model; SyntheticManagementView _view; IDisposable _subscriptions; + IReadOnlyList _commands; + readonly IWindowSettingsManager _settings_manager; - public static SyntheticManagementWindow create(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state) { - var window = new SyntheticManagementWindow(factory, bus, orders_model); - window.initialize(state); - return window; - } - SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model) + public SyntheticManagementWindow(SyntheticManagementWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state, WindowSettingsManager settings_manager) : base("Synthetic Management", bus, factory, SparkCommands.ShowSyntheticManagement.icon) { _orders_model = orders_model; + _state = state; + _settings_manager = settings_manager; + Icon.Freeze(); + } + + public void thread_init() { + Log.notnull(_state); + var vm_connectable_builder = new ConnectableSeriesBuilder(_vm.observable); var current_row_source = vm_connectable_builder.create(vm => vm.current_row_observable); var current_row_connectable_builder = new ConnectableSeriesBuilder(current_row_source); - Commands = new SparkCommand[] { + + _commands = new SparkCommand[] { SparkCommand.create("Add", Icons.AddSynthetic, SparkTags.ShowSyntheticCreateTag).complete( () => new SecurityCriteria(), () => true ), SparkCommand.create("Remove", Icons.RemoveSynthetic, SparkTags.SyntheticDeleteTag).complete( @@ -51,7 +60,24 @@ namespace mbts.spark { filters = new OrderBookWindowFactory.FilterStates { security_criteria = new SecurityCriteria(_vm.value.row_vms.Select(r => r.security)) } - }) + } + ), SparkCommands.window_settings_command(new DialogCommand(() => { + var vm = new WindowSettingsDialogVM( + _settings_manager, + us => us.synthetic_management_grid, + UserSettings.FileExtensions.GRID, + s => _vm.value.grid_settings_manager.create_vm( + // we need to get the most up to date GridSettings when launching b/c users may + // have changed some settings on the grid directly, like column widths. + s.with(_vm.value.columns.Select( c => c.create())), + this.observe_title() + ) + ); + // TODO: bind to main window's Title + var dialog = new SimpleGridSettingsDialog { Title = "Synthetic Management Grid Settings", DataContext = vm }; + vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog); + return dialog; + })) }; _subscriptions = Disposable.Create( @@ -69,21 +95,21 @@ namespace mbts.spark { uint select_security_id(RowModificationParams? rmp) => rmp.HasValue ? rmp.Value.security.Id : 0; (SecurityImmutable, ExecutionStrategyImmutable) select_row_strategy_tuple(RowStrategy? rs) => rs.HasValue ? (rs.Value.security, rs.Value.strategy) : default; bool can_execute(RowModificationParams? rmp) => rmp.HasValue && !rmp.Value.has_working_orders; - } - - public void initialize(StartupData state) { - Log.notnull(state); - _state = state; - _vm.value = new SyntheticManagementDialogVM(_bus, _orders_model, state.sorting_criteria, state.filters ?? new FilterStates()); + + _vm.value = new SyntheticManagementDialogVM(_bus, _orders_model, _settings_manager, _state.sorting_criteria, _state.filters ?? new FilterStates()); if (_view == null) _view = new SyntheticManagementView { DataContext = _vm.value }; else _view.DataContext = _vm.value; } public override string window_type => _window_type; public override UIElement Content => _view; - public override IEnumerable Commands { get; } - public override StartupData SaveState => _state = new StartupData(_vm.value.rows_view.GetSortingCriteria(), _vm.value.get_filter_states()); - public override double MinWidth => 700; + public override IEnumerable Commands => _commands; + public override StartupData SaveState => _state = _vm.value.create(); + public override double MinWidth => + StartupData.DefaultGridSettings.columns.Sum(gcs => gcs.width) + + SPARK_MENU_COLLAPSED_WIDTH + + LAUNCHER_BUTTON_BORDER_WIDTH + + EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH; public override void Dispose() { Disposable.dispose(ref _vm); diff --git a/src/client/spark/Modules/Spreader/SyntheticManagementWindowFactory.cs b/src/client/spark/Modules/Spreader/SyntheticManagementWindowFactory.cs index 11eb6adb2..afb327aa7 100644 --- a/src/client/spark/Modules/Spreader/SyntheticManagementWindowFactory.cs +++ b/src/client/spark/Modules/Spreader/SyntheticManagementWindowFactory.cs @@ -1,30 +1,46 @@ using bts.api; +using mbts.persist; using mbts.spark; using mbts.spark.filters; using mbts.ui; +using mbts.ui.settings; using ProtoBuf; using System; using System.Collections.Generic; using System.Reactive.Linq; using System.Threading.Tasks; +using static mbts.ui.controls.DIYGrid; +using static mbts.ui.spark.SyntheticManagementView; namespace bts.shell { class SyntheticManagementWindowFactory : ShellWindowFactory { readonly IBus _bus; readonly OrdersModel _orders_model; + readonly UserSettingsHub _settings_hub; - public SyntheticManagementWindowFactory(SpreaderShellModule module, IBus bus, OrdersModel orders_model) : base(module, "SyntheticManagement") { + public SyntheticManagementWindowFactory(SpreaderShellModule module, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(module, "SyntheticManagement") { _bus = bus; _orders_model = orders_model; + _settings_hub = settings_hub; _dispose = bus.subscribe(SparkTags.ShowSyntheticManagementTag).OnDispatcher().Subscribe(show); } + public static string col_id(Columns col) => col.ToString(); + public override Task load(StartupData state) => - Task.FromResult(SyntheticManagementWindow.create(this, _bus, _orders_model, state)); + Task.FromResult(_create(state)); public async void show(SparkTags.Nothing nothing) => _bus.post(SparkTags.ShowWindow, await load(new StartupData())); + IShellWindow _create(StartupData state) { + var settings_manager = new WindowSettingsManager(_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); + } + [ProtoContract] public class StartupData { public const int Version = 1; @@ -33,11 +49,41 @@ namespace bts.shell { public readonly List sorting_criteria; [ProtoMember(2)] public readonly FilterStates filters; + [ProtoMember(3)] + public readonly GridSettings grid; - public StartupData() : this(new List(), null) { } - public StartupData(List sorting_criteria, FilterStates filters) { + public StartupData() : this(new List(), null, null) { } + public StartupData(List sorting_criteria, FilterStates filters, GridSettings grid = default) { this.sorting_criteria = sorting_criteria; this.filters = filters; + this.grid = grid; + } + + public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion); + + static class DefaultColumns { + public static GridColumnSettings name => new GridColumnSettings(col_id(Columns.Name), DefaultColumnHeaders.Name, width: DefaultColumnWidths.Security); + public static GridColumnSettings description => new GridColumnSettings(col_id(Columns.Description), DefaultColumnHeaders.Description, width: DefaultColumnWidths.Description); + public static GridColumnSettings n_legs => new GridColumnSettings(col_id(Columns.n_legs), DefaultColumnHeaders.N_Legs, width: DefaultColumnWidths.N_Legs); + public static GridColumnSettings n_working_orders => new GridColumnSettings(col_id(Columns.n_working_orders), DefaultColumnHeaders.N_Working_Orders, width: DefaultColumnWidths.N_Working_Orders); + public static GridColumnSettings security_key => new GridColumnSettings(col_id(Columns.SecurityKey), DefaultColumnHeaders.SecurityKey, width: DefaultColumnWidths.SecurityKey); + + public static IEnumerable default_sorting_criterion { + get { + yield return new SortingCriteria(name.id, true); + } + } + + public static IEnumerable all { + get { + yield return name; + yield return description; + yield return n_legs; + yield return n_working_orders; + yield return security_key; + } + } + } } diff --git a/src/client/spark/Modules/Spreader/VM/ParameterRows/Leg/MarketDataCalculationModel.cs b/src/client/spark/Modules/Spreader/VM/ParameterRows/Leg/MarketDataCalculationModel.cs new file mode 100644 index 000000000..f526589a3 --- /dev/null +++ b/src/client/spark/Modules/Spreader/VM/ParameterRows/Leg/MarketDataCalculationModel.cs @@ -0,0 +1,18 @@ +using mbts.ui.spark; +using LegMarketDataCalculation = bts.spark.LegSpreaderParams.Types.LegMarketDataCalculation; + +namespace mbts.spark { + partial class SyntheticEditorVM { + class MarketDataCalculationModel : RowModelBase { + public override string display_name { get; } = "Market Data Calculation"; + public override string help_text { get; } + public override string man_page { get; } = string.Empty; + + public override LegMarketDataCalculation get_value(LegSpreaderParamsTuple leg_tup) => + leg_tup.leg_params.LegMdCalc; + + public override void set_value(LegSpreaderParamsTuple leg_tup, LegMarketDataCalculation value) => + leg_tup.leg_params.LegMdCalc = value; + } + } +} \ No newline at end of file diff --git a/src/client/spark/Modules/Spreader/VM/ParameterRows/Leg/MarketDataTypeModel.cs b/src/client/spark/Modules/Spreader/VM/ParameterRows/Leg/MarketDataTypeModel.cs new file mode 100644 index 000000000..4e1125380 --- /dev/null +++ b/src/client/spark/Modules/Spreader/VM/ParameterRows/Leg/MarketDataTypeModel.cs @@ -0,0 +1,18 @@ +using mbts.ui.spark; +using MarketDataType = bts.spark.LegSpreaderParams.Types.MarketDataType; + +namespace mbts.spark { + partial class SyntheticEditorVM { + class MarketDataTypeModel : RowModelBase { + public override string display_name { get; } = "Market Data Type"; + public override string help_text { get; } + public override string man_page { get; } = string.Empty; + + public override MarketDataType get_value(LegSpreaderParamsTuple leg_tup) => + leg_tup.leg_params.MarketDataType; + + public override void set_value(LegSpreaderParamsTuple leg_tup, MarketDataType value) => + leg_tup.leg_params.MarketDataType = value; + } + } +} \ No newline at end of file diff --git a/src/client/spark/Modules/Spreader/VM/SyntheticEditorVM.cs b/src/client/spark/Modules/Spreader/VM/SyntheticEditorVM.cs index 96d766b6b..2b162676a 100644 --- a/src/client/spark/Modules/Spreader/VM/SyntheticEditorVM.cs +++ b/src/client/spark/Modules/Spreader/VM/SyntheticEditorVM.cs @@ -203,6 +203,8 @@ namespace mbts.spark { new IsQuoteModel(), new HedgeRatioModel(), new QuoteHiddenModel(), + new MarketDataTypeModel(), + new MarketDataCalculationModel(), new QuoteThrottleEnabledModel(quote_throttle_inside_ticks, quote_throttle_outside_ticks), quote_throttle_inside_ticks, quote_throttle_outside_ticks, diff --git a/src/client/spark/Modules/Spreader/VM/SyntheticManagementDialogVM.cs b/src/client/spark/Modules/Spreader/VM/SyntheticManagementDialogVM.cs index d8d6a31dd..489ba2935 100644 --- a/src/client/spark/Modules/Spreader/VM/SyntheticManagementDialogVM.cs +++ b/src/client/spark/Modules/Spreader/VM/SyntheticManagementDialogVM.cs @@ -1,22 +1,31 @@ using bts.api; using bts.shell; using bts.spark; +using mbts.persist; using mbts.spark.filters; using mbts.ui; +using mbts.ui.controls; +using mbts.ui.settings; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; +using System.Windows.Data; using System.Windows.Input; using static bts.shell.SyntheticManagementWindowFactory; +using static mbts.ui.controls.DIYGrid; +using static mbts.ui.spark.SyntheticManagementView; namespace mbts.spark { class SyntheticManagementDialogVM : SecurityMDGrid { readonly IBus _bus; readonly OrdersModel _orders_model; + IWindowSettingsManager _settings_manager; - public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model) : this(bus, orders_model, null, null) { } - public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model, IReadOnlyCollection sorting, FilterStates filters) + + public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model, IWindowSettingsManager settings_manager) : this(bus, orders_model, settings_manager, null, null) { } + public SyntheticManagementDialogVM(IBus bus, OrdersModel orders_model, IWindowSettingsManager settings_manager, IReadOnlyCollection sorting, FilterStates filters) : base(orders_model.spreader.strategy_tuple.Select(map => map.Values), sorting) { _bus = bus; _orders_model = orders_model; @@ -28,7 +37,41 @@ namespace mbts.spark { n_working_orders_filter.load_state(filters.n_working_orders); security_key_filter.load_state(filters.security_key); } - _dispose = DataGridFilterHelper.hook_filters(rows_view, create_time_filter, name_filter, description_filter, n_legs_filter, n_working_orders_filter, security_key_filter); + _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( + 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); + + // + void add_col(Columns col_key, string header, Func get_text, string prop, IDataGridColumnFilter filter, string filter_template, double width) => + columns.Add(new RenderTextColumn(col_key, get_text, filter, filter_template) { + id = col_id(col_key), + Header = header, + sort_member_path = prop, + Width = width + }); + // + grid_settings_manager = new SimpleGridSettingsManager(columns, new Dictionary { }, 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), + settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager) + ); } protected override Row row_factory((ExecutionStrategyImmutable? strategy, SecurityMD md) row_model) => @@ -36,6 +79,20 @@ namespace mbts.spark { protected override void on_row_removed(Row row) => row.Dispose(); + public ObservableCollection columns { get; } = new ObservableCollection(); + public SimpleGridSettingsManager 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 create_time_filter { get; } = new TimeFilterVM(r => r.create_time, nameof(Row.create_time)); public IDataGridColumnPersistentFilter name_filter { get; } = new RegexFilterVM(r => r.name, nameof(Row.name)); @@ -44,7 +101,7 @@ namespace mbts.spark { public IDataGridColumnPersistentFilter n_working_orders_filter { get; } = new UIntMinMax(r => r.n_working_orders, nameof(Row.n_legs)); public IDataGridColumnPersistentFilter security_key_filter { get; } = new RegexFilterVM(r => r.security_key.ToString(), nameof(Row.security_key)); - public FilterStates get_filter_states() => + FilterStates _get_filter_states() => new FilterStates { description = description_filter.get_state(), create_time = create_time_filter.get_state(), @@ -55,7 +112,7 @@ namespace mbts.spark { }; #endregion - public class Row : SecurityMDRow, IEquatable { + public class Row : SecurityMDRow, IEquatable, IRowVM, IActionableRowVM { public readonly ExecutionStrategyImmutable? strategy; readonly IBus _bus; @@ -85,6 +142,17 @@ namespace mbts.spark { 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 row_animations => new ObservableCollection(); + + public IReadOnlyList cell_stores => new IGridCellStore[0]; + + + public event Action invalidated; + + public void raise_invalidated() => invalidated?.Invoke(); public bool Equals(Row other) => other != null && @@ -96,6 +164,8 @@ namespace mbts.spark { public override int GetHashCode() => security_key.GetHashCode(); + + void _show_ladder() => _bus.post(SparkTags.ShowLadderTag, md); diff --git a/src/client/spark/Modules/Trading/Fills/FillsVM.cs b/src/client/spark/Modules/Trading/Fills/FillsVM.cs index f4de61a8c..ab88250b9 100644 --- a/src/client/spark/Modules/Trading/Fills/FillsVM.cs +++ b/src/client/spark/Modules/Trading/Fills/FillsVM.cs @@ -1,6 +1,7 @@ using bts.api; using bts.api.orderman; using bts.spark; +using mbts.persist; using mbts.spark.filters; using mbts.spark.utilities; using mbts.ui; @@ -13,6 +14,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Windows.Data; using static bts.api.orderman.Fill.Types; using static bts.shell.FillsWindowFactory; @@ -23,13 +25,16 @@ using OrderType = bts.api.orderman.CustomOrderDefinition.CustomOneofCase; namespace mbts.spark { class FillsVM : SecurityGrid { readonly OrdersModel _model; - - public FillsVM(IBus bus, OrdersModel model, StartupData state) - : this(bus, model, _get_observable(bus, model, state), state.sorting_criteria, state.filters) { } + IWindowSettingsManager _settings_manager; - FillsVM(IBus bus, OrdersModel model, IObservable> fills_source, IReadOnlyCollection sorting_criteria, FilterStates filters) - : base(RXCollection.CreateIncrementalUpdate(fills_source), sorting_criteria) { + + public FillsVM(IBus bus, OrdersModel model, IObservable state_obs, IWindowSettingsManager settings_manager) + : this(bus, model, state_obs, _get_observable(bus, model, state_obs), settings_manager) { } + + FillsVM(IBus bus, OrdersModel model, IObservable state_obs, IConnectableObservable> fills_source, IWindowSettingsManager settings_manager) + : base(RXCollection.CreateIncrementalUpdate(fills_source), null) { _model = model; + _settings_manager = settings_manager; account_filter = DiscreteValuesFilterVM.Create(_model.factory.subscribe_accounts().Select(d => d.Values), r => r.account, nameof(Row.account)); user_filter = DiscreteValuesFilterVM.Create(_model.factory.subscribe_users().Select(d => d.Values), r => r.user, nameof(Row.user)); security_filter = DiscreteValuesFilterVM.Create( @@ -44,30 +49,6 @@ namespace mbts.spark { SecurityDataHelper.EqualityComparer.IDComparer, Comparer.Default ); - if (filters != null) { - fill_time_filter.load_state(filters.fill_time); - time_in_force_filter.load_state(filters.time_in_force); - order_type_filter.load_state(filters.order_type); - account_filter.load_state(filters.account); - user_filter.load_state(filters.user); - exchange_filter.load_state(filters.exchange); - security_filter.load_state(filters.security); - side_filter.load_state(filters.side); - price_filter.load_state(filters.price); - size_filter.load_state(filters.size); - fill_kind_filter.load_state(filters.fill_kind); - fill_key_filter.load_state(filters.fill_key); - order_key_filter.load_state(filters.order_key); - security_key_filter.load_state(filters.security_key); - } else { - fill_time_filter.load_state( - new TimeFilterTuple(ui.controls.TimeFilterMode.CMEOpen, - TimeTextTuple.FromDateTime(DateTime.Today), - TimeTextTuple.FromDateTime(DateTime.Today.AddDays(1)) - ) - ); - fill_kind_filter.load_state(EnumHelper.get_values_except(FillKind.Sod).ToArray()); - } // columns add_col(Columns.Security, DefaultColumnHeaders.Security, r => r.security.ToString(), nameof(Row.security), security_filter, DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Security); add_col(Columns.FillTime, DefaultColumnHeaders.FillTime, r => r.fill_time.ToString("M/d h:mm:ss tt"), nameof(Row.fill_time), fill_time_filter, DataGridFilterKnownTemplates.TimeFilter, DefaultColumnWidths.Time); @@ -75,30 +56,83 @@ namespace mbts.spark { add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, nameof(Row.account), account_filter, DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Account); add_col(Columns.Exchange, DefaultColumnHeaders.Exchange, r => r.exchange.ToString(), nameof(Row.exchange), exchange_filter, DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.Exchange); add_col(Columns.TimeInForce, DefaultColumnHeaders.TimeInForce, r => r.time_in_force.ToString(), nameof(Row.time_in_force), time_in_force_filter, DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.TimeInForce); - columns.Add(new OrderTypeColumn(Columns.OrderType, new Binding(nameof(Row.order_type)), new Binding(nameof(Row.order_metadata)), order_type_filter) { sort_member_path = nameof(Row.order_type) }); - columns.Add(new SideColumn(Columns.Side, r => r.side, side_filter) { sort_member_path = nameof(Row.side) }); + columns.Add(new OrderTypeColumn(Columns.OrderType, new Binding(nameof(Row.order_type)), new Binding(nameof(Row.order_metadata)), order_type_filter) { id = col_id(Columns.OrderType), sort_member_path = nameof(Row.order_type) }); + columns.Add(new SideColumn(Columns.Side, r => r.side, side_filter) { id = col_id(Columns.Side), sort_member_path = nameof(Row.side) }); add_col(Columns.Size, DefaultColumnHeaders.Size, r => r.size.ToString(), nameof(Row.size), size_filter, DataGridFilterKnownTemplates.UIntMinMax, DefaultColumnWidths.Size); add_col(Columns.Price, DefaultColumnHeaders.Price, r => r.format_price(r.price), nameof(Row.price), price_filter, DataGridFilterKnownTemplates.DoubleMinMax, DefaultColumnWidths.Price); - add_col(Columns.FillKind, "Fill Kind", r => r.fill_kind.ToString(), nameof(Row.fill_kind), fill_kind_filter, DataGridFilterKnownTemplates.DiscreteValues, 70); + add_col(Columns.FillKind, DefaultColumnHeaders.FillKind, r => r.fill_kind.ToString(), nameof(Row.fill_kind), fill_kind_filter, DataGridFilterKnownTemplates.DiscreteValues, DefaultColumnWidths.FillKind); add_col(Columns.FillKey, DefaultColumnHeaders.FillKey, r => r.fill_key.ToString(), nameof(Row.fill_key), fill_key_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.FillKey); add_col(Columns.OrderKey, DefaultColumnHeaders.OrderKey, r => r.order_key.ToString(), nameof(Row.order_key), order_key_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.OrderKey); add_col(Columns.SecurityKey, DefaultColumnHeaders.SecurityKey, r => r.security_key.ToString(), nameof(Row.security_key), security_key_filter, DataGridFilterKnownTemplates.Regex, DefaultColumnWidths.SecurityKey); // void add_col(Columns col_key, string header, Func get_text, string prop, IDataGridColumnFilter filter, string filter_template, double width) => - columns.Add(new RenderTextColumn(col_key, get_text, filter, filter_template) { + columns.Add(new RenderTextColumn(col_key, get_text, filter, filter_template) { + id = col_id(col_key), Header = header, sort_member_path = prop, Width = width }); + grid_settings_manager = new SimpleGridSettingsManager(columns, new Dictionary { }, 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.Add(DataGridFilterHelper.hook_filters(rows_view, - fill_time_filter, time_in_force_filter, order_type_filter, account_filter, user_filter, exchange_filter, - security_filter, side_filter, price_filter, size_filter, fill_kind_filter, fill_key_filter, order_key_filter, security_key_filter - )); + _resources.AddRange( + state_obs.OnDispatcher().Subscribe(s => + { + using (rows_view.DeferRefresh()) { + if (s.grid != null) { + // this means we are not using defaults, + // Note: settings_manager in turn notifies grid_settings_manager of this update + _settings_manager.instance_settings = s.grid; + } + if (s.filters is FilterStates filters) { + fill_time_filter.load_state(filters.fill_time); + time_in_force_filter.load_state(filters.time_in_force); + order_type_filter.load_state(filters.order_type); + account_filter.load_state(filters.account); + user_filter.load_state(filters.user); + exchange_filter.load_state(filters.exchange); + security_filter.load_state(filters.security); + side_filter.load_state(filters.side); + price_filter.load_state(filters.price); + size_filter.load_state(filters.size); + fill_kind_filter.load_state(filters.fill_kind); + fill_key_filter.load_state(filters.fill_key); + order_key_filter.load_state(filters.order_key); + security_key_filter.load_state(filters.security_key); + } else { + fill_time_filter.load_state( + new TimeFilterTuple(ui.controls.TimeFilterMode.CMEOpen, + TimeTextTuple.FromDateTime(DateTime.Today), + TimeTextTuple.FromDateTime(DateTime.Today.AddDays(1)) + ) + ); + fill_kind_filter.load_state(EnumHelper.get_values_except(FillKind.Sod).ToArray()); + } + rows_view.set_sort_descriptions(s.sorting_criterion, columns); + } + }), + DataGridFilterHelper.hook_filters(rows_view, + fill_time_filter, time_in_force_filter, order_type_filter, account_filter, user_filter, exchange_filter, + security_filter, side_filter, price_filter, size_filter, fill_kind_filter, fill_key_filter, order_key_filter, security_key_filter + ), + settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager), + fills_source.Connect() + ); } public ObservableCollection columns { get; } = new ObservableCollection(); + public SimpleGridSettingsManager grid_settings_manager { get; } + + public GridSettings get_grid_save_state() { + if (_settings_manager.instance_settings == null) + return null; + else + return grid_settings_manager.create(); + } + #region Filters public IDataGridColumnPersistentFilter fill_time_filter { get; } = new TimeFilterVM(r => r.fill_time, nameof(Row.fill_time)); public IDataGridColumnPersistentFilter time_in_force_filter { get; } = DiscreteValuesFilterVM.Create(EnumHelper.get_values().ToLArray(), r => r.time_in_force, nameof(Row.time_in_force)); @@ -144,6 +178,9 @@ namespace mbts.spark { } else return model.on_fill_tuples; } + static IConnectableObservable> _get_observable(IBus bus, OrdersModel model, IObservable state) => + state.SelectManyInSeries(s => _get_observable(bus, model, s)); + public class Row : SecurityRow, IEquatable, IRowVM { readonly OrdersModel _model; readonly FillImmutable _fill; diff --git a/src/client/spark/Modules/Trading/Fills/FillsWindow.cs b/src/client/spark/Modules/Trading/Fills/FillsWindow.cs index 325ac30f3..cb4e56609 100644 --- a/src/client/spark/Modules/Trading/Fills/FillsWindow.cs +++ b/src/client/spark/Modules/Trading/Fills/FillsWindow.cs @@ -2,9 +2,16 @@ using bts.api; using bts.shell; using bts.spark; +using mbts.persist; using mbts.ui; +using mbts.ui.settings; using mbts.ui.spark; +using mbts.ui.utility; using mbts.utils; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reactive.Linq; using System.Windows; using StartupData = bts.shell.FillsWindowFactory.StartupData; @@ -13,29 +20,76 @@ namespace mbts.spark { const string _window_type = "Fills", _thread_id = "Fills"; readonly AllowOnce _init_guard = new AllowOnce("Fills"); readonly OrdersModel _orders_model; + readonly IWindowSettingsManager _settings_manager; FillsVM _vm; FillsView _view; StartupData _state; + IReadOnlyList _commands; + IDisposable _subscriptions; - public FillsWindow(FillsWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state) + public FillsWindow(FillsWindowFactory factory, IBus bus, OrdersModel orders_model, StartupData state, WindowSettingsManager settings_manager) : base("Fills", bus, factory, SparkCommands.ShowFills.icon) { _orders_model = orders_model; _state = state; + _settings_manager = settings_manager; } + event Action _state_changed; + public override string window_type => _window_type; public override UIElement Content => _view; - public override StartupData SaveState => _state = new StartupData(_state.orders, _vm.rows_view.GetSortingCriteria(), _vm.get_filters()); - public override double MinWidth => 1050 + SPARK_MENU_COLLAPSED_WIDTH + LAUNCHER_BUTTON_BORDER_WIDTH; + public override IEnumerable Commands => _commands; + + public override StartupData SaveState => _state = new StartupData(_state.orders, _vm.rows_view.get_sorting_criteria(_vm.columns), _vm.get_filters(), _vm.get_grid_save_state()); + public override double MinWidth => + StartupData.DefaultGridSettings.columns.Sum(gcs => gcs.width) + + SPARK_MENU_COLLAPSED_WIDTH + + LAUNCHER_BUTTON_BORDER_WIDTH + + EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH; + public string thread_id => _thread_id; public void thread_init() { _init_guard.check_access(); Log.notnull(_state); - _vm = new FillsVM(_bus, _orders_model, _state); + var state_src = Observable.FromEvent, FillsWindowFactory.StartupData>(cb => _state_changed += cb, cb => _state_changed -= cb).StartWith(_state); + _vm = new FillsVM(_bus, _orders_model, state_src, _settings_manager); _view = new FillsView { DataContext = _vm }; + + _commands = new List { + // wrap the grid settings view factory in a dialogCommand here since the vm is destroyed/recreated when the security criteria changes + SparkCommands.window_settings_command(new DialogCommand(() => { + var vm = new WindowSettingsDialogVM( + _settings_manager, + us => us.fills_grid, + UserSettings.FileExtensions.GRID, + s => _vm.grid_settings_manager.create_vm( + // we need to get the most up to date GridSettings when launching b/c users may + // have changed some settings on the grid directly, like column widths. + s.with(_vm.columns.Select( c => c.create())), + this.observe_title() + ) + ); + // TODO: bind to main window's Title + var dialog = new SimpleGridSettingsDialog { Title = $"{_window_type} Grid Settings", DataContext = vm }; + vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog); + return dialog; + })) + }; + _subscriptions = Disposable.Create(() => + _state_changed?.Invoke( + _state = new StartupData(_state.orders, + _vm.rows_view.get_sorting_criteria(_vm.columns), + _vm.get_filters(), + _vm.grid_settings_manager.create() + ) + ) + ); } - public override void Dispose() => Disposable.dispose(ref _vm); + public override void Dispose() { + Disposable.dispose(ref _vm); + Commands.OfType().DisposeAll(); + } } } \ No newline at end of file diff --git a/src/client/spark/Modules/Trading/Fills/FillsWindowFactory.cs b/src/client/spark/Modules/Trading/Fills/FillsWindowFactory.cs index a4021e500..b4e789c2b 100644 --- a/src/client/spark/Modules/Trading/Fills/FillsWindowFactory.cs +++ b/src/client/spark/Modules/Trading/Fills/FillsWindowFactory.cs @@ -1,49 +1,118 @@ using bts.api; using bts.api.orderman; +using mbts.persist; using mbts.spark; using mbts.spark.filters; using mbts.ui; +using mbts.ui.settings; using ProtoBuf; using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using static mbts.ui.controls.DIYGrid; +using static mbts.ui.spark.FillsView; namespace bts.shell { class FillsWindowFactory : ShellWindowFactory { readonly IBus _bus; readonly OrdersModel _orders_model; + readonly UserSettingsHub _settings_hub; - public FillsWindowFactory(TradingShellModule module, IBus bus, OrdersModel orders_model) + public FillsWindowFactory(TradingShellModule module, IBus bus, UserSettingsHub settings_hub, OrdersModel orders_model) : base(module, "Fills") { _bus = bus; _orders_model = orders_model; + _settings_hub = settings_hub; _dispose = bus.subscribe(SparkTags.ShowFillsTag).OnDispatcher().Subscribe(show); } + public static string col_id(Columns col) => col.ToString(); + public override Task load(StartupData state) => - Task.FromResult(new FillsWindow(this, _bus, _orders_model, state)); + Task.FromResult(_create(state)); public async void show(StartupData state) => _bus.post(SparkTags.ShowWindow, await load(state)); + IShellWindow _create(StartupData state) { + var settings_manager = new WindowSettingsManager(_settings_hub, + state.grid, + get: us => us.fills_grid, + set: (us, gs) => us.with(provided_fills_gridsettings: gs)); + return new FillsWindow(this, _bus, _orders_model, state, settings_manager); + } + [ProtoContract] public class StartupData { public const int Version = 1; - public StartupData() { } - public StartupData(uint[] orders) : this(orders, null, null) { } - public StartupData(FilterStates filters) : this(null, null, filters) { } - public StartupData(uint[] orders, List sorting_criteria, FilterStates filters) { - this.orders = orders; - this.sorting_criteria = sorting_criteria; - this.filters = filters; - } + [ProtoMember(1)] public uint[] orders; [ProtoMember(2)] - public List sorting_criteria; + List _sorting_criteria; [ProtoMember(3)] public FilterStates filters; + [ProtoMember(4)] + public GridSettings grid; + + public StartupData() { } + public StartupData(uint[] orders) : this(orders, null, null) { } + public StartupData(FilterStates filters) : this(null, null, filters) { } + public StartupData(uint[] orders, List sorting_criteria, FilterStates filters, GridSettings grid = default) { + this.orders = orders; + this._sorting_criteria = sorting_criteria; + this.filters = filters; + this.grid = grid; + } + + public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion); + public IReadOnlyList sorting_criterion => _sorting_criteria ?? (_sorting_criteria = DefaultColumns.default_sorting_criterion.ToList()); + public static StartupData Default() => + new StartupData(null, null, new FilterStates(), null); + + static class DefaultColumns { + public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security); + public static GridColumnSettings fill_time => new GridColumnSettings(col_id(Columns.FillTime), DefaultColumnHeaders.FillTime, width: DefaultColumnWidths.Time); + public static GridColumnSettings user => new GridColumnSettings(col_id(Columns.User), DefaultColumnHeaders.User, width: DefaultColumnWidths.User); + public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account); + public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, width: DefaultColumnWidths.Exchange); + public static GridColumnSettings time_in_force => new GridColumnSettings(col_id(Columns.TimeInForce), DefaultColumnHeaders.TimeInForce, width: DefaultColumnWidths.TimeInForce); + public static GridColumnSettings order_type => new GridColumnSettings(col_id(Columns.OrderType), DefaultColumnHeaders.OrderType, width: DefaultColumnWidths.OrderType); + public static GridColumnSettings side => new GridColumnSettings(col_id(Columns.Side), DefaultColumnHeaders.Side, width: DefaultColumnWidths.Side); + public static GridColumnSettings quantity => new GridColumnSettings(col_id(Columns.Size), DefaultColumnHeaders.Side, width: DefaultColumnWidths.Size, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings price => new GridColumnSettings(col_id(Columns.Price), DefaultColumnHeaders.Price, width: DefaultColumnWidths.Price, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings fill_kind => new GridColumnSettings(col_id(Columns.FillKind), DefaultColumnHeaders.FillKind, width: DefaultColumnWidths.FillKind); + public static GridColumnSettings fill_key => new GridColumnSettings(col_id(Columns.FillKey), DefaultColumnHeaders.FillKey, width: DefaultColumnWidths.FillKey); + public static GridColumnSettings order_key => new GridColumnSettings(col_id(Columns.OrderKey), DefaultColumnHeaders.OrderKey, width: DefaultColumnWidths.OrderKey); + public static GridColumnSettings security_key => new GridColumnSettings(col_id(Columns.SecurityKey), DefaultColumnHeaders.SecurityKey, width: DefaultColumnWidths.SecurityKey); + public static IEnumerable default_sorting_criterion { + get { + yield return new SortingCriteria(fill_time.id, false); + } + } + + public static IEnumerable all { + get { + yield return security; + yield return fill_time; + yield return user; + yield return account; + yield return exchange; + yield return time_in_force; + yield return order_type; + yield return side; + yield return quantity; + yield return price; + yield return fill_kind; + yield return fill_key; + yield return order_key; + yield return security_key; + } + } + } + } [ProtoContract] diff --git a/src/client/spark/Modules/Trading/LadderWindowFactory.cs b/src/client/spark/Modules/Trading/LadderWindowFactory.cs index 76a1c32e2..ff6a809de 100644 --- a/src/client/spark/Modules/Trading/LadderWindowFactory.cs +++ b/src/client/spark/Modules/Trading/LadderWindowFactory.cs @@ -4,6 +4,7 @@ using mbts.config; using mbts.persist; using mbts.spark; using mbts.ui; +using mbts.ui.spark; using ProtoBuf; using System; using System.Collections.Generic; @@ -22,15 +23,17 @@ namespace bts.shell { readonly IWindowManager _wm; readonly UserSettingsHub _settings_hub; readonly OrdersModel _orders_model; + readonly MarketDataAuxiliaryModel _md_auxiliary_model; readonly int _max_ladders_open; IReadOnlyDictionary _securities = new Dictionary(); - public LadderWindowFactory(IShellModule owner, IBus bus, IWindowManager wm, UserSettingsHub settings_hub, OrdersModel orders_model) + public LadderWindowFactory(IShellModule owner, IBus bus, IWindowManager wm, UserSettingsHub settings_hub, OrdersModel orders_model, MarketDataAuxiliaryModel md_auxiliary_model) : base(owner, "SparkLadder") { _bus = bus; _wm = wm; _settings_hub = settings_hub; _orders_model = orders_model; + _md_auxiliary_model = md_auxiliary_model; var knobs = Configuration.get(); _max_ladders_open = (int)(knobs.max_open_ladders); _security_timeout = TimeSpan.FromSeconds(Math.Max(15, knobs.security_timeout_secs)); @@ -56,7 +59,7 @@ namespace bts.shell { IShellWindow create_ladder_window(SecurityMD md, LadderSettings state_settings) { var id = Interlocked.Increment(ref _ladder_count) % Environment.ProcessorCount; var view = new Lazy(() => { - var model = new LadderModel(md, _orders_model); + var model = new LadderModel(md, _orders_model, _md_auxiliary_model); var settings_manager = new WindowSettingsManager( _settings_hub, state_settings, us => sanitize_ladder_settings(us.ladder), @@ -85,6 +88,12 @@ namespace bts.shell { s.session_high_line = LadderSettings.Default.session_high_line; if (s.session_range_fill == null) s.session_range_fill = LadderSettings.Default.session_range_fill; + if (s.volume_column == null) + s.volume_column = LadderSettings.Default.volume_column; + if (s.size_display == null) + s.size_display = MarketSizeDisplay.get_default(); + if (s.size_display.normal == null) + s.size_display = s.size_display.with(MarketSizeDisplay.get_default().normal); s.row_height = LadderRender.measure_row_height(s); return s; } diff --git a/src/client/spark/Modules/Trading/OrderBook/OrderBookColumns.cs b/src/client/spark/Modules/Trading/OrderBook/OrderBookColumns.cs index fa1695f6b..61d980532 100644 --- a/src/client/spark/Modules/Trading/OrderBook/OrderBookColumns.cs +++ b/src/client/spark/Modules/Trading/OrderBook/OrderBookColumns.cs @@ -3,6 +3,7 @@ using bts.api.orderman; using bts.utils; using mbts.spark.filters; using mbts.ui; +using mbts.ui.controls; using mbts.ui.converters; using mbts.ui.utility; using mbts.ui.utility.text; @@ -208,7 +209,7 @@ namespace mbts.spark { "Parent Key", nameof(OrderBookRowVM.parent_order_key), r => r.parent_order_key.store, - 80, + DefaultColumnWidths.ParentKey, new RegexFilterVM( r => r.parent_order_key.text, value_path(nameof(OrderBookRowVM.parent_order_key))), @@ -321,6 +322,7 @@ namespace mbts.spark { ListSortDirection? _cached_sort_direction; readonly string _filter_template; protected readonly Func _get_cell_store; + GlyphContext _root_glyph_context, _child_glyph_context; protected OrderbookColumnBase(Columns col_key, string header, Func get_cell_store, string sort_prop, double width, IDataGridColumnPersistentFilter filter, string filter_template) : base(col_key) { id = key.ToString(); @@ -331,6 +333,12 @@ namespace mbts.spark { this.filter = filter; _filter_template = filter_template; _cached_sort_direction = SortDirection; + font_params_changed += refresh_glyph_contexts; + refresh_glyph_contexts(); + void refresh_glyph_contexts() { + _root_glyph_context = GlyphContext.create(font_family, RenderUtils.root_font_weight); + _child_glyph_context = GlyphContext.create(font_family, RenderUtils.child_font_weight); + } } public IDataGridColumnPersistentFilter filter { get; } @@ -346,6 +354,7 @@ namespace mbts.spark { _cached_sort_direction = new_value; base.OnSortDirectionChanged(new_value); } + protected GlyphContext _get_glyph_context(OrderBookRowVM row) => row.is_child ? _child_glyph_context : _root_glyph_context; } public class SimpleTextColumn : OrderbookColumnBase, IRenderColumn { @@ -355,7 +364,7 @@ namespace mbts.spark { public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) { var cell = _get_cell_store(row); if (!string.IsNullOrEmpty(cell.current.text)) - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, cell.current.text, position, RowHeight, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, cell.current.text, position, RowHeight, font_brush, null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, cell.animations); } } @@ -366,7 +375,7 @@ namespace mbts.spark { : base(Columns.Side, "B/S", r => r.side.store, nameof(OrderBookRowVM.side), DefaultColumnWidths.Side, filter, DataGridFilterKnownTemplates.EnumValues) { } public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) => - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, row.side.text, position, RowHeight, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.side.text, position, RowHeight, RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, row.side.store.animations); } @@ -377,18 +386,18 @@ namespace mbts.spark { } public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) => - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, row.quantity.text, position, RowHeight, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.quantity.text, position, RowHeight, RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, row.quantity.store.animations); } public class FilledColumn : OrderbookColumnBase, IRenderColumn { public FilledColumn(IDataGridColumnPersistentFilter filter) - : base(Columns.Filled, "Filled Qty", r => r.filled_quantity.store, nameof(OrderBookRowVM.filled_quantity), DefaultColumnWidths.Size, filter, DataGridFilterKnownTemplates.UIntMinMax) { + : base(Columns.Filled, "Filled Qty", r => r.filled_quantity.store, nameof(OrderBookRowVM.filled_quantity), DefaultColumnWidths.Filled, filter, DataGridFilterKnownTemplates.UIntMinMax) { sort_member_path = nameof(OrderBookRowVM.filled_quantity); } public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) => - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, row.filled_quantity.text, position, RowHeight, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.filled_quantity.text, position, RowHeight, RenderUtils.get_side_brush(row), null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, row.filled_quantity.store.animations); } @@ -397,26 +406,26 @@ namespace mbts.spark { : base(Columns.Price, "Price", r => r.price.store, nameof(OrderBookRowVM.price), DefaultColumnWidths.Price, filter, DataGridFilterKnownTemplates.DoubleMinMax) { } public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) => - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, row.price.text, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.price.text, position, RowHeight, font_brush, null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, row.price.store.animations); } public class AvgFilledColumn : OrderbookColumnBase, IRenderColumn { public AvgFilledColumn(IDataGridColumnPersistentFilter filter) - : base(Columns.AvgFilled, "Avg Filled", r => r.avg_price.store, nameof(OrderBookRowVM.avg_price), DefaultColumnWidths.Price, filter, DataGridFilterKnownTemplates.DoubleMinMax) { } + : base(Columns.AvgFilled, "Avg Filled", r => r.avg_price.store, nameof(OrderBookRowVM.avg_price), DefaultColumnWidths.AveragePrice, filter, DataGridFilterKnownTemplates.DoubleMinMax) { } public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) { - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, row.avg_price.text, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.avg_price.text, position, RowHeight, font_brush, null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, row.avg_price.store.animations); } } public class LastFilledColumn : OrderbookColumnBase, IRenderColumn { public LastFilledColumn(IDataGridColumnPersistentFilter filter) - : base(Columns.LastPrice, "Last Filled", r => r.last_price.store, nameof(OrderBookRowVM.last_price), DefaultColumnWidths.Price, filter, DataGridFilterKnownTemplates.DoubleMinMax) { } + : base(Columns.LastPrice, "Last Filled", r => r.last_price.store, nameof(OrderBookRowVM.last_price), DefaultColumnWidths.LastPrice, filter, DataGridFilterKnownTemplates.DoubleMinMax) { } public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) => - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, row.last_price.text, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.last_price.text, position, RowHeight, font_brush, null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, row.last_price.store.animations); } @@ -425,7 +434,7 @@ namespace mbts.spark { : base(Columns.Time, "Time", r => r.time_created.store, nameof(OrderBookRowVM.time_created), DefaultColumnWidths.Time, filter, DataGridFilterKnownTemplates.TimeFilter) { } public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) => - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, row.time_created.text, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.time_created.text, position, RowHeight, font_brush, null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, row.time_created.store.animations); } @@ -434,7 +443,7 @@ namespace mbts.spark { : base(Columns.LastChange, "Last Change", r => r.last_change.store, nameof(OrderBookRowVM.last_change), DefaultColumnWidths.Time, filter, DataGridFilterKnownTemplates.TimeFilter) { } public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) => - CellRender.draw_text(dc, RenderUtils.get_glyph_context(row), font_size, row.last_change.text, + CellRender.draw_text(dc, _get_glyph_context(row), font_size, row.last_change.text, position, RowHeight, font_brush, null, horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding, row.last_change.store.animations); } @@ -470,7 +479,7 @@ namespace mbts.spark { id = key.ToString(); this.filter = filter; Header = "Last Message"; - Width = 80; + Width = DIYGrid.DefaultColumnWidths.LastMessage; sort_member_path = nameof(OrderBookRowVM.last_msg); tooltip_binding = new Binding(nameof(OrderBookRowVM.last_msg)); } @@ -521,7 +530,9 @@ namespace mbts.spark { toggle_button.SetBinding(ToggleButton.VisibilityProperty, new Binding(nameof(OrderBookRowVM.is_parent)) { Converter = _expander_visiblilty_converter }); toggle_button.SetResourceReference(ToggleButton.StyleProperty, ExpandCollapseToggleStyleKey); // - var security_view = new TextBlock { FontFamily = font_family, FontSize = font_size, Foreground = font_brush }; + var security_view = new TextBlock { Foreground = font_brush }; + security_view.SetBinding(TextBlock.FontFamilyProperty, new Binding(nameof(font_family)) { Source = this }); + security_view.SetBinding(TextBlock.FontSizeProperty, new Binding(nameof(font_size)) { Source = this }); security_view.SetBinding(TextBlock.TextProperty, $"{nameof(OrderBookRowVM.security)}.{nameof(IGridCell.text)}"); security_view.SetBinding(TextBlock.HorizontalAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this }); security_view.SetBinding(TextBlock.ForegroundProperty, new Binding(nameof(font_brush)) { Source = this }); @@ -548,7 +559,10 @@ namespace mbts.spark { #endregion static class RenderUtils { - public static readonly GlyphContext _child_glyph_context = TextUtils.StandardGlyphContext.with(FontWeights.Light); + public static FontWeight + root_font_weight = FontWeights.Normal, + child_font_weight = FontWeights.Light; + public static double font_size = FontSizes.Small; public static readonly IValueConverter @@ -567,7 +581,6 @@ namespace mbts.spark { return mb; } - public static GlyphContext get_glyph_context(OrderBookRowVM r) => r.is_child ? _child_glyph_context : TextUtils.StandardGlyphContext; public static Brush get_side_brush(OrderBookRowVM r) => r.side.value == Side.Buy ? BTSColors.MarketData.bid : BTSColors.MarketData.ask; public static BindingBase get_font_weight_binding() => new Binding(nameof(OrderBookRowVM.is_child)) { Converter = _font_weight_converter }; } diff --git a/src/client/spark/Modules/Trading/OrderBook/OrderBookVM.cs b/src/client/spark/Modules/Trading/OrderBook/OrderBookVM.cs index 04bfd3d7e..b07c705ba 100644 --- a/src/client/spark/Modules/Trading/OrderBook/OrderBookVM.cs +++ b/src/client/spark/Modules/Trading/OrderBook/OrderBookVM.cs @@ -4,6 +4,7 @@ using bts.api.core; using bts.api.orderman; using bts.shell; using mbts.collections; +using mbts.persist; using mbts.ui; using mbts.ui.controls; using mbts.ui.settings; @@ -23,12 +24,17 @@ using System.Windows.Data; using System.Windows.Input; using static bts.shell.OrderBookWindowFactory; using static mbts.spark.OrderBookView; +using _GridSettingsManager = mbts.ui.GridSettingsManager; namespace mbts.spark { partial class OrderBookVM : DisposableViewModelBase, IHandleRowSelection { readonly LiveSortingColumKeys _live_sorting_columns; readonly Property _current_row; + readonly Subject<(IEnumerable added, IEnumerable removed)> _order_securities_subject = + new Subject<(IEnumerable added, IEnumerable removed)>(); + readonly IWindowSettingsManager _settings_manager; OrderBookStartupData _state; + Dictionary _order_rows_cache = new Dictionary(); Dictionary _branch_filter_roots = new Dictionary(); ObservableCollection _order_rows = new ObservableCollection(); @@ -36,7 +42,8 @@ namespace mbts.spark { SetMap _parent_to_children = new SetMap(); SetMap _security_orders = new SetMap(); - public OrderBookVM(IObservable owner_title, IBus bus, INotificationService notification_service, OrdersModel model) { + public OrderBookVM(IObservable owner_title, IBus bus, INotificationService notification_service, OrdersModel model, IWindowSettingsManager settings_manager) { + _settings_manager = settings_manager; var order_securities_subject = new Subject<(IEnumerable added, IEnumerable removed)>(); columns = new OrderBookColumnCollection(bus, model, order_securities_subject); rows_view = CollectionViewSource.GetDefaultView(_order_rows); @@ -63,6 +70,7 @@ namespace mbts.spark { }); grid_settings_manager = _create_grid_settings_manager(owner_title, bus, notification_service, model, _order_rows, rows_view, columns); + grid_settings_manager.load_from(OrderBookStartupData.DefaultGrid); _live_sorting_columns = new LiveSortingColumKeys(grid_settings_manager.live_sorting_props); SetMap missing_security_orders = new SetMap(); @@ -71,6 +79,7 @@ namespace mbts.spark { _dispose = Disposable.Create( bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(on_securities), model.on_orders.OnDispatcher().Subscribe(on_orders), + settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager), grid_settings_manager, columns, order_securities_subject); @@ -215,7 +224,7 @@ namespace mbts.spark { public OrderRecordImmutable? current_order => current_row?.order; public IEnumerable selected_orders => _selected_rows.Select(r => r.order); - public GridSettingsManager grid_settings_manager { get; } + public _GridSettingsManager grid_settings_manager { get; } public ICommand collapse_all_rows_command { get; } public ICommand expand_all_rows_command { get; } @@ -225,14 +234,17 @@ namespace mbts.spark { } public OrderBookStartupData create() { - var (grid, animations) = grid_settings_manager.create(); - return new OrderBookStartupData(columns.create(), grid, animations.Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_builder, a.cell_animations, a.row_animations, a.sound_notifications))); + if (_settings_manager.instance_settings == null) + return new OrderBookStartupData(columns.create(), use_defaults: true); + var gs = grid_settings_manager.create(); + return new OrderBookStartupData(columns.create(), gs.general, gs.predicate_animations, use_defaults: false); } public void load_from(OrderBookStartupData state) { if (state == null) return; _state = state; - grid_settings_manager.load_from((state.grid, state.predicate_animations)); + var gs = state.grid_settings_wrapper(); + if (gs != null) grid_settings_manager.load_from(gs); columns.load_from(state.filters); } @@ -242,7 +254,7 @@ namespace mbts.spark { protected override void Dispose(bool disposing) => Disposable.dispose(_order_rows); - static GridSettingsManager _create_grid_settings_manager( + static _GridSettingsManager _create_grid_settings_manager( IObservable owner_title, IBus bus, INotificationService notification_service, OrdersModel model, ObservableCollection rows, ICollectionView rows_view, ObservableCollection columns) { // make font-weight immutable for all columns (the font-weight rule is hard-coded: Regular for root-orders, Light for all others) @@ -254,7 +266,7 @@ namespace mbts.spark { foreach (var c in new[] { Columns.Side, Columns.Size, Columns.Filled }) { col_settings_flags[c.ToString()] = GridColumnSettingsFeatureFlags.ImmutableFontColor | GridColumnSettingsFeatureFlags.ImmutableFontWeight; } - return new GridSettingsManager( + return new _GridSettingsManager( owner_title, bus, notification_service, model, rows, rows_view, columns, () => { var pvm = PredicateBuilderVMWrapper.create( @@ -267,7 +279,9 @@ namespace mbts.spark { OrderRecordTuplePredicateVMFactory.get_oneofs(bus, model), rp => rp.predicate_type_case); pvm.selected_predicate_type = _corresponding_predicate_type(EnumHelper.parse_or_default(col_id)); return pvm; - }, notification_service.data_cache, col_settings_flags); + }, + (gen, anims) => new OrderBookGridSettings(gen, anims.Select(a => new OrderRecordTuplePredicateAnimations(a.predicate_builder, a.cell_animations, a.row_animations, a.sound_notifications))), + notification_service.data_cache, col_settings_flags); } static OrderRecordTuplePredicate.PredicateTypeOneofCase _corresponding_predicate_type(Columns col) { diff --git a/src/client/spark/Modules/Trading/OrderBook/OrderBookWindow.cs b/src/client/spark/Modules/Trading/OrderBook/OrderBookWindow.cs index cce32129f..398d78020 100644 --- a/src/client/spark/Modules/Trading/OrderBook/OrderBookWindow.cs +++ b/src/client/spark/Modules/Trading/OrderBook/OrderBookWindow.cs @@ -2,18 +2,27 @@ using bts.api.orderman; using bts.shell; using bts.spark; +using mbts.persist; using mbts.ui; +using mbts.ui.converters; +using mbts.ui.spark; +using mbts.ui.utility; using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Linq; using System.Windows; +using System.Windows.Data; using System.Windows.Media; +//using IOrderBookWSM = mbts.persist.IWindowSettingsManager>; +using OrderBookGridSettings = bts.shell.OrderBookWindowFactory.OrderBookGridSettings; +using OrderBookGridSettingsVM = mbts.spark.GridSettingsVM; namespace mbts.spark { internal class OrderBookWindow : SparkWindow, ISharedWindow { - const string _window_type = "OrderBook", ORDERBOOK_THREAD_ID = "OrderBook"; + const string _window_type = "OrderBook", ORDERBOOK_THREAD_ID = "OrderBook", _default_title = "Order Book"; readonly INotificationService _notification_service; + readonly IWindowSettingsManager _settings_manager; OrderBookWindowFactory.OrderBookStartupData _state; OrdersModel _model; OrderBookVM _vm; @@ -32,12 +41,14 @@ namespace mbts.spark { , _toggle_expand_all_command , _show_grid_settings_command; - public OrderBookWindow(OrdersModel model, OrderBookWindowFactory factory, IBus bus, INotificationService notification_service, OrderBookWindowFactory.OrderBookStartupData state, ImageSource icon) - : base("Order Book", bus, factory, SparkCommands.ShowOrders.icon) { + public OrderBookWindow(OrdersModel model, OrderBookWindowFactory factory, IBus bus, INotificationService notification_service, + OrderBookWindowFactory.OrderBookStartupData state, ImageSource icon, IWindowSettingsManager settings_manager) + : base(_default_title, bus, factory, SparkCommands.ShowOrders.icon) { _model = model; _factory = factory; _notification_service = notification_service; _state = state; + _settings_manager = settings_manager; } public override OrderBookWindowFactory.OrderBookStartupData SaveState => _state = _vm.create(); @@ -45,8 +56,11 @@ namespace mbts.spark { public override string window_type => _window_type; public override UIElement Content => _view; public override IEnumerable Commands => _commands; - public override double MinWidth => 1577; - + public override double MinWidth => + OrderBookWindowFactory.OrderBookStartupData.DefaultGrid.general.columns.Sum(gcs => gcs.width) + + SPARK_MENU_COLLAPSED_WIDTH + + LAUNCHER_BUTTON_BORDER_WIDTH + + EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH; public string thread_id => ORDERBOOK_THREAD_ID; public override void Dispose() { @@ -55,7 +69,7 @@ namespace mbts.spark { } public void thread_init() { - _vm = new OrderBookVM(this.observe_title(), _bus, _notification_service, _model); + _vm = new OrderBookVM(this.observe_title(), _bus, _notification_service, _model, _settings_manager); _vm.load_from(_state); _view = new OrderBookView(_vm); @@ -106,9 +120,32 @@ namespace mbts.spark { Icons.DownArrow, _vm.expand_all_rows_command); - _show_grid_settings_command = SparkCommands.window_settings_command(_vm.grid_settings_manager.launch_settings_editor); + _show_grid_settings_command = SparkCommands.window_settings_command(new DialogCommand(() => { + var vm = new WindowSettingsDialogVM( + _settings_manager, + us => us.order_book_grid, + UserSettings.FileExtensions.GRID, + s => { + if (s.general is null) + s = OrderBookWindowFactory.OrderBookStartupData.DefaultGrid; + return _vm.grid_settings_manager.create_vm( + // we need to get the most up to date GridSettings when launching b/c users may + // have changed some settings on the grid directly, like column widths. + s.general.with(_vm.columns.Select(c => c.create())), + this.observe_title() + ); + } + ); + // TODO: bind to main window's Title + var dialog = new SimpleGridSettingsDialog { DataContext = vm }; + dialog.SetBinding(Window.TitleProperty, new Binding($"{nameof(vm.instance_vm)}.{nameof(OrderBookGridSettingsVM.owner_title)}") { + Converter = ValueConverter.create(str => (string.IsNullOrEmpty(str) ? _default_title : str) + " Settings") + }); + vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog); + return dialog; + })); - _commands = new SparkCommand[] { + _commands = new SparkCommand[] { _show_fills_command, _modify_order_command, _cancel_selected_orders_command, diff --git a/src/client/spark/Modules/Trading/OrderBook/OrderBookWindowFactory.cs b/src/client/spark/Modules/Trading/OrderBook/OrderBookWindowFactory.cs index c3037472b..78d8978fd 100644 --- a/src/client/spark/Modules/Trading/OrderBook/OrderBookWindowFactory.cs +++ b/src/client/spark/Modules/Trading/OrderBook/OrderBookWindowFactory.cs @@ -1,8 +1,9 @@ using bts.api; using bts.api.orderman; using bts.spark; -using Google.Protobuf.Collections; +using bts.utils; using mbts; +using mbts.persist; using mbts.spark; using mbts.spark.filters; using mbts.ui; @@ -11,6 +12,7 @@ using mbts.ui.settings; using ProtoBuf; using System; using System.Collections.Generic; +using System.ComponentModel; using System.Reactive.Linq; using System.Threading.Tasks; using System.Windows; @@ -22,19 +24,22 @@ namespace bts.shell { readonly INotificationService _notification_service; readonly INotificationServiceDataCache _notification_service_data_cache; readonly OrdersModel _model; + readonly UserSettingsHub _settings_hub; - public OrderBookWindowFactory(IShellModule owner, IBus bus, INotificationService notification_service, INotificationServiceDataCache notification_service_data_cache, OrdersModel model) : base(owner, "SparkOrderBookWindow") { + public OrderBookWindowFactory(IShellModule owner, IBus bus, INotificationService notification_service, INotificationServiceDataCache notification_service_data_cache, OrdersModel model, UserSettingsHub settings_hub) : base(owner, "SparkOrderBookWindow") { _bus = bus; _notification_service = notification_service; _notification_service_data_cache = notification_service_data_cache; _model = model; - + _settings_hub = settings_hub; _dispose = bus.subscribe(SparkTags.ShowOrderBookTag).OnDispatcher().Subscribe(show); } public override Task load(OrderBookStartupData state) { - var w = new OrderBookWindow(_model, this, _bus, _notification_service, state, SparkCommands.ShowOrders.icon) as IShellWindow; - return Task.FromResult(w); + var settings_manager = new WindowSettingsManager(_settings_hub, state.grid_settings_wrapper(), + us => us.order_book_grid, (us, s) => us.with(order_book_grid: s)); + var w = new OrderBookWindow(_model, this, _bus, _notification_service, state, SparkCommands.ShowOrders.icon, settings_manager.as_non_shared()); + return Task.FromResult(w); } async void show(OrderBookStartupData startup_data) { @@ -49,27 +54,42 @@ namespace bts.shell { [ProtoMember(10)] public FilterStates filters; [ProtoMember(15)] + [Obsolete] public GridSettings grid; [ProtoMember(20)] + [Obsolete] List _predicate_animations; + [ProtoMember(25)] + public bool use_defaults; public OrderBookStartupData() { } - public OrderBookStartupData(FilterStates filters, GridSettings grid = default, IEnumerable predicate_animations = null) { + public OrderBookStartupData(FilterStates filters, GridSettings grid = default, IEnumerable predicate_animations = null, bool use_defaults = false) { this.filters = filters; + this.use_defaults = use_defaults; +#pragma warning disable CS0612 // Type or member is obsolete this.grid = grid; _predicate_animations = new List(); if (predicate_animations != null) _predicate_animations.AddRange(predicate_animations); +#pragma warning restore CS0612 // Type or member is obsolete } +#pragma warning disable CS0612 // Type or member is obsolete + [Obsolete] public IReadOnlyList predicate_animations => _predicate_animations ?? (_predicate_animations = new List()); + public OrderBookGridSettings grid_settings_wrapper() => use_defaults ? null : new OrderBookGridSettings(grid, predicate_animations); +#pragma warning restore CS0612 // Type or member is obsolete public static OrderBookStartupData Default => new OrderBookStartupData( new FilterStates { statuses = new[] { OrderStatus.Working } - }, GridSettings.Default.with(DefaultColumns.all, DefaultColumns.sorting_criterion)); + }, _default_general_grid, use_defaults: true); + + public static OrderBookGridSettings DefaultGrid => new OrderBookGridSettings(_default_general_grid); + + static GridSettings _default_general_grid => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.sorting_criterion); static class DefaultColumns { public static GridColumnSettings security => new GridColumnSettings(Columns.Security.ToString(), DIYGrid.DefaultColumnHeaders.Security, width: DIYGrid.DefaultColumnWidths.Security); @@ -85,14 +105,14 @@ namespace bts.shell { public static GridColumnSettings side => new GridColumnSettings(Columns.Side.ToString(), "B/S", width: DIYGrid.DefaultColumnWidths.Side); public static GridColumnSettings quantity => new GridColumnSettings(Columns.Size.ToString(), "Qty", width: DIYGrid.DefaultColumnWidths.Size, horizontal_alignment: HorizontalAlignment.Right); public static GridColumnSettings price => new GridColumnSettings(Columns.Price.ToString(), DIYGrid.DefaultColumnHeaders.Price, width: DIYGrid.DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right); - public static GridColumnSettings filled_quantity => new GridColumnSettings(Columns.Filled.ToString(), "Filled Qty", width: DIYGrid.DefaultColumnWidths.Size, horizontal_alignment: HorizontalAlignment.Right); - public static GridColumnSettings avg_price => new GridColumnSettings(Columns.AvgFilled.ToString(), "Avg Filled", width: DIYGrid.DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right); - public static GridColumnSettings last_price => new GridColumnSettings(Columns.LastPrice.ToString(), "Last Filled", width: DIYGrid.DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right); + public static GridColumnSettings filled_quantity => new GridColumnSettings(Columns.Filled.ToString(), "Filled Qty", width: DIYGrid.DefaultColumnWidths.Filled, horizontal_alignment: HorizontalAlignment.Right); + public static GridColumnSettings avg_price => new GridColumnSettings(Columns.AvgFilled.ToString(), "Avg Filled", width: DIYGrid.DefaultColumnWidths.AveragePrice, horizontal_alignment: HorizontalAlignment.Right); + public static GridColumnSettings last_price => new GridColumnSettings(Columns.LastPrice.ToString(), "Last Filled", width: DIYGrid.DefaultColumnWidths.LastPrice, horizontal_alignment: HorizontalAlignment.Right); public static GridColumnSettings status => new GridColumnSettings(Columns.Status.ToString(), "Status", width: 80); - public static GridColumnSettings last_message => new GridColumnSettings(Columns.LastMessage.ToString(), "Last Message", width: 80); + public static GridColumnSettings last_message => new GridColumnSettings(Columns.LastMessage.ToString(), "Last Message", width: DIYGrid.DefaultColumnWidths.LastMessage); public static GridColumnSettings security_key => new GridColumnSettings(Columns.SecurityKey.ToString(), DIYGrid.DefaultColumnHeaders.SecurityKey, width: DIYGrid.DefaultColumnWidths.SecurityKey); public static GridColumnSettings order_key => new GridColumnSettings(Columns.OrderKey.ToString(), DIYGrid.DefaultColumnHeaders.OrderKey, width: DIYGrid.DefaultColumnWidths.OrderKey); - public static GridColumnSettings parent_key => new GridColumnSettings(Columns.ParentKey.ToString(), "Parent Key", width: DIYGrid.DefaultColumnWidths.OrderKey); + public static GridColumnSettings parent_key => new GridColumnSettings(Columns.ParentKey.ToString(), "Parent Key", width: DIYGrid.DefaultColumnWidths.ParentKey); public static IEnumerable sorting_criterion { get { @@ -171,5 +191,38 @@ namespace bts.shell { [ProtoMember(20)] public string[] symbols; } + + [ProtoContract] + public class OrderBookGridSettings : IGridSettingsWithNotifications, IEquatable { + [ProtoMember(1)] + public GridSettings general { get; set; } + [ProtoMember(2)] + List _predicate_animations; + + public OrderBookGridSettings() { } + public OrderBookGridSettings(GridSettings general = default, IEnumerable predicate_animations = null) { + this.general = general; + _predicate_animations = new List(); + if (predicate_animations != null) + _predicate_animations.AddRange(predicate_animations); + } + + public IReadOnlyList predicate_animations => _predicate_animations ?? (_predicate_animations = new List()); + IEnumerable> IGridSettingsWithNotifications.predicate_animations => predicate_animations; + + public OrderBookGridSettings with(GridSettings general) => + new OrderBookGridSettings(general ?? this.general, predicate_animations); + + public bool Equals(OrderBookGridSettings other) => + other != null && + EqualityComparer.null_check_equals(general, other.general) && + EqualityComparer.safe_sequence_equals(predicate_animations, other.predicate_animations); + + public override bool Equals(object obj) => Equals(obj as OrderBookGridSettings); + public override int GetHashCode() => + EqualityComparer.aggregate_hash( + EqualityComparer.safe_get_hash_code(general), + EqualityComparer.safe_aggregate_hash(predicate_animations)); + } } } \ No newline at end of file diff --git a/src/client/spark/Modules/Trading/PAndLWindowFactory.cs b/src/client/spark/Modules/Trading/PAndLWindowFactory.cs index 270b95ad4..27eba0300 100644 --- a/src/client/spark/Modules/Trading/PAndLWindowFactory.cs +++ b/src/client/spark/Modules/Trading/PAndLWindowFactory.cs @@ -1,30 +1,38 @@ using bts.api; using bts.api.orderman; using bts.spark; +using mbts.persist; using mbts.spark; using mbts.spark.filters; using mbts.ui; +using mbts.ui.settings; using mbts.ui.spark; using ProtoBuf; using System; using System.Collections.Generic; +using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; +using static mbts.spark.PAndLView; +using static mbts.ui.controls.DIYGrid; namespace bts.shell { internal class PAndLWindowFactory : ShellWindowFactory { readonly IBus _bus; readonly PAndLModel _model; - public PAndLWindowFactory(IShellModule owner, IBus bus, PAndLModel model) : base(owner, "SparkPAndL") { + readonly UserSettingsHub _settings_hub; + + public PAndLWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, PAndLModel model) : base(owner, "SparkPAndL") { _bus = bus; _model = model; - + _settings_hub = settings_hub; _dispose = bus.subscribe(SparkTags.ShowPAndLTag).OnDispatcher().Subscribe(show); } + public static string col_id(Columns col) => col.ToString(); + public override Task load(PAndLStartupData state) { - var w = new PAndLWindow(_model, this, _bus, state, SparkCommands.ShowPAndL.icon) as IShellWindow; - return Task.FromResult(w); + return Task.FromResult(_create(state)); } async void show(PAndLStartupData startup_data) { @@ -32,6 +40,14 @@ namespace bts.shell { _bus.post(SparkTags.ShowWindow, w); } + IShellWindow _create(PAndLStartupData state) { + var settings_manager = new WindowSettingsManager(_settings_hub, + state.grid, + get: us => us.pnl_grid, + set: (us, gs) => us.with(provided_pnl_gridsettings: gs)); + return new PAndLWindow(_model, this, _bus, state, SparkCommands.ShowPAndL.icon, settings_manager); + } + [ProtoContract] public class PAndLStartupData { @@ -40,21 +56,75 @@ namespace bts.shell { /// public PAndLStartupData() { } - public PAndLStartupData(List sorting_criteria, FilterStates filters, pnl_calc_type calc_type) { - this.sort_criteria = sorting_criteria; + public PAndLStartupData(List sorting_criteria, FilterStates filters, pnl_calc_type calc_type, GridSettings grid = default) { + _sort_criteria = sorting_criteria; this.filters = filters; this.calc_type = calc_type; + this.grid = grid; } + public static GridSettings DefaultGridSettings => GridSettings.Default.with(DefaultColumns.all, DefaultColumns.default_sorting_criterion); + + public IReadOnlyList sorting_criterion => _sort_criteria ?? (_sort_criteria = DefaultColumns.default_sorting_criterion.ToList()); + public static PAndLStartupData Default() => + new PAndLStartupData(null, new FilterStates(), pnl_calc_type.midpoint); + + [ProtoMember(5)] - public readonly List sort_criteria; + List _sort_criteria; [ProtoMember(10)] public FilterStates filters; [ProtoMember(15)] public pnl_calc_type calc_type; //[ProtoMember(20)] // ProtoMember 20 cannot be used further + [ProtoMember(25)] + public GridSettings grid; + static class DefaultColumns { + + public static GridColumnSettings account => new GridColumnSettings(col_id(Columns.Account), DefaultColumnHeaders.Account, width: DefaultColumnWidths.Account); + public static GridColumnSettings exchange => new GridColumnSettings(col_id(Columns.Exchange), DefaultColumnHeaders.Exchange, width: DefaultColumnWidths.Exchange); + public static GridColumnSettings symbol => new GridColumnSettings(col_id(Columns.Symbol), DefaultColumnHeaders.Symbol, width: DefaultColumnWidths.Symbol); + public static GridColumnSettings security => new GridColumnSettings(col_id(Columns.Security), DefaultColumnHeaders.Security, width: DefaultColumnWidths.Security); + public static GridColumnSettings security_type => new GridColumnSettings(col_id(Columns.SecurityType), DefaultColumnHeaders.SecurityType, width: DefaultColumnWidths.SecurityType); + public static GridColumnSettings start_net_position => new GridColumnSettings(col_id(Columns.StartNetPos), DefaultColumnHeaders.StartNetPos, width: DefaultColumnWidths.Start_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings start_pnl => new GridColumnSettings(col_id(Columns.StartPNL), DefaultColumnHeaders.StartPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings day_long_pnl => new GridColumnSettings(col_id(Columns.DayLong), DefaultColumnHeaders.DayLong, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings day_short_pnl => new GridColumnSettings(col_id(Columns.DayShort), DefaultColumnHeaders.DayShort, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings day_pnl => new GridColumnSettings(col_id(Columns.DayPNL), DefaultColumnHeaders.DayPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings total_net_pos => new GridColumnSettings(col_id(Columns.TotalNetPos), DefaultColumnHeaders.TotalNetPos, width: DefaultColumnWidths.Total_Net_Pos, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings total_pnl => new GridColumnSettings(col_id(Columns.TotalPNL), DefaultColumnHeaders.TotalPnL, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + public static GridColumnSettings pnl_price => new GridColumnSettings(col_id(Columns.PNLPrice), DefaultColumnHeaders.PnLPrice, width: DefaultColumnWidths.PnL_Generic, horizontal_alignment: System.Windows.HorizontalAlignment.Right); + + public static IEnumerable default_sorting_criterion { + get { + yield return new SortingCriteria(account.id, true); + yield return new SortingCriteria(exchange.id, true); + yield return new SortingCriteria(symbol.id, true); + yield return new SortingCriteria(security.id, true); + } + } + + public static IEnumerable all { + get { + yield return account; + yield return exchange; + yield return symbol; + yield return security; + yield return security_type; + yield return start_net_position; + yield return start_pnl; + yield return day_long_pnl; + yield return day_short_pnl; + yield return day_pnl; + yield return total_net_pos; + yield return total_pnl; + yield return pnl_price; + } + } + + } } [ProtoContract] diff --git a/src/client/spark/Modules/Trading/TradingShellModule.cs b/src/client/spark/Modules/Trading/TradingShellModule.cs index 9924f00ff..22a6c2f14 100644 --- a/src/client/spark/Modules/Trading/TradingShellModule.cs +++ b/src/client/spark/Modules/Trading/TradingShellModule.cs @@ -1,4 +1,5 @@ using bts.api; +using bts.api.md.auxiliary; using bts.spark; using mbts.persist; using mbts.spark; @@ -19,11 +20,12 @@ namespace bts.shell { PAndLWindowFactory _plwf; [ImportingConstructor] - public TradingShellModule(IBus bus, IWindowManager wm, INotificationService notification_service, INotificationServiceDataCache notification_service_data_cache, UserSettingsHub settings_hub, OrdersModel orders_model, PAndLModel pandl_model) { - _lwf = new LadderWindowFactory(this, bus, wm, settings_hub, orders_model); - _obwf = new OrderBookWindowFactory(this, bus, notification_service, notification_service_data_cache, orders_model); - _fwf = new FillsWindowFactory(this, bus, orders_model); - _plwf = new PAndLWindowFactory(this, bus, pandl_model); + public TradingShellModule(IBus bus, IWindowManager wm, INotificationService notification_service, INotificationServiceDataCache notification_service_data_cache, + UserSettingsHub settings_hub, OrdersModel orders_model, PAndLModel pandl_model, MarketDataAuxiliaryModel md_auxiliary_model) { + _lwf = new LadderWindowFactory(this, bus, wm, settings_hub, orders_model, md_auxiliary_model); + _obwf = new OrderBookWindowFactory(this, bus, notification_service, notification_service_data_cache, orders_model, settings_hub); + _fwf = new FillsWindowFactory(this, bus, settings_hub, orders_model); + _plwf = new PAndLWindowFactory(this, bus, settings_hub, pandl_model); } public ImageSource icon => Icons.Trades; diff --git a/src/client/spark/Orders/OrderPropertiesEditorVM.cs b/src/client/spark/Orders/OrderPropertiesEditorVM.cs index 3b225cdd4..509c67f35 100644 --- a/src/client/spark/Orders/OrderPropertiesEditorVM.cs +++ b/src/client/spark/Orders/OrderPropertiesEditorVM.cs @@ -1,6 +1,7 @@ using bts; using bts.api; using bts.api.orderman; +using bts.utils; using mbts.ui; using mbts.ui.shell; using System; @@ -11,6 +12,60 @@ using System.Linq; using System.Reactive.Linq; namespace mbts.spark { + struct OrderTemplateKey : IEquatable { + public readonly CustomOrderDefinition.CustomOneofCase type; + public readonly string name; + + public OrderTemplateKey(CustomOrderDefinition.CustomOneofCase type) + : this(type, OrderUtils.order_type_display_name(type)) { } + + public OrderTemplateKey(CustomOrderDefinition.CustomOneofCase type, string name) { + this.type = type; + this.name = name; + } + + public bool Equals(OrderTemplateKey other) => + type == other.type && + name == other.name; + + public override bool Equals(object obj) => + obj is OrderTemplateKey other && Equals(other); + + public override int GetHashCode() => + EqualityComparer.aggregate_hash( + type.GetHashCode(), + name == null ? 0 : name.GetHashCode()); + + public override string ToString() => $"{name}:{type}"; + + public static bool operator ==(OrderTemplateKey a, OrderTemplateKey b) => a.Equals(b); + public static bool operator !=(OrderTemplateKey a, OrderTemplateKey b) => !a.Equals(b); + } + + struct OrderTemplateHeader : IEquatable { + public readonly OrderTemplateKey key; + // Note: display_name is NOT part of EQUALITY + public readonly string display_name; + + public OrderTemplateHeader(CustomOrderDefinition.CustomOneofCase type) + : this(new OrderTemplateKey(type), OrderUtils.order_type_display_name(type)) { } + + public OrderTemplateHeader(OrderTemplateKey key, string display_name) { + this.key = key; + this.display_name = display_name; + } + + public bool Equals(OrderTemplateHeader other) => + key.Equals(other.key); + + public override bool Equals(object obj) => + obj is OrderTemplateHeader other && Equals(other); + + public override int GetHashCode() => key.GetHashCode(); + + public override string ToString() => $"{display_name} ({key})"; + } + class InlineOrderProperties { public readonly InlineUserProperty account; public readonly InlineUserProperty time_in_force; @@ -39,7 +94,7 @@ namespace mbts.spark { class OrderPropertiesEditorVM : DisposableViewModelBase { readonly OrdersModel _model; readonly Property _security; - readonly SelectorProperty _accounts; + readonly SelectorProperty _accounts; readonly SelectorProperty _time_in_force; readonly SelectorProperty _order_type; IDisposable _order_type_param_errors_sub; @@ -58,14 +113,21 @@ namespace mbts.spark { nameof(selected_account), model.factory.subscribe_account_tuples(), order_properties.Select(ops => ops.account), - () => accounts?.FirstOrDefault() ?? default, - val => (!string.IsNullOrWhiteSpace(val.name) && accounts.Contains(val), "Please select an account")); + a => new AccountTuple?(a), + () => { + foreach (var a in accounts) { + return a; + } + return new AccountTuple?(); + }, + EqualityComparer.Default, + new[] { new PropertyValidation(a => (a.HasValue && !string.IsNullOrWhiteSpace(a.Value.name) && accounts.Contains(a.Value), "Please select an account")) }.ToReadOnlyList()); _order_type = backing( nameof(selected_order_type), - security_source.Select(s => model.factory.default_order_type_defs(s).Select(create_order_type_vm)), + security_source.Select(s => model.factory.default_order_type_templates(s).Select(kv => create_order_type_vm(kv.Value, kv.Key.key.name, kv.Key.display_name))), order_properties.Select(get_order_type_vm_prop), get_default_order_type_vm, - (a, b) => a.order_type == b.order_type); + EqualityComparer.null_check_wrapper(OrderTypeVMExtensions.key_comparer)); _order_type.Changed += on_selected_order_type_changed; _time_in_force = backing( nameof(selected_time_in_force), @@ -77,19 +139,22 @@ namespace mbts.spark { tif => time_in_forces.Contains(tif)); _dispose = Disposable.Create(() => _order_type.Changed -= on_selected_order_type_changed); // local methods - IOrderTypeVM create_order_type_vm(CustomOrderDefinitionImmutable cd) { + IOrderTypeVM create_order_type_vm(CustomOrderDefinitionImmutable cd, string id, string display_name) { IOrderTypeVM vm = null; if(cd.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic) - vm = OrderTypeVMFactory.create(cd.CustomCase, model, security_md_source, cd.Dynamic.DynamicOrderType); + vm = OrderTypeVMFactory.create(cd.CustomCase, id, display_name, model, security_md_source, cd.Dynamic.DynamicOrderType); else - vm = OrderTypeVMFactory.create(cd.CustomCase, model, security_md_source); + vm = OrderTypeVMFactory.create(cd.CustomCase, id, display_name, model, security_md_source); vm.load_from(cd); return vm; } InlineUserProperty get_order_type_vm_prop(InlineOrderProperties p) { if (!p.order_type_def.has_value) return new InlineUserProperty(); - return new InlineUserProperty(create_order_type_vm(p.order_type_def.value), p.order_type_def.force_override); + return new InlineUserProperty( + create_order_type_vm(p.order_type_def.value, p.order_type_def.value.CustomCase.ToString(), + OrderUtils.order_type_display_name(p.order_type_def.value.CustomCase)), + p.order_type_def.force_override); } IOrderTypeVM get_default_order_type_vm() { var ot = model.factory.default_order_type(security); @@ -114,13 +179,13 @@ namespace mbts.spark { public SecurityImmutable security => _security.Value; public ReadOnlyObservableCollection accounts => _accounts.items; - public AccountTuple selected_account { get => _accounts.selected_item; set => _accounts.selected_item = value; } + public AccountTuple? selected_account { get => _accounts.selected_item; set => _accounts.selected_item = value; } public ReadOnlyObservableCollection time_in_forces => _time_in_force.items; public TimeInForce selected_time_in_force { get => _time_in_force.selected_item; set => _time_in_force.selected_item = value; } public ReadOnlyObservableCollection order_types => _order_type.items; public IOrderTypeVM selected_order_type { get => _order_type.selected_item; set => _order_type.selected_item = value; } - public IObservable observe_account(bool start_immediately = false) => + public IObservable observe_account(bool start_immediately = false) => _accounts.observe_current(start_immediately); public IObservable observe_time_in_force(bool start_immediately = false) => diff --git a/src/client/spark/Orders/OrderTypeVMs/CoverOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/CoverOrderVM.cs index 89ee3f5e8..0192876f5 100644 --- a/src/client/spark/Orders/OrderTypeVMs/CoverOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/CoverOrderVM.cs @@ -17,22 +17,22 @@ namespace mbts.spark { class InlineCoverOrderProperties { - public InlineCoverOrderProperties(CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) + public InlineCoverOrderProperties(OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); var hedgetype = CustomOneofCase.Limit; - this.selected_hedge_order_type = new InlineUserProperty(hedgetype, true); - this.hedge_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_hedge_order_type = new InlineUserProperty(new OrderTemplateHeader(hedgetype), true); + this.hedge_order_types = new InlineUserProperty>(child_order_types, true); hedge_ticks = new InlineUserProperty(1, false); } - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; - public readonly InlineUserProperty selected_hedge_order_type; - public readonly InlineUserProperty> hedge_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_hedge_order_type; + public readonly InlineUserProperty> hedge_order_types; public readonly InlineUserProperty hedge_ticks; @@ -42,25 +42,25 @@ namespace mbts.spark { readonly IObservable _security_md_source; SecurityImmutable? _security; - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineCoverOrderProperties _inline_props; IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; - ObservableAutoWrapper _hedge_order_choices; + ObservableAutoWrapper _hedge_order_choices; IOrderTypeVM _hedge_order; IDisposable _hedge_order_errors_sub; public int hedge_ticks { get; set; } = 1; - public CoverOrderVM(OrdersModel model, IObservable security_md_source) + public CoverOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) : this(model, security_md_source, security_md_source.Select(smd => - new InlineCoverOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) + new InlineCoverOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } public CoverOrderVM(OrdersModel model, IObservable security_md_source, - IObservable inline_properties) : base(model) + IObservable inline_properties, string id, string display_name) : base(model, id, display_name) { _security_md_source = security_md_source; _dispose = Disposable.Create( @@ -75,11 +75,11 @@ namespace mbts.spark } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } if (_hedge_order_choices != null && (_hedge_order == null || p.selected_hedge_order_type.force_override)) { - _select_hedge_order(p.selected_hedge_order_type.value); + _select_hedge_order(p.selected_hedge_order_type.value.key.type); } if (p.hedge_ticks.force_override) { @@ -116,7 +116,7 @@ namespace mbts.spark get { if (_child_order_choices == null) - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _child_order_choices; } } @@ -126,7 +126,7 @@ namespace mbts.spark get { if (_hedge_order_choices == null) - _hedge_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _hedge_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _hedge_order_choices; } } @@ -200,7 +200,7 @@ namespace mbts.spark void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } @@ -216,7 +216,7 @@ namespace mbts.spark void _select_hedge_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_hedge_order_type.has_value ? - _inline_props.selected_hedge_order_type.value : + _inline_props.selected_hedge_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_hedge_order(ot); } diff --git a/src/client/spark/Orders/OrderTypeVMs/DynamicOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/DynamicOrderVM.cs index eaa302c86..0ef39f658 100644 --- a/src/client/spark/Orders/OrderTypeVMs/DynamicOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/DynamicOrderVM.cs @@ -53,14 +53,14 @@ namespace mbts.spark { class InlineDynamicOrderProperties { - public InlineDynamicOrderProperties(CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) + public InlineDynamicOrderProperties(OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); } - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; } class DynamicOrderVM : OrderTypeVMBase, IHierarchicalOrderTypeVM, IDisposable @@ -68,8 +68,8 @@ namespace mbts.spark readonly IObservable _security_md_source; SecurityImmutable? _security; - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineDynamicOrderProperties _inline_props; IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; @@ -77,13 +77,13 @@ namespace mbts.spark string _dyn_type; List _parameters; - public DynamicOrderVM(OrdersModel model, IObservable security_md_source, string dyn_type) + public DynamicOrderVM(OrdersModel model, IObservable security_md_source, string dyn_type, string id, string display_name) : this(model, security_md_source, dyn_type, security_md_source.Select(smd => - new InlineDynamicOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) + new InlineDynamicOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } public DynamicOrderVM(OrdersModel model, IObservable security_md_source, string dyn_type, - IObservable inline_properties) : base(model) + IObservable inline_properties, string id, string display_name) : base(model, id, display_name) { _security_md_source = security_md_source; _dyn_type = dyn_type; @@ -100,7 +100,7 @@ namespace mbts.spark } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } _inline_props = p; _validate(); @@ -139,7 +139,7 @@ namespace mbts.spark get { if (_child_order_choices == null) - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _child_order_choices; } } @@ -185,7 +185,7 @@ namespace mbts.spark void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } diff --git a/src/client/spark/Orders/OrderTypeVMs/IOrderTypeVM.cs b/src/client/spark/Orders/OrderTypeVMs/IOrderTypeVM.cs index 3f7fc9c33..355592e80 100644 --- a/src/client/spark/Orders/OrderTypeVMs/IOrderTypeVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/IOrderTypeVM.cs @@ -1,11 +1,13 @@ using bts.api.orderman; using bts.spark; +using bts.utils; using System.Collections.Generic; using System.Windows.Input; using static bts.api.orderman.CustomOrderDefinition; namespace mbts.spark { interface IOrderTypeVM : IPropertyWrapperVM { + string id { get; } CustomOneofCase order_type { get; } string type_name { get; } bool is_bts_type { get; } @@ -21,4 +23,12 @@ namespace mbts.spark { ICollection child_order_types { get; } IOrderTypeVM child_order { get; } } + + static class OrderTypeVMExtensions { + public static IEqualityComparer key_comparer => + EqualityComparer.Create(a => a.key()); + + public static OrderTemplateKey key(this IOrderTypeVM vm) => + new OrderTemplateKey(vm.order_type, vm.id); + } } \ No newline at end of file diff --git a/src/client/spark/Orders/OrderTypeVMs/IcebergOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/IcebergOrderVM.cs index cd844c9c6..31c9a3d11 100644 --- a/src/client/spark/Orders/OrderTypeVMs/IcebergOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/IcebergOrderVM.cs @@ -14,34 +14,34 @@ using static bts.api.orderman.CustomOrderDefinition; namespace mbts.spark { static partial class OrderTypeVMFactory { class InlineIcebergOrderProperties { - public InlineIcebergOrderProperties(CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) { + public InlineIcebergOrderProperties(OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); show_quantity = new InlineUserProperty(1, true); } - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; public readonly InlineUserProperty show_quantity; } class IcebergOrderVM : OrderTypeVMBase, IHierarchicalOrderTypeVM, IDisposable { readonly IObservable _security_md_source; SecurityImmutable? _security; - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineIcebergOrderProperties _inline_props; IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; - public IcebergOrderVM(OrdersModel model, IObservable security_md_source) + public IcebergOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) : this(model, security_md_source, security_md_source.Select(smd => - new InlineIcebergOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) { } + new InlineIcebergOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } public IcebergOrderVM(OrdersModel model, IObservable security_md_source, - IObservable inline_properties) - : base(model) { + IObservable inline_properties, string id, string display_name) + : base(model, id, display_name) { _security_md_source = security_md_source; _dispose = Disposable.Create( security_md_source.OnDispatcher() @@ -52,7 +52,7 @@ namespace mbts.spark { _order_types.SetItems(p.child_order_types.value); } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } if (p.show_quantity.force_override) { show_quantity = p.show_quantity.value; @@ -68,7 +68,7 @@ namespace mbts.spark { public ICollection child_order_types { get { if (_child_order_choices == null) { - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); } return _child_order_choices; } @@ -122,7 +122,7 @@ namespace mbts.spark { void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } diff --git a/src/client/spark/Orders/OrderTypeVMs/JoinOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/JoinOrderVM.cs index 26ac1583c..ae8ce4cd7 100644 --- a/src/client/spark/Orders/OrderTypeVMs/JoinOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/JoinOrderVM.cs @@ -17,19 +17,19 @@ namespace mbts.spark { class InlineJoinOrderProperties { - public InlineJoinOrderProperties (CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) + public InlineJoinOrderProperties (OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); var hedgetype = CustomOneofCase.Lean; - if(!child_order_types.Contains(hedgetype)) + if(!child_order_types.Any(t => t.key.type == hedgetype)) hedgetype = CustomOneofCase.Limit; //ExceptionAssert.InvalidOperation.That(child_order_types.Contains(hedgetype), $"Hedge order type {hedgetype} is not in hedge_order_types"); - this.selected_hedge_order_type = new InlineUserProperty(hedgetype, true); - this.hedge_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_hedge_order_type = new InlineUserProperty(new OrderTemplateHeader(hedgetype), true); + this.hedge_order_types = new InlineUserProperty>(child_order_types, true); tick_offset_on_deploy = new InlineUserProperty(0, false); join_threshold = new InlineUserProperty( @@ -37,10 +37,10 @@ namespace mbts.spark enable_hedge = new InlineUserProperty(false, false); hedge_ticks = new InlineUserProperty(1, false); } - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; - public readonly InlineUserProperty selected_hedge_order_type; - public readonly InlineUserProperty> hedge_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_hedge_order_type; + public readonly InlineUserProperty> hedge_order_types; public readonly InlineUserProperty tick_offset_on_deploy; @@ -55,13 +55,13 @@ namespace mbts.spark { readonly IObservable _security_md_source; SecurityImmutable? _security; - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineJoinOrderProperties _inline_props; IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; - ObservableAutoWrapper _hedge_order_choices; + ObservableAutoWrapper _hedge_order_choices; IOrderTypeVM _hedge_order; IDisposable _hedge_order_errors_sub; @@ -79,12 +79,12 @@ namespace mbts.spark } public int hedge_ticks { get; set; } = 1; - public JoinOrderVM(OrdersModel model, IObservable security_md_source) + public JoinOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) : this(model, security_md_source, security_md_source.Select(smd => - new InlineJoinOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) { } + new InlineJoinOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } public JoinOrderVM(OrdersModel model, IObservable security_md_source, - IObservable inline_properties) : base(model) + IObservable inline_properties, string id, string display_name) : base(model, id, display_name) { _security_md_source = security_md_source; _dispose = Disposable.Create( @@ -98,11 +98,11 @@ namespace mbts.spark } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } if (_hedge_order_choices != null && (_hedge_order == null || p.selected_hedge_order_type.force_override)) { - _select_hedge_order(p.selected_hedge_order_type.value); + _select_hedge_order(p.selected_hedge_order_type.value.key.type); } if (p.tick_offset_on_deploy.force_override) { @@ -157,7 +157,7 @@ namespace mbts.spark get { if (_child_order_choices == null) - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _child_order_choices; } } @@ -167,7 +167,7 @@ namespace mbts.spark get { if (_hedge_order_choices == null) - _hedge_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _hedge_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _hedge_order_choices; } } @@ -241,7 +241,7 @@ namespace mbts.spark void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } @@ -257,7 +257,7 @@ namespace mbts.spark void _select_hedge_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_hedge_order_type.has_value ? - _inline_props.selected_hedge_order_type.value : + _inline_props.selected_hedge_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_hedge_order(ot); } diff --git a/src/client/spark/Orders/OrderTypeVMs/LeanOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/LeanOrderVM.cs index 9f61bc4ff..0c436a742 100644 --- a/src/client/spark/Orders/OrderTypeVMs/LeanOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/LeanOrderVM.cs @@ -17,18 +17,18 @@ namespace mbts.spark { class InlineLeanOrderProperties { - public InlineLeanOrderProperties (CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) + public InlineLeanOrderProperties (OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); cross_threshold = new InlineUserProperty( new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1}, false); max_chase_ticks = new InlineUserProperty(2, false); trade_toward_behavior = new InlineUserProperty(LeanOrderDefinition.Types.OnToward.Cancel, false); } - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; public readonly InlineUserProperty cross_threshold; public readonly InlineUserProperty max_chase_ticks; @@ -39,8 +39,8 @@ namespace mbts.spark { readonly IObservable _security_md_source; SecurityImmutable? _security; - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineLeanOrderProperties _inline_props; IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; @@ -49,12 +49,12 @@ namespace mbts.spark public uint max_chase_ticks { get; set; } = 2; public LeanOrderDefinition.Types.OnToward trade_toward_behavior { get; set; } = LeanOrderDefinition.Types.OnToward.Cancel; - public LeanOrderVM(OrdersModel model, IObservable security_md_source) + public LeanOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) : this(model, security_md_source, security_md_source.Select(smd => - new InlineLeanOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) { } + new InlineLeanOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } public LeanOrderVM(OrdersModel model, IObservable security_md_source, - IObservable inline_properties) : base(model) + IObservable inline_properties, string id, string display_name) : base(model, id, display_name) { _security_md_source = security_md_source; _dispose = Disposable.Create( @@ -68,7 +68,7 @@ namespace mbts.spark } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } if (p.cross_threshold.force_override) { @@ -115,7 +115,7 @@ namespace mbts.spark get { if (_child_order_choices == null) - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _child_order_choices; } } @@ -161,7 +161,7 @@ namespace mbts.spark void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } diff --git a/src/client/spark/Orders/OrderTypeVMs/LimitOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/LimitOrderVM.cs index 80bf27d75..c9ce62236 100644 --- a/src/client/spark/Orders/OrderTypeVMs/LimitOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/LimitOrderVM.cs @@ -3,7 +3,7 @@ namespace mbts.spark { static partial class OrderTypeVMFactory { private class LimitOrderVM : OrderTypeVMBase { - internal LimitOrderVM(OrdersModel model) : base(model) { } + internal LimitOrderVM(OrdersModel model) : base(model, "Limit", OrderUtils.order_type_display_name(CustomOrderDefinition.CustomOneofCase.Limit)) { } public override void assign_to(CustomOrderDefinition value) => value.Limit = factory.limit_order(); diff --git a/src/client/spark/Orders/OrderTypeVMs/MarketOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/MarketOrderVM.cs index ee0fb8422..dbefb3206 100644 --- a/src/client/spark/Orders/OrderTypeVMs/MarketOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/MarketOrderVM.cs @@ -3,7 +3,7 @@ namespace mbts.spark { static partial class OrderTypeVMFactory { private class MarketOrderVM : OrderTypeVMBase { - internal MarketOrderVM(OrdersModel model) : base(model) { } + internal MarketOrderVM(OrdersModel model) : base(model, "Market", OrderUtils.order_type_display_name(CustomOrderDefinition.CustomOneofCase.Market)) { } public override void assign_to(CustomOrderDefinition parent) => parent.Market = factory.market_order(); diff --git a/src/client/spark/Orders/OrderTypeVMs/OrderTypeVMFactory.cs b/src/client/spark/Orders/OrderTypeVMs/OrderTypeVMFactory.cs index e9efc6e27..e2a5d3817 100644 --- a/src/client/spark/Orders/OrderTypeVMs/OrderTypeVMFactory.cs +++ b/src/client/spark/Orders/OrderTypeVMs/OrderTypeVMFactory.cs @@ -12,36 +12,36 @@ namespace mbts.spark { static partial class OrderTypeVMFactory { public static IOrderTypeVM market(OrdersModel model) => new MarketOrderVM(model); public static IOrderTypeVM limit(OrdersModel model) => new LimitOrderVM(model); - public static IOrderTypeVM stop(OrdersModel model) => new StopOrderVM(model); - public static IOrderTypeVM stop_limit(OrdersModel model, IObservable security_md_source) => new StopLimitOrderVM(model, security_md_source); - public static IOrderTypeVM time_box(OrdersModel model, IObservable security_md_source) => new TimeBoxOrderVM(model, security_md_source); - public static IOrderTypeVM tick(OrdersModel model, IObservable security_md_source) => new TickOrderVM(model, security_md_source); - public static IOrderTypeVM lean(OrdersModel model, IObservable security_md_source) => new LeanOrderVM(model, security_md_source); - public static IOrderTypeVM join(OrdersModel model, IObservable security_md_source) => new JoinOrderVM(model, security_md_source); - public static IOrderTypeVM top(OrdersModel model, IObservable security_md_source) => new TopOrderVM(model, security_md_source); - public static IOrderTypeVM cover(OrdersModel model, IObservable security_md_source) => new CoverOrderVM(model, security_md_source); - public static IOrderTypeVM trigger(OrdersModel model, IObservable security_md_source) => new TriggerOrderVM(model, security_md_source); - public static IOrderTypeVM iceberg(OrdersModel model, IObservable security_md_source) => new IcebergOrderVM(model, security_md_source); - public static IOrderTypeVM spreader(OrdersModel model, IObservable security_source) => new SpreaderOrderVM(model, security_source); - public static IOrderTypeVM dynamic(OrdersModel model, IObservable security_md_source, string type) => new DynamicOrderVM(model, security_md_source, type); + public static IOrderTypeVM stop(OrdersModel model, string id, string display_name) => new StopOrderVM(model, id, display_name); + public static IOrderTypeVM stop_limit(OrdersModel model, IObservable security_md_source, string id, string display_name) => new StopLimitOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM time_box(OrdersModel model, IObservable security_md_source, string id, string display_name) => new TimeBoxOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM tick(OrdersModel model, IObservable security_md_source, string id, string display_name) => new TickOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM lean(OrdersModel model, IObservable security_md_source, string id, string display_name) => new LeanOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM join(OrdersModel model, IObservable security_md_source, string id, string display_name) => new JoinOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM top(OrdersModel model, IObservable security_md_source, string id, string display_name) => new TopOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM cover(OrdersModel model, IObservable security_md_source, string id, string display_name) => new CoverOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM trigger(OrdersModel model, IObservable security_md_source, string id, string display_name) => new TriggerOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM iceberg(OrdersModel model, IObservable security_md_source, string id, string display_name) => new IcebergOrderVM(model, security_md_source, id, display_name); + public static IOrderTypeVM spreader(OrdersModel model, IObservable security_source, string id, string display_name) => new SpreaderOrderVM(model, security_source, id, display_name); + public static IOrderTypeVM dynamic(OrdersModel model, IObservable security_md_source, string dynamic_type, string id, string display_name) => new DynamicOrderVM(model, security_md_source, dynamic_type, id, display_name); - public static IOrderTypeVM create(CustomOneofCase order_type, OrdersModel model, + public static IOrderTypeVM create(CustomOneofCase order_type, string id, string display_name, OrdersModel model, IObservable security_md_source, string dynamic_type = null ) { switch (order_type) { case CustomOneofCase.Market: return market(model); case CustomOneofCase.Limit: return limit(model); - case CustomOneofCase.Stop: return stop(model); - case CustomOneofCase.StopLimit: return stop_limit(model, security_md_source); - case CustomOneofCase.TimeBox: return time_box(model, security_md_source); - case CustomOneofCase.Tick: return tick(model, security_md_source); - case CustomOneofCase.Lean: return lean(model, security_md_source); - case CustomOneofCase.Join: return join(model, security_md_source); - case CustomOneofCase.Top: return top(model, security_md_source); - case CustomOneofCase.Cover: return cover(model, security_md_source); - case CustomOneofCase.Trigger: return trigger(model, security_md_source); - case CustomOneofCase.Iceberg: return iceberg(model, security_md_source); - case CustomOneofCase.Spreader: return spreader(model, security_md_source.Select(smd => smd.security)); - case CustomOneofCase.Dynamic: return dynamic(model, security_md_source, dynamic_type); + case CustomOneofCase.Stop: return stop(model, id, display_name); + case CustomOneofCase.StopLimit: return stop_limit(model, security_md_source, id, display_name); + case CustomOneofCase.TimeBox: return time_box(model, security_md_source, id, display_name); + case CustomOneofCase.Tick: return tick(model, security_md_source, id, display_name); + case CustomOneofCase.Lean: return lean(model, security_md_source, id, display_name); + case CustomOneofCase.Join: return join(model, security_md_source, id, display_name); + case CustomOneofCase.Top: return top(model, security_md_source, id, display_name); + case CustomOneofCase.Cover: return cover(model, security_md_source, id, display_name); + case CustomOneofCase.Trigger: return trigger(model, security_md_source, id, display_name); + case CustomOneofCase.Iceberg: return iceberg(model, security_md_source, id, display_name); + case CustomOneofCase.Spreader: return spreader(model, security_md_source.Select(smd => smd.security), id, display_name); + case CustomOneofCase.Dynamic: return dynamic(model, security_md_source, dynamic_type, id, display_name); default: throw new NotImplementedException(); } } @@ -51,12 +51,17 @@ namespace mbts.spark { protected readonly OrdersModel model; protected OrderFactory factory => model.factory; - protected OrderTypeVMBase(OrdersModel model) => this.model = model; + protected OrderTypeVMBase(OrdersModel model, string id, string display_name) { + this.model = model; + this.id = id; + this.display_name = display_name; + } public abstract CustomOneofCase order_type { get; } public abstract string type_name { get; } public bool is_bts_type => display_name.StartsWith("BTS"); - public string display_name => OrderUtils.order_type_display_name(order_type); + public string id { get; } + public string display_name { get; } public OrderMetadata meta => factory.get_metadata(order_type); public virtual bool has_editable_properties => !meta.NoProps; public virtual bool editable_properties_open { get; set; } @@ -76,7 +81,7 @@ namespace mbts.spark { } private abstract class OrderTypeVMBase : OrderTypeVMBase where T : Google.Protobuf.IMessage, new() { - protected OrderTypeVMBase(OrdersModel model) : base(model) { } + protected OrderTypeVMBase(OrdersModel model, string id, string display_name) : base(model, id, display_name) { } public override CustomOneofCase order_type => OrderDataHelper.get_order_type(new T()); diff --git a/src/client/spark/Orders/OrderTypeVMs/SpreaderOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/SpreaderOrderVM.cs index 40f43a9bd..7ac53dcc6 100644 --- a/src/client/spark/Orders/OrderTypeVMs/SpreaderOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/SpreaderOrderVM.cs @@ -13,20 +13,20 @@ namespace mbts.spark { readonly StrategyEditorWrapper _strategy_editor_wrapper; IDisposable _dispose; - public SpreaderOrderVM(OrdersModel model, IObservable security_source) + public SpreaderOrderVM(OrdersModel model, IObservable security_source, string id, string display_name) : this(model, security_source, security_source.SelectMany(s => model.spreader.security_strategy(s.Id) .Select(strategy => { var so = model.factory.spreader_order(); so.Security = s.CloneToMutable(); so.ExecutionStrategy = strategy.CloneToMutable(); return new InlineUserProperty(so, true); - }))) { } + })), id, display_name) { } public SpreaderOrderVM( OrdersModel model, IObservable security_source, - IObservable> order_template_source) - : base(model) { + IObservable> order_template_source, string id, string display_name) + : base(model, id, display_name) { _template = backing(nameof(template), new SpreaderOrderDefinitionImmutable?()); _strategy_editor_wrapper = new StrategyEditorWrapper(this); _dispose = Disposable.Create( diff --git a/src/client/spark/Orders/OrderTypeVMs/StopLimitOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/StopLimitOrderVM.cs index f6620cfa3..0af2b19ec 100644 --- a/src/client/spark/Orders/OrderTypeVMs/StopLimitOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/StopLimitOrderVM.cs @@ -29,10 +29,11 @@ namespace mbts.spark { InlineStopLimitOrderProperties _inline_props; IDisposable _dispose; - internal StopLimitOrderVM(OrdersModel model, IObservable security_md_source) - : this(model, security_md_source.Select(smd => smd.security.spec_or_default()), InlineStopLimitOrderProperties.subscribe(security_md_source)) { } + internal StopLimitOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) + : this(model, security_md_source.Select(smd => smd.security.spec_or_default()), InlineStopLimitOrderProperties.subscribe(security_md_source), id, display_name) { } - internal StopLimitOrderVM(OrdersModel model, IObservable spec_source, IObservable inline_properties) : base(model) { + internal StopLimitOrderVM(OrdersModel model, IObservable spec_source, IObservable inline_properties, string id, string display_name) + : base(model, id, display_name) { _stop_price = new UserProperty(backing(nameof(stop_price), double.NaN)); _dispose = Disposable.Create( spec_source.OnDispatcher().Subscribe(s => { diff --git a/src/client/spark/Orders/OrderTypeVMs/StopOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/StopOrderVM.cs index 47b333abc..3fc291428 100644 --- a/src/client/spark/Orders/OrderTypeVMs/StopOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/StopOrderVM.cs @@ -6,8 +6,7 @@ namespace mbts.spark { private class StopOrderVM : OrderTypeVMBase { double _stop_price = 0; - internal StopOrderVM(OrdersModel model) : base(model) { } - + internal StopOrderVM(OrdersModel model, string id, string display_name) : base(model, id, display_name) { } public double stop_price { get => _stop_price; diff --git a/src/client/spark/Orders/OrderTypeVMs/TickOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/TickOrderVM.cs index 3f9f62663..ee868ef2b 100644 --- a/src/client/spark/Orders/OrderTypeVMs/TickOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/TickOrderVM.cs @@ -17,11 +17,11 @@ namespace mbts.spark { class InlineTickOrderProperties { - public InlineTickOrderProperties (CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) + public InlineTickOrderProperties (OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); tick_offset_on_deploy = new InlineUserProperty(0, false); cross_threshold = new InlineUserProperty( new bts.spark.QuantityMeasure() { Ratio = 1.0, Absolute = 1}, false); @@ -29,8 +29,8 @@ namespace mbts.spark max_chase_ticks = new InlineUserProperty(10, false); detach_on_toward = new InlineUserProperty(true, false); } - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; public readonly InlineUserProperty tick_offset_on_deploy; public readonly InlineUserProperty cross_threshold; @@ -43,8 +43,8 @@ namespace mbts.spark { readonly IObservable _security_md_source; SecurityImmutable? _security; - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineTickOrderProperties _inline_props; IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; @@ -55,12 +55,12 @@ namespace mbts.spark public uint max_chase_ticks { get; set; } = 10; public bool detach_on_toward { get; set; } = true; - public TickOrderVM(OrdersModel model, IObservable security_md_source) + public TickOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) : this(model, security_md_source, security_md_source.Select(smd => - new InlineTickOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) { } + new InlineTickOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } public TickOrderVM(OrdersModel model, IObservable security_md_source, - IObservable inline_properties) : base(model) + IObservable inline_properties, string id, string display_name) : base(model, id, display_name) { _security_md_source = security_md_source; _dispose = Disposable.Create( @@ -74,7 +74,7 @@ namespace mbts.spark } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } if (p.tick_offset_on_deploy.force_override) { @@ -131,7 +131,7 @@ namespace mbts.spark get { if (_child_order_choices == null) - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _child_order_choices; } } @@ -177,7 +177,7 @@ namespace mbts.spark void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } diff --git a/src/client/spark/Orders/OrderTypeVMs/TimeBoxOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/TimeBoxOrderVM.cs index cd3f058cc..d8410c1a5 100644 --- a/src/client/spark/Orders/OrderTypeVMs/TimeBoxOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/TimeBoxOrderVM.cs @@ -14,15 +14,15 @@ using static bts.api.orderman.CustomOrderDefinition; namespace mbts.spark { static partial class OrderTypeVMFactory { class InlineTimeBoxOrderProperties { - public InlineTimeBoxOrderProperties(CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) { + public InlineTimeBoxOrderProperties(OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); start = new InlineUserProperty(DateTime.Now, true); stop = new InlineUserProperty(DateTime.Now.AddMinutes(5), true); } - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; public readonly InlineUserProperty start, stop; } @@ -30,8 +30,8 @@ namespace mbts.spark { const double _start_immediately_delay_ms = 500; readonly IObservable _security_md_source; SecurityImmutable? _security; - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineTimeBoxOrderProperties _inline_props; TimeTextTuple _absolute_start = TimeTextTuple.FromDateTime(DateTime.Now.AddMinutes(5)), _absolute_stop = TimeTextTuple.FromDateTime(DateTime.Now.AddMinutes(10)); TimeSpan? _relative_start = TimeSpan.FromMinutes(5), _relative_stop = TimeSpan.FromMinutes(5); @@ -40,14 +40,14 @@ namespace mbts.spark { IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; - public TimeBoxOrderVM(OrdersModel model, IObservable security_md_source) + public TimeBoxOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) : this(model, security_md_source, security_md_source.Select(smd => - new InlineTimeBoxOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) { } + new InlineTimeBoxOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } public TimeBoxOrderVM(OrdersModel model, IObservable security_md_source, - IObservable inline_properties) - : base(model) { + IObservable inline_properties, string id, string display_name) + : base(model, id, display_name) { _security_md_source = security_md_source; _dispose = Disposable.Create( security_md_source.OnDispatcher() @@ -58,7 +58,7 @@ namespace mbts.spark { _order_types.SetItems(p.child_order_types.value); } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } if (p.start.force_override) { absolute_start = TimeTextTuple.FromDateTime(p.start.value); @@ -94,7 +94,7 @@ namespace mbts.spark { public ICollection child_order_types { get { if (_child_order_choices == null) { - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); } return _child_order_choices; } @@ -173,7 +173,7 @@ namespace mbts.spark { void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } diff --git a/src/client/spark/Orders/OrderTypeVMs/TopOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/TopOrderVM.cs index 1a315a130..afc2279a0 100644 --- a/src/client/spark/Orders/OrderTypeVMs/TopOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/TopOrderVM.cs @@ -17,17 +17,17 @@ namespace mbts.spark { class InlineTopOrderProperties { - public InlineTopOrderProperties(CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) + public InlineTopOrderProperties(OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); var hedgetype = CustomOneofCase.Lean; - if(!child_order_types.Contains(hedgetype)) + if(!child_order_types.Any(ct => ct.key.type == hedgetype)) hedgetype = CustomOneofCase.Limit; - this.selected_hedge_order_type = new InlineUserProperty(hedgetype, true); - this.hedge_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_hedge_order_type = new InlineUserProperty(new OrderTemplateHeader(hedgetype), true); + this.hedge_order_types = new InlineUserProperty>(child_order_types, true); watch_level_tick_offset = new InlineUserProperty(0, false); top_threshold = new InlineUserProperty( @@ -35,10 +35,10 @@ namespace mbts.spark enable_hedge = new InlineUserProperty(false, false); hedge_ticks = new InlineUserProperty(1, false); } - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; - public readonly InlineUserProperty selected_hedge_order_type; - public readonly InlineUserProperty> hedge_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_hedge_order_type; + public readonly InlineUserProperty> hedge_order_types; public readonly InlineUserProperty watch_level_tick_offset; @@ -53,13 +53,13 @@ namespace mbts.spark { readonly IObservable _security_md_source; SecurityImmutable? _security; - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineTopOrderProperties _inline_props; IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; - ObservableAutoWrapper _hedge_order_choices; + ObservableAutoWrapper _hedge_order_choices; IOrderTypeVM _hedge_order; IDisposable _hedge_order_errors_sub; @@ -77,13 +77,13 @@ namespace mbts.spark } public int hedge_ticks { get; set; } = 1; - public TopOrderVM(OrdersModel model, IObservable security_md_source) + public TopOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) : this(model, security_md_source, security_md_source.Select(smd => - new InlineTopOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) + new InlineTopOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } public TopOrderVM(OrdersModel model, IObservable security_md_source, - IObservable inline_properties) : base(model) + IObservable inline_properties, string id, string display_name) : base(model, id, display_name) { _security_md_source = security_md_source; _dispose = Disposable.Create( @@ -98,11 +98,11 @@ namespace mbts.spark } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } if (_hedge_order_choices != null && (_hedge_order == null || p.selected_hedge_order_type.force_override)) { - _select_hedge_order(p.selected_hedge_order_type.value); + _select_hedge_order(p.selected_hedge_order_type.value.key.type); } if (p.watch_level_tick_offset.force_override) { @@ -157,7 +157,7 @@ namespace mbts.spark get { if (_child_order_choices == null) - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _child_order_choices; } } @@ -167,7 +167,7 @@ namespace mbts.spark get { if (_hedge_order_choices == null) - _hedge_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _hedge_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _hedge_order_choices; } } @@ -241,7 +241,7 @@ namespace mbts.spark void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } @@ -257,7 +257,7 @@ namespace mbts.spark void _select_hedge_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_hedge_order_type.has_value ? - _inline_props.selected_hedge_order_type.value : + _inline_props.selected_hedge_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_hedge_order(ot); } diff --git a/src/client/spark/Orders/OrderTypeVMs/TriggerOrderVM.cs b/src/client/spark/Orders/OrderTypeVMs/TriggerOrderVM.cs index 9bd4a24a2..23b12fddc 100644 --- a/src/client/spark/Orders/OrderTypeVMs/TriggerOrderVM.cs +++ b/src/client/spark/Orders/OrderTypeVMs/TriggerOrderVM.cs @@ -14,8 +14,8 @@ using static bts.api.orderman.CustomOrderDefinition; namespace mbts.spark { static partial class OrderTypeVMFactory { class InlineTriggerOrderProperties { - public readonly InlineUserProperty selected_child_order_type; - public readonly InlineUserProperty> child_order_types; + public readonly InlineUserProperty selected_child_order_type; + public readonly InlineUserProperty> child_order_types; public readonly InlineUserProperty trigger_price_type; public readonly InlineUserProperty payup_price_type; public readonly InlineUserProperty fixed_price; @@ -23,10 +23,10 @@ namespace mbts.spark { public readonly InlineUserProperty use_fixed_price, is_priceout_checked, is_timeout_checked; public readonly InlineUserProperty timeout_secs; - public InlineTriggerOrderProperties(CustomOneofCase selected_child_order_type, IReadOnlyCollection child_order_types) { + public InlineTriggerOrderProperties(OrderTemplateHeader selected_child_order_type, IReadOnlyCollection child_order_types) { ExceptionAssert.InvalidOperation.That(child_order_types.Contains(selected_child_order_type), $"Child order type {selected_child_order_type} is not in child_order_types"); - this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); - this.child_order_types = new InlineUserProperty>(child_order_types, true); + this.selected_child_order_type = new InlineUserProperty(selected_child_order_type, true); + this.child_order_types = new InlineUserProperty>(child_order_types, true); trigger_price_type = new InlineUserProperty(TriggerPriceType.Bid, false); payup_price_type = new InlineUserProperty(); use_fixed_price = new InlineUserProperty(false); @@ -47,19 +47,20 @@ namespace mbts.spark { readonly UserProperty _use_fixed_price, _is_timeout_checked, _is_priceout_checked; SecurityMD? _security_md; SecurityImmutable? _security => _security_md.security(); - ObservableCollection _order_types = new ObservableCollection(); - ObservableAutoWrapper _child_order_choices; + ObservableCollection _order_types = new ObservableCollection(); + ObservableAutoWrapper _child_order_choices; InlineTriggerOrderProperties _inline_props; IOrderTypeVM _child_order; IDisposable _dispose, _child_order_errors_sub; ulong _bbo_ref = 0, _last_trade_ref = 0; double _best_bid = double.NaN, _best_ask = double.NaN, _last_trade_price = double.NaN; - public TriggerOrderVM(OrdersModel model, IObservable security_md_source) + public TriggerOrderVM(OrdersModel model, IObservable security_md_source, string id, string display_name) : this(model, security_md_source, security_md_source.Select(smd => - new InlineTriggerOrderProperties(model.factory.default_order_type(smd.security), model.factory.default_order_types(smd.security).ToList()))) { } + new InlineTriggerOrderProperties(new OrderTemplateHeader(model.factory.default_order_type(smd.security)), model.factory.default_order_types(smd.security).Select(t => new OrderTemplateHeader(t)).ToList())), id, display_name) { } - public TriggerOrderVM(OrdersModel model, IObservable security_md_source, IObservable inline_properties) : base(model) { + public TriggerOrderVM(OrdersModel model, IObservable security_md_source, IObservable inline_properties, string id, string display_name) + : base(model, id, display_name) { _trigger_price_type = create_user_prop(nameof(trigger_price_type), TriggerPriceType.Bid); _use_fixed_price = create_user_prop(nameof(use_fixed_price), true); _fixed_price = create_user_prop(nameof(fixed_price), double.NaN); @@ -88,7 +89,7 @@ namespace mbts.spark { _order_types.SetItems(p.child_order_types.value); } if (_child_order_choices != null && (_child_order == null || p.selected_child_order_type.force_override)) { - _select_child_order(p.selected_child_order_type.value); + _select_child_order(p.selected_child_order_type.value.key.type); } // trigger price _trigger_price_type.on_inline_prop(p.trigger_price_type); @@ -135,7 +136,7 @@ namespace mbts.spark { public ICollection child_order_types { get { if (_child_order_choices == null) - _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot, model, _security_md_source)); + _child_order_choices = new ObservableAutoWrapper(_order_types, ot => create(ot.key.type, ot.key.name, ot.display_name, model, _security_md_source)); return _child_order_choices; } } @@ -216,7 +217,7 @@ namespace mbts.spark { void _select_child_order() { CustomOneofCase ot = _inline_props != null && _inline_props.selected_child_order_type.has_value ? - _inline_props.selected_child_order_type.value : + _inline_props.selected_child_order_type.value.key.type : model.factory.default_order_type(_security.Value, true); _select_child_order(ot); } diff --git a/src/client/spark/Orders/OrdersModel.cs b/src/client/spark/Orders/OrdersModel.cs index 0b5c3ba1e..168feca9a 100644 --- a/src/client/spark/Orders/OrdersModel.cs +++ b/src/client/spark/Orders/OrdersModel.cs @@ -32,7 +32,7 @@ namespace mbts.spark { IDisposable _dispose; [ImportingConstructor] - private OrdersModel(IBus bus, OrderFactory factory, OrderService service, PositionService position_service, SpreaderModel spreader, ErrorReporter error) { + private OrdersModel(IBus bus, OrderFactory factory, OrderService service, PositionService position_service, SpreaderModel spreader, ErrorReporter error, AdminModel admin_model) { _bus = bus; _factory = factory; _service = service; @@ -41,15 +41,15 @@ namespace mbts.spark { _error = error; var scheduler = ThreadPoolScheduler.Instance; var interval = TimeSpan.FromMilliseconds(100); - on_orders = _service.subscribei_orders().Naggle(interval, scheduler).Table().Cache(); - on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).Table().Cache(); + on_orders = _service.subscribei_orders().Naggle(interval, scheduler).PermissionFilter(admin_model).Table().Cache(); + on_positions = _position_service.subscribei_position_data().Naggle(interval, scheduler).PermissionFilter(admin_model).Table().Cache(); working_orders = on_orders.Scan( ImmutableDictionary.Empty, (workers, table) => _update_working_orders(workers, table)) .Replay(1).RefCount(); on_order_tuples = OrderRecordTupleSource.create_observable(bus.subscribe(SparkTags.SecuritiesTag), on_orders); - on_fills = _service.subscribei_fills().Naggle(interval, scheduler).Table().Cache(); + on_fills = _service.subscribei_fills().Naggle(interval, scheduler).PermissionFilter(admin_model).Table().Cache(); _activity = new OrderActivitySummarizer(bus, on_orders); on_fill_tuples = FillTupleSource.create_observable(bus.subscribe(SparkTags.SecuritiesTag), on_orders, on_fills); _security_orders_demuxer = new TableUpdateDemuxer(o => o.Key, o => o.security_key()); diff --git a/src/client/spark/Spark.csproj b/src/client/spark/Spark.csproj index 76a20bc1a..b6f3b57a2 100644 --- a/src/client/spark/Spark.csproj +++ b/src/client/spark/Spark.csproj @@ -75,6 +75,11 @@ Code + + + MSBuild:Compile + + diff --git a/src/client/spark/core/Bus/SparkCommands.cs b/src/client/spark/core/Bus/SparkCommands.cs index f134ace8a..bb516d5c3 100644 --- a/src/client/spark/core/Bus/SparkCommands.cs +++ b/src/client/spark/core/Bus/SparkCommands.cs @@ -113,10 +113,10 @@ namespace bts.spark // TODO: remove this method, eventually want all windows to use window_settings_command() public static SparkCommand change_securities_command(Func<(PartialCommand, SecurityCriteria)> get_param) => - SparkCommand.create("Settings", Icons.Admin, SparkTags.ShowAndEditPickMultiple, get_param); + SparkCommand.create("Securities", Icons.HandPointingOverDocument, SparkTags.ShowAndEditPickMultiple, get_param); public static PartialCommand change_securities_callback(string window_name) => - SparkCommand.create(window_name, Icons.Admin, new SparkTags.SparkTag($"Change {window_name} securities callback")); + SparkCommand.create(window_name, Icons.HandPointingOverDocument, new SparkTags.SparkTag($"Change {window_name} securities callback")); public static SparkCommand window_settings_command(IDialogCommand launch_settings_editor) => new SparkCommandWrapper("Settings", Icons.Admin, launch_settings_editor); diff --git a/src/client/spark/core/Exceptions/ExceptionBuilder.cs b/src/client/spark/core/Exceptions/ExceptionBuilder.cs index 71d91df74..262f451fe 100644 --- a/src/client/spark/core/Exceptions/ExceptionBuilder.cs +++ b/src/client/spark/core/Exceptions/ExceptionBuilder.cs @@ -5,6 +5,9 @@ namespace mbts.utils { public static class Argument { public static ArgumentException must_be(string description, string name) => new ArgumentException($"Must be {description}.", name); + + public static ArgumentException unexpected_case(T value, string name) => + new ArgumentException($"Unexpected {typeof(T)} '{value}.'", name); } } } \ No newline at end of file diff --git a/src/client/spark/core/FontsAndColors/BTSColors.cs b/src/client/spark/core/FontsAndColors/BTSColors.cs index 24d8cbb59..cb52e8c2e 100644 --- a/src/client/spark/core/FontsAndColors/BTSColors.cs +++ b/src/client/spark/core/FontsAndColors/BTSColors.cs @@ -608,6 +608,12 @@ namespace mbts.ui { public static BTSColor positive_diff { get { return Color.FromRgb(0, 90, 50); } } } + public static class LadderSpecial { + public static BTSColor passive_volume_bar { get; } = Colors.SlateBlue; + public static BTSColor total_volume_bar { get; } = 0x009B69; + public static BTSColor default_column_background { get; } = 0xBABABA; + } + // Style Guide Colors public static Color brand_lightest { get { return UI.brand_lightest; } } public static Brush brand_lightest_brush { get { return new SolidColorBrush(brand_lightest); } } diff --git a/src/client/spark/core/GlobalExtensions/GenericCollectionExtensions.cs b/src/client/spark/core/GlobalExtensions/GenericCollectionExtensions.cs index 5a121369f..900a84bdb 100644 --- a/src/client/spark/core/GlobalExtensions/GenericCollectionExtensions.cs +++ b/src/client/spark/core/GlobalExtensions/GenericCollectionExtensions.cs @@ -271,9 +271,6 @@ namespace System.Collections.Generic { #endregion #region Max - public static T Max(this IEnumerable enumerable, Func select_value) where T : IComparable => - Max(enumerable, select_value, default(T)); - public static T Max(this IEnumerable enumerable, Func select_value, T return_if_empty) where T : IComparable { var etr = enumerable.GetEnumerator(); if (!etr.MoveNext()) return return_if_empty; diff --git a/src/client/spark/core/GlobalExtensions/GlobalExtensions.cs b/src/client/spark/core/GlobalExtensions/GlobalExtensions.cs index 944ce0305..1bdd74b4d 100644 --- a/src/client/spark/core/GlobalExtensions/GlobalExtensions.cs +++ b/src/client/spark/core/GlobalExtensions/GlobalExtensions.cs @@ -153,40 +153,6 @@ namespace mbts return rv.ToDictionary(kv => kv.Key, kv => kv.Value); } - //this overloaded declaration, (as opposed to one function with comp = null) allows observable_collection.sync_with to be passed into functions as a parameterless Action - public static void sync_with(this IList dest, IEnumerable src) { dest.sync_with(src, EqualityComparer.Default); } - public static void sync_with(this IList dest, IEnumerable src, IEqualityComparer comp) { - Log.notnull(dest); - if (src == null) { - dest.Clear(); - return; - } - var srcList = src as IList ?? src.ToList(); - var changes = bts.utils.Diff.diff(dest, srcList, comp).ToList(); - // Apply the diff - for(int i = 0; i < changes.Count; ++i) { - var c = changes[i]; - // removes - for (var x = 0; x < c.deletedLeft; ++x) { - dest.RemoveAt(c.StartLeft); - } - // inserts - for (var x = 0; x < c.insertedRight; ++x) { - dest.Insert(c.StartLeft + x, srcList[c.StartRight + x]); - } - - // Adjust start index of remaining changes - for (int ii = i + 1; ii < changes.Count; ++ii) { - var cc = changes[ii]; - var delta = c.insertedRight - c.deletedLeft; - if (cc.StartLeft >= c.StartLeft) { - cc.StartLeft += delta; - changes[ii] = cc;//remember: struct - } - } - } - } - public static bool sync_with_setwise(this ObservableCollection dest, IEnumerable src) { return dest.sync_with_setwise(src, EqualityComparer.Default); } public static bool sync_with_setwise(this ObservableCollection dest, IEnumerable src, IEqualityComparer comp) { if (!src.is_distinct(comp)) { diff --git a/src/client/spark/core/GlobalExtensions/TickExtensions.cs b/src/client/spark/core/GlobalExtensions/TickExtensions.cs index 648f9b4b7..2ed66ffbc 100644 --- a/src/client/spark/core/GlobalExtensions/TickExtensions.cs +++ b/src/client/spark/core/GlobalExtensions/TickExtensions.cs @@ -103,9 +103,23 @@ namespace mbts { return _round_tick(tuf, val); } - public static (int n_low, int n_high) tickdiff(this RegionalTickFactors regional_tick, double x, double y) { - if (DoubleHelper.IsNaNOrInfinity(x)) throw new ArgumentException("Cannot be NaN or Infinity.", nameof(x)); - if (DoubleHelper.IsNaNOrInfinity(y)) throw new ArgumentException("Cannot be NaN or Infinity.", nameof(y)); + public static bool try_get_tickdiff(this RegionalTickFactors regional_tick, double x, double y, out int n_low, out int n_high) { + if (DoubleHelper.IsNaNOrInfinity(x) || DoubleHelper.IsNaNOrInfinity(y)) { + n_low = default; + n_high = default; + return false; + } + (n_low, n_high) = tickdiff_unchecked(regional_tick, x, y); + return true; + } + + /// + /// Callers should be certain that and are not nan or infinity. + /// + /// + /// + /// + internal static (int n_low, int n_high) tickdiff_unchecked(this RegionalTickFactors regional_tick, double x, double y) { double ltie = regional_tick.low_tick_inclusive_end; double raw_diff = x - y; if (x < ltie) { diff --git a/src/client/spark/core/ShellInterface/UserSettings/UserSettings.cs b/src/client/spark/core/ShellInterface/UserSettings/UserSettings.cs index 23c2cb0c7..73c79bb5a 100644 --- a/src/client/spark/core/ShellInterface/UserSettings/UserSettings.cs +++ b/src/client/spark/core/ShellInterface/UserSettings/UserSettings.cs @@ -1,5 +1,5 @@ -using bts.proto; -using Google.Protobuf.WellKnownTypes; +using Google.Protobuf.WellKnownTypes; +using mbts.ui.settings; using ProtoBuf; using System; @@ -12,7 +12,12 @@ namespace mbts.spark { public static UserSettings factory_defaults => new UserSettings { _ladder = LadderSettings.Default, - _notification = NotificationSettings.Default + _notification = NotificationSettings.Default, + _market_view_grid = bts.shell.MarketViewWindowFactory.StartupData.DefaultGridSettings, + _fills_grid = bts.shell.FillsWindowFactory.StartupData.DefaultGridSettings, + _time_and_sales_grid = bts.shell.TimeAndSalesWindowFactory.StartupData.DefaultGridSettings, + _synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings, + _order_book_grid = bts.shell.OrderBookWindowFactory.OrderBookStartupData.DefaultGrid }; [ProtoMember(1)] @@ -21,19 +26,53 @@ namespace mbts.spark { // This way, when they are enabled, there won't be any backwards compatability issues to worry about, as no clients will have persisted them yet. NotificationSettings _notification; + [ProtoMember(5)] + GridSettings _market_view_grid; + [ProtoMember(6)] + GridSettings _time_and_sales_grid; + [ProtoMember(7)] + GridSettings _fills_grid; + [ProtoMember(8)] + GridSettings _synthetic_management_grid; + [ProtoMember(9)] + GridSettings _pnl_grid; + [ProtoMember(10)] + bts.shell.OrderBookWindowFactory.OrderBookGridSettings _order_book_grid; + public UserSettings() { } public LadderSettings ladder => _ladder ?? (_ladder = LadderSettings.Default); public NotificationSettings notification => _notification ?? (_notification = NotificationSettings.Default); + public GridSettings market_view_grid => _market_view_grid ?? (_market_view_grid = bts.shell.MarketViewWindowFactory.StartupData.DefaultGridSettings); + public GridSettings time_and_sales_grid => _time_and_sales_grid ?? (_time_and_sales_grid = GridSettings.Default.with(bts.shell.TimeAndSalesWindowFactory.StartupData.DefaultColumns.all)); + public GridSettings fills_grid => _fills_grid ?? (_fills_grid = bts.shell.FillsWindowFactory.StartupData.DefaultGridSettings); + public GridSettings synthetic_management_grid => _synthetic_management_grid ?? (_synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings); + public GridSettings pnl_grid => _pnl_grid ?? (_pnl_grid = bts.shell.PAndLWindowFactory.PAndLStartupData.DefaultGridSettings); + public bts.shell.OrderBookWindowFactory.OrderBookGridSettings order_book_grid => _order_book_grid ?? (_order_book_grid = bts.shell.OrderBookWindowFactory.OrderBookStartupData.Default.grid_settings_wrapper()); - public UserSettings with(LadderSettings ladder = null, NotificationSettings notification = null) => + public UserSettings with(LadderSettings ladder = null, + NotificationSettings notification = null, + GridSettings provided_market_view_gridsettings = null, + GridSettings time_and_sales = null, + GridSettings provided_fills_gridsettings = null, + GridSettings provided_synthetic_management_gridsettings = null, + GridSettings provided_pnl_gridsettings = null, + bts.shell.OrderBookWindowFactory.OrderBookGridSettings order_book_grid = null) => new UserSettings { _ladder = ladder ?? _ladder, - _notification = notification ?? _notification + _notification = notification ?? _notification, + _market_view_grid = provided_market_view_gridsettings ?? _market_view_grid, + _time_and_sales_grid = time_and_sales ?? _time_and_sales_grid, + _fills_grid = provided_fills_gridsettings ?? _fills_grid, + _synthetic_management_grid = provided_synthetic_management_gridsettings ?? _synthetic_management_grid, + _pnl_grid = provided_pnl_gridsettings ?? _pnl_grid, + _order_book_grid = order_book_grid ?? _order_book_grid }; public static class FileExtensions { - public const string LADDER = "ladder"; + public const string + LADDER = "ladder", + GRID = "grid"; } } diff --git a/src/client/spark/core/ShellInterface/UserSettings/UserSettingsHub.cs b/src/client/spark/core/ShellInterface/UserSettings/UserSettingsHub.cs index 7d8219af5..be09f4632 100644 --- a/src/client/spark/core/ShellInterface/UserSettings/UserSettingsHub.cs +++ b/src/client/spark/core/ShellInterface/UserSettings/UserSettingsHub.cs @@ -16,7 +16,8 @@ namespace mbts.persist { const string NAME_SPACE = "settings", DEFAULT_KEY = "default"; IUserPersist _persist; UserSettings _default_settings; - readonly Subject _default_settings_subject = new Subject(); + readonly BehaviorSubject _default_settings_subject = new BehaviorSubject(UserSettings.factory_defaults); + readonly object _default_settings_lock = new object(); [ImportingConstructor] public UserSettingsHub(IUserPersist persist) { @@ -33,7 +34,12 @@ namespace mbts.persist { /// Assuming there aren't any rogue agents saving UserSettings by themselves, there should never be any discrepancies. public IObservable subscribe_default_settings() => _default_settings_subject.AsObservable(); - internal UserSettings current => _default_settings ?? UserSettings.factory_defaults; + internal UserSettings current { + get { + lock (_default_settings_lock) + return _default_settings ?? UserSettings.factory_defaults; + } + } public async Task save_default(UserSettings value) { _set_and_notify(value); @@ -41,9 +47,11 @@ namespace mbts.persist { } void _set_and_notify(UserSettings value) { - _default_settings = value; - // notify observers before persisting so the changes are reflected immediately in the ui - _default_settings_subject.OnNext(value); + lock (_default_settings_lock) { + _default_settings = value; + // notify observers before persisting so the changes are reflected immediately in the ui + _default_settings_subject.OnNext(value); + } } } } \ No newline at end of file diff --git a/src/client/spark/core/ShellInterface/UserSettings/WindowSettings/IWindowSettingsManager.cs b/src/client/spark/core/ShellInterface/UserSettings/WindowSettings/IWindowSettingsManager.cs index e6734260f..7fca8e3e9 100644 --- a/src/client/spark/core/ShellInterface/UserSettings/WindowSettings/IWindowSettingsManager.cs +++ b/src/client/spark/core/ShellInterface/UserSettings/WindowSettings/IWindowSettingsManager.cs @@ -1,7 +1,9 @@ using mbts.spark; +using mbts.utils; using System; using System.Linq; using System.Reactive.Linq; +using System.Reactive.Subjects; using System.Threading.Tasks; namespace mbts.persist { @@ -11,8 +13,15 @@ namespace mbts.persist { event Action instance_settings_changed; IObservable subscribe_default(); Task save_default_settings(T value); + IWindowSettingsManagerNonShared as_non_shared(); } + /// + /// An implementation of IWindowSettingsManager that uses a clone of the actual default settings; + /// provides a convenient way to avoid problems that arise from sharing a single default settings object among multiple window instances. + /// + interface IWindowSettingsManagerNonShared : IWindowSettingsManager, IDisposable where T : class { } + class WindowSettingsManager : IWindowSettingsManager where T : class { readonly UserSettingsHub _hub; readonly Func _get; @@ -43,6 +52,42 @@ namespace mbts.persist { public IObservable subscribe_default() => _hub.subscribe_default_settings().Select(_get).DistinctUntilChanged(); + + public IWindowSettingsManagerNonShared as_non_shared() => + new NonSharedWrapper(this); + + class NonSharedWrapper : IWindowSettingsManagerNonShared { + readonly WindowSettingsManager _wsm; + readonly BehaviorSubject _default_subject; + IDisposable _dispose; + + public NonSharedWrapper(WindowSettingsManager wsm) { + _wsm = wsm; + _default_subject = new BehaviorSubject(_wsm.default_settings ?? wsm._get(UserSettings.factory_defaults)); + _dispose = wsm.subscribe_default().Subscribe(s => + _default_subject.OnNext(default_settings = MiscUtils.clone_proto(s))); + } + + public T instance_settings { + get => _wsm.instance_settings; + set => _wsm.instance_settings = value; + } + public T default_settings { get; private set; } + public event Action instance_settings_changed { + add => _wsm.instance_settings_changed += value; + remove => _wsm.instance_settings_changed -= value; + } + public async Task save_default_settings(T value) => + await _wsm.save_default_settings(value); + + public IObservable subscribe_default() => + _default_subject.AsObservable(); + + public IWindowSettingsManagerNonShared as_non_shared() => + new NonSharedWrapper(_wsm); + + public void Dispose() => Disposable.dispose(ref _dispose); + } } static class WindowSettingsManagerExtensions { @@ -54,8 +99,7 @@ namespace mbts.persist { Observable.CombineLatest( Observable.FromEvent, T>(cb => wsm.instance_settings_changed += cb, cb => wsm.instance_settings_changed -= cb) .StartWith(wsm.instance_settings), - wsm.subscribe_default() - .StartWith(wsm.default_settings), + wsm.subscribe_default(), (i, d) => i ?? d ).Where(v => v != null) .DistinctUntilChanged(); diff --git a/src/client/spark/core/Utility/CollectionHelper.cs b/src/client/spark/core/Utility/CollectionHelper.cs index 1b83ea870..2cbbec8f6 100644 --- a/src/client/spark/core/Utility/CollectionHelper.cs +++ b/src/client/spark/core/Utility/CollectionHelper.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; @@ -14,16 +13,13 @@ namespace mbts.utils { public static void sync_with_never_duplicate(ObservableCollection dest, IEnumerable src, IEqualityComparer comp) { int i = 0; foreach (var val in src.empty_if_null()) { - int curr_index = dest.TakeWhile(dval => !comp.Equals(dval, val)).Count(); - if (curr_index == dest.Count) { - dest.Add(val); - } else if (curr_index == i) { + int curr_index = dest.IndexOf(val); + if (curr_index == i) { continue; - } else if (curr_index < dest.Count) { + } else if (curr_index > 0 && curr_index < dest.Count) { dest.RemoveAt(curr_index); - dest.Insert(i, val); } - ++i; + dest.Insert(i++, val); } } @@ -49,5 +45,12 @@ namespace mbts.utils { ++i; } } + + public static TResult[] convert_array(T[] source, Func selector) { + TResult[] rv = new TResult[source.Length]; + for (int i = 0; i < rv.Length; i++) + rv[i] = selector(source[i]); + return rv; + } } } \ No newline at end of file diff --git a/src/client/spark/core/Utility/ExpirationFormatter.cs b/src/client/spark/core/Utility/ExpirationFormatter.cs deleted file mode 100644 index 1bea1ae9e..000000000 --- a/src/client/spark/core/Utility/ExpirationFormatter.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using bts; -using bts.api; - -namespace mbts.ui.utility -{ - class ExpirationFormatter - { - private static class DateFormatter - { - private static string[] month_codes = new string[] { "F", "G", "H", "J", "K", "M", "N", "Q", "U", "V", "X", "Z" }; - private static string[] month_three_letters = new string[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; - - public static string format_condensed(DateTime date, bool use_month_code, bool show_year) - { - return String.Format("{0}{1}{2}", date.Day, month_codes[date.Month - 1], show_year ? (date.Year - 2000).ToString() : String.Empty); - } - - public static string format(DateTime date, bool use_month_code, bool show_day) { - string month_text = use_month_code ? "{0}{1:00}".fmt(month_codes[date.Month - 1], date.Year - 2000) : "{0} {1:00}".fmt(month_three_letters[date.Month - 1], date.Year - 2000); - return show_day ? "{0} {1}".fmt(date.Day, month_text) : month_text; - } - - public static string format_standard(DateTime date) { return "{0} {1} {2:00}".fmt(date.Day, month_three_letters[date.Month - 1], date.Year - 2000); } - public static string format_coded(DateTime date) { return "{0} {1}{2:00}".fmt(date.Day, month_codes[date.Month - 1], date.Year - 2000); } - public static string format_coded_hide_day(DateTime date) { return "{0}{1}".fmt(month_codes[date.Month - 1], date.Year - 2000); } - - public static DateTime parse_standard(string date_text) { - string[] split = date_text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - int year = 0; - int month = Array.IndexOf(month_three_letters, split[1]) + 1; - int day = 0; - int.TryParse(split[2], out year); - int.TryParse(split[0], out day); - return new DateTime(year + 2000, month, day); - } - - public static DateTime parse_coded(string date_text) { - string[] split = date_text.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries); - string year_text = split[1].Substring(1); - string month_text = split[1].Substring(0, 1); - string day_text = split[0]; - int month = Array.IndexOf(month_codes, month_text) + 1; - int day, year; - int.TryParse(year_text, out year); - int.TryParse(day_text, out day); - return new DateTime(year + 2000, month, day); - } - - public static DateTime parse_coded_hide_day(string date_text) { - string year_text = date_text.Substring(1); - string month_text = date_text.Substring(0, 1); - int month = Array.IndexOf(month_codes, month_text) + 1; - int year; - int.TryParse(year_text, out year); - return new DateTime(year + 2000, month, 0); - } - } - - private static IDictionary> _offset_map; - - static ExpirationFormatter() - { - try - { - _offset_map = new Dictionary>(); - _offset_map[SecurityType.Option] = new Dictionary(); - _offset_map[SecurityType.Future] = new Dictionary(); - - bts.config.ExpirationDisplayOffset offset_list = mbts.config.Configuration.get(); - foreach (bts.config.ExpirationDisplayOffsetEntry e in offset_list.entries) - { - string security_name = e.security_name; - string security_type_text = e.security_type; - SecurityType security_type = SecurityType.Unset; - Enum.TryParse(security_type_text, true, out security_type); - if (security_type == SecurityType.Option || security_type == SecurityType.Future) - _offset_map[security_type][security_name] = e; - } - } - catch (Exception ex) - { - Log.Ui.error(String.Format("Exception message = {0}", ex.Message)); - } - } - - private static bts.config.ExpirationDisplayOffsetEntry _get_offset_entry(SecurityType security_type, string security_name) - { - bts.config.ExpirationDisplayOffsetEntry rv = null; - IDictionary this_security_type_map = null; - if (_offset_map.TryGetValue(security_type, out this_security_type_map)) - if (this_security_type_map.TryGetValue(security_name, out rv)) - return rv; - return rv; - } - - public static DateTime convert_real_to_mapped(SecurityType security_type, string security_name, DateTime security_expiration) - { - bts.config.ExpirationDisplayOffsetEntry offset_entry = _get_offset_entry(security_type, security_name); - DateTime rv = security_expiration; - if (offset_entry != null) - { - if (offset_entry.addition_type == bts.config.ExpirationOffsetType.type.Month) - rv = security_expiration.AddMonths(offset_entry.addition_count); - else if (offset_entry.addition_type == bts.config.ExpirationOffsetType.type.MonthMoveToStart) - { - rv = security_expiration.AddMonths(offset_entry.addition_count); - rv = new DateTime(rv.Year, rv.Month, 1); - } - } - return rv; - } - - public static string format_expiration_standard(SecurityType security_type, string security_name, DateTime security_expiration, bool map_expiration) { - return format_expiration_standard(security_type, security_name, security_expiration, map_expiration, false); - } - - public static string format_expiration_standard(SecurityType security_type, string security_name, DateTime security_expiration, bool map_expiration, bool use_month_code) { - return format_expiration_standard(security_type, security_name, security_expiration, map_expiration, use_month_code, true); - } - - public static string format_expiration_standard(SecurityType security_type, string security_name, DateTime security_expiration, bool map_expiration, bool use_month_code, bool show_day) { - DateTime mapped_expiration = map_expiration ? convert_real_to_mapped(security_type, security_name, security_expiration) : security_expiration; - return DateFormatter.format(mapped_expiration, use_month_code, show_day); - } - - public static string format_symbol_expiration_standard (SecurityType security_type, string security_name, DateTime security_expiration, bool map_expiration) { - return format_symbol_expiration_standard(security_type, security_name, security_expiration, map_expiration, false, false, true); - } - - public static string format_symbol_expiration_standard (SecurityType security_type, string security_name, DateTime security_expiration, bool map_expiration, bool show_security_type) { - return format_symbol_expiration_standard(security_type, security_name, security_expiration, map_expiration, show_security_type, false, true); - } - - public static string format_symbol_expiration_standard(SecurityType security_type, string security_name, DateTime security_expiration, bool map_expiration, bool show_security_type, bool use_month_code) { - return format_symbol_expiration_standard(security_type, security_name, security_expiration, map_expiration, show_security_type, use_month_code, true); - } - - public static string format_symbol_expiration_standard (SecurityType security_type, string security_name, DateTime security_expiration, bool map_expiration, bool show_security_type, bool use_month_code, bool show_day) { - string exp_text = format_expiration_standard(security_type, security_name, security_expiration, map_expiration, use_month_code, show_day); - return !show_security_type ? String.Format("{0} {1}", security_name, exp_text) : String.Format("{0} {1} {2}", security_type.ToString().ToUpper(), security_name, exp_text); - } - - public static string format_option_expiration_condensed(string option_name, DateTime option_expiration, bool map_expiration, bool show_symbol_name, bool show_year) - { - DateTime mapped_expiration = map_expiration ? convert_real_to_mapped(SecurityType.Option, option_name, option_expiration) : option_expiration; - return String.Format("{0}{1}", show_symbol_name ? option_name : String.Empty, DateFormatter.format_condensed(mapped_expiration, true, show_year)); - } - - public static DateTime parse_standard_formatted_expiration(SecurityType security_type, string security_name, string security_expiration) { return DateFormatter.parse_standard(security_expiration); } - } -} diff --git a/src/client/spark/core/Utility/KeyDrivenObservable.cs b/src/client/spark/core/Utility/KeyDrivenObservable.cs index 940705abf..9e3b668ed 100644 --- a/src/client/spark/core/Utility/KeyDrivenObservable.cs +++ b/src/client/spark/core/Utility/KeyDrivenObservable.cs @@ -17,7 +17,8 @@ namespace mbts.utils { /// class KeyDrivenObservableSeries : ISubject, IDisposable { readonly Func> _selector; - object _lock = new object(); + readonly object _lock = new object(); + bool _is_disposed; Subject _value_subject = new Subject(); IDisposable _current_key_sub; @@ -31,15 +32,19 @@ namespace mbts.utils { // need to lock bc simultaneous callbacks on diff threads could generate 2 subscriptions at once, // only 1 of which would get disposed in the next callback. Disposable.dispose(ref _current_key_sub); - _current_key_sub = _selector(key)?.Subscribe(_value_subject); + if (!_is_disposed) + _current_key_sub = _selector(key)?.Subscribe(_value_subject); } } public IDisposable Subscribe(IObserver observer) => _value_subject.Subscribe(observer); public void Dispose() { - Disposable.dispose(ref _current_key_sub); - Disposable.dispose(ref _value_subject); + lock (_lock) { + _is_disposed = true; + Disposable.dispose(ref _current_key_sub); + Disposable.dispose(ref _value_subject); + } } } diff --git a/src/client/spark/core/Utility/RefValueContainer.cs b/src/client/spark/core/Utility/RefValueContainer.cs new file mode 100644 index 000000000..1938241c0 --- /dev/null +++ b/src/client/spark/core/Utility/RefValueContainer.cs @@ -0,0 +1,36 @@ +namespace mbts.utils { + /// + /// Facilities multi-threaded read/write operations to a particular reference. + /// + /// The reference type to be read/written. + class RefValueContainer where T : class { + readonly object _lock = new object(); + T _value; + bool _is_dirty; + + /// + /// Returns the current value if it has not been read already, otherwise returns null. + /// + public T read_new_value() { + lock (_lock) { + if (_is_dirty) { + _is_dirty = false; + return _value; + } + return null; + } + } + + public void write_value(T value) { + lock (_lock) { + _is_dirty = true; + _value = value; + } + } + + public void mark_dirty() { + lock (_lock) + _is_dirty = true; + } + } +} \ No newline at end of file diff --git a/src/client/spark/core/Utility/ViewModelBase.cs b/src/client/spark/core/Utility/ViewModelBase.cs index d90069b73..317393440 100644 --- a/src/client/spark/core/Utility/ViewModelBase.cs +++ b/src/client/spark/core/Utility/ViewModelBase.cs @@ -110,7 +110,7 @@ namespace mbts.ui.shell #endregion #region automatic property - protected delegate void PropertyChange(T oldval, T newval); + protected internal delegate void PropertyChange(T oldval, T newval); // use nameof() to add compile time checks protected Property backing(string name, T default_value = default(T)) => @@ -526,7 +526,7 @@ namespace mbts.ui.shell /// If the user has not selected an item, but the current items do not contain the desired item, it will re-evaluate whether or not to select the item when new items arrive. /// protected SelectorProperty backing(string name, IObservable> items, IObservable> desired_item, Func select_default, IReadOnlyList> validation_steps) => - backing(name, items, desired_item, select_default, null, validation_steps); + backing(name, items, desired_item, select_default, EqualityComparer.Default, validation_steps); /// /// Returns a SelectorProperty<T> that observes an items source, and a desired item. When either observable ticks, it determines whether or not to assign the selected_item property. @@ -538,8 +538,8 @@ namespace mbts.ui.shell /// The observable sequence of a desired item. /// The fallback function to select an item by default. /// The item-matching function used to preserve a selected item when new items arrive. - protected SelectorProperty backing(string name, IObservable> items, IObservable> desired_item, Func select_default, Func items_match) => - backing(name, items, desired_item, select_default, items_match, new List>(0)); + protected SelectorProperty backing(string name, IObservable> items, IObservable> desired_item, Func select_default, IEqualityComparer comparer) => + backing(name, items, desired_item, select_default, comparer, new List>(0)); /// /// Returns a SelectorProperty<T> that observes an items source, and a desired item. When either observable ticks, it determines whether or not to assign the selected_item property. @@ -552,14 +552,24 @@ namespace mbts.ui.shell /// The fallback function to select an item by default. /// The item-matching function used to preserve a selected item when new items arrive. /// The validation steps to take when selected_item changes. - protected SelectorProperty backing(string name, IObservable> items, IObservable> desired_item, Func select_default, Func items_match, IReadOnlyList> validation_steps) { - if (_observable_properties == null) _observable_properties = new List(); - var prop = new SelectorProperty(this, name, select_default, items_match, validation_steps); - _observable_properties.Add(items.OnDispatcher().Subscribe(prop)); - _observable_properties.Add(desired_item.OnDispatcher().Subscribe(prop)); + protected SelectorProperty backing(string name, IObservable> items, IObservable> desired_item, Func select_default, IEqualityComparer comparer, IReadOnlyList> validation_steps) { + var prop = new SelectorProperty(this, name, select_default, comparer, validation_steps); + attach(prop, items, desired_item); return prop; } + protected SelectorProperty backing(string name, IObservable> items, IObservable> desired_item, Func item_to_value, Func select_default, IEqualityComparer comparer, IReadOnlyList> validation_steps) { + var prop = new SelectorProperty(this, name, item_to_value, select_default, comparer, validation_steps); + attach(prop, items, desired_item); + return prop; + } + + protected void attach(SelectorProperty selector_property, IObservable> items, IObservable> desired_item) { + if (_observable_properties == null) _observable_properties = new List(); + _observable_properties.Add(items.OnDispatcher().Subscribe(selector_property)); + _observable_properties.Add(desired_item.OnDispatcher().Subscribe(selector_property)); + } + protected void set_dispose(IDisposable disposable) { if (_dispose != null) throw new InvalidOperationException("Cannot set disposable twice"); _dispose = disposable; @@ -576,37 +586,44 @@ namespace mbts.ui.shell OnDisposed?.Invoke(true); } + protected class SelectorProperty : SelectorProperty { + public SelectorProperty(DisposableViewModelBase owner, string prop_name, Func select_default, IEqualityComparer comparer, IReadOnlyList> validation_steps) + : base(owner, prop_name, i => i, select_default, comparer, validation_steps) { } + } + /// /// Designed to observe an items source, and a [settings-driven] desired item. When a new desired item is observed, it will evaluate it's current state and determine if it should be assigned to the selected_item. /// Notes: If the user has selected an item (ie the selected_item has been assigned through the public setter), the desired item will be ignored unless force_override is true. /// If the user has not selected an item, but the current items do not contain the desired item, it will re-evaluate whether or not to select the item when new items arrive. /// - protected class SelectorProperty : IObserver>, IObserver>, IObservable<(T oldval, T newval)> { + protected class SelectorProperty : IObserver>, IObserver>, IObservable<(TValue oldval, TValue newval)> { readonly DisposableViewModelBase _owner; readonly string _prop_name; - readonly IReadOnlyList> _validation_steps; - readonly ObservableCollection _items = new ObservableCollection(); - readonly Func _select_default; - readonly Func _items_match; // used to maintain selected_item through an items-change - T _selected_item; - InlineUserProperty _desired_item; + readonly IReadOnlyList> _validation_steps; + readonly ObservableCollection _items = new ObservableCollection(); + readonly Func _select_default; + readonly IEqualityComparer _value_comparer; + readonly Func _item_to_value; + TValue _selected_item; + InlineUserProperty _desired_item = new InlineUserProperty(); bool _is_user_selected_item; - public SelectorProperty(DisposableViewModelBase owner, string prop_name, Func select_default, Func items_match, IReadOnlyList> validation_steps) { + public SelectorProperty(DisposableViewModelBase owner, string prop_name, Func item_to_value, Func select_default, IEqualityComparer value_comparer, IReadOnlyList> validation_steps) { _owner = owner; _prop_name = prop_name; + _item_to_value = item_to_value; _select_default = select_default; - _items_match = items_match; + _value_comparer = value_comparer ?? EqualityComparer.Default; _validation_steps = validation_steps; - items = new ReadOnlyObservableCollection(_items); + items = new ReadOnlyObservableCollection(_items); } - public event PropertyChange Changing; - public event PropertyChange Changed; + public event PropertyChange Changing; + public event PropertyChange Changed; - public ReadOnlyObservableCollection items { get; } + public ReadOnlyObservableCollection items { get; } - public T selected_item { + public TValue selected_item { get => _selected_item; set { _is_user_selected_item = true; @@ -614,31 +631,31 @@ namespace mbts.ui.shell } } - public IDisposable Subscribe(IObserver<(T oldval, T newval)> observer) { + public IDisposable Subscribe(IObserver<(TValue oldval, TValue newval)> observer) { Changed += pch; return Disposable.Create(() => Changed -= pch); - void pch(T o, T n) => observer.OnNext((o, n)); + void pch(TValue o, TValue n) => observer.OnNext((o, n)); } - public IObservable observe_current(bool start_now = false) { + public IObservable observe_current(bool start_now = false) { var source = this.Select(vals => vals.newval); if (start_now) - source = source.StartWith(_selected_item); + source = source.StartWith(selected_item); return source; } - void IObserver>.OnNext(InlineUserProperty iup) { + void IObserver>.OnNext(InlineUserProperty iup) { _desired_item = iup; _try_auto_select(); } - void IObserver>.OnError(Exception error) { } - void IObserver>.OnCompleted() { } + void IObserver>.OnError(Exception error) { } + void IObserver>.OnCompleted() { } - void IObserver>.OnNext(IEnumerable value) { - if (_items_match != null && _is_user_selected_item) { + void IObserver>.OnNext(IEnumerable value) { + if (_is_user_selected_item) { var curr_selected_item = selected_item; _items.sync_with(value); - if (_try_match_item(curr_selected_item, out T match)) + if (_try_match_item(curr_selected_item, out TValue match)) selected_item = match; else { _is_user_selected_item = false; @@ -646,18 +663,13 @@ namespace mbts.ui.shell } } else { _items.sync_with(value); - if (_is_user_selected_item) { - _is_user_selected_item = _items.Contains(_selected_item); - } - if (!_is_user_selected_item) { - _try_auto_select(); - } + _try_auto_select(); } } - void IObserver>.OnError(Exception error) { } - void IObserver>.OnCompleted() { } + void IObserver>.OnError(Exception error) { } + void IObserver>.OnCompleted() { } - void _run_validation(T val) { + void _run_validation(TValue val) { var invalid_steps = _validation_steps.Select(v => v.validate(val)).Where(r => !r.valid); if (invalid_steps.Count() > 0) { var error_msg = string.Join("\n", invalid_steps.Where(r => !string.IsNullOrEmpty(r.msg)).Select(r => r.msg)); @@ -667,15 +679,15 @@ namespace mbts.ui.shell void _try_auto_select() { if (_is_user_selected_item) { - if (desired_item_valid() && _desired_item.force_override && _try_match_item(_desired_item.value, out T match)) { + if (desired_item_valid() && _desired_item.force_override && _try_match_item(_desired_item.value, out TValue match)) { _set_selected_item(match); return; } - } else if (desired_item_valid() && _try_match_item(_desired_item.value, out T match)) { + } else if (desired_item_valid() && _try_match_item(_desired_item.value, out TValue match)) { _set_selected_item(match); return; } - if (_try_match_item(_select_default(), out T default_match)) + if (_try_match_item(_select_default(), out TValue default_match)) _set_selected_item(default_match); else { // the _set_selected_item has never been entered, so validation has never ran, force it here @@ -685,8 +697,8 @@ namespace mbts.ui.shell bool desired_item_valid() => (_desired_item?.has_value).GetValueOrDefault(); } - void _set_selected_item(T value) { - if (!Equals(value, _selected_item)) { + void _set_selected_item(TValue value) { + if (!_value_comparer.Equals(value, _selected_item)) { Changing?.Invoke(_selected_item, value); var old = _selected_item; _selected_item = value; @@ -696,10 +708,20 @@ namespace mbts.ui.shell } } - bool _try_match_item(T item, out T match) => - _items_match != null ? - _items.TryGetFirst(i => _items_match(i, item), out match) : - _items.TryGetFirst(i => i.Equals(item), out match); + bool _try_match_item(TValue value, out TValue match) { + foreach (TItem item in _items) { + TValue v = _item_to_value(item); + if (_value_comparer.Equals(v, value)) { + match = v; + return true; + } + } + match = default; + return false; + } + + bool _try_match_item(TItem item, out TValue match) => + _try_match_item(_item_to_value(item), out match); } public event Action OnDisposed; diff --git a/src/client/spark/core/data/Proto/ColorShowProto.cs b/src/client/spark/core/data/Proto/ColorShowProto.cs new file mode 100644 index 000000000..12b2acfe2 --- /dev/null +++ b/src/client/spark/core/data/Proto/ColorShowProto.cs @@ -0,0 +1,47 @@ +using bts.utils; +using mbts.ui; +using ProtoBuf; +using System; +using System.Windows.Media; + +namespace bts.proto { + [ProtoContract] + struct ColorShowProto : IEquatable { + [ProtoMember(1)] + readonly uint _value; + [ProtoMember(2)] + readonly bool _show; + + public ColorShowProto(Color value, bool show = true) + : this(value.rgb_color(), show) { } + + public ColorShowProto(uint value, bool show = true) { + _value = value; + _show = show; + } + + public Color color => _value.rgb_color(); + public bool show => _show; + + public Color? result { + get { + if (_show) + return color; + return null; + } + } + + public bool Equals(ColorShowProto other) => + _value == other._value && + _show == other._show; + + public override bool Equals(object obj) => + obj is ColorShowProto ncp && Equals(ncp); + + public override int GetHashCode() => + EqualityComparer.aggregate_hash(_value.GetHashCode(), _show.GetHashCode()); + + public static bool operator ==(ColorShowProto a, ColorShowProto b) => a.Equals(b); + public static bool operator !=(ColorShowProto a, ColorShowProto b) => !a.Equals(b); + } +} \ No newline at end of file diff --git a/src/client/spark/view/Controls/DIYGrid/CellProperty.cs b/src/client/spark/view/Controls/DIYGrid/CellProperty.cs index d21e1f98e..758d0474d 100644 --- a/src/client/spark/view/Controls/DIYGrid/CellProperty.cs +++ b/src/client/spark/view/Controls/DIYGrid/CellProperty.cs @@ -15,7 +15,7 @@ namespace mbts.spark { value = initial_value; } - event Action<(T oldval, T newval)> _value_set; + protected event Action<(T oldval, T newval)> _value_set; public T value { get; private set; } public string text => _text ?? (_text = _format(value)); diff --git a/src/client/spark/view/Controls/DIYGrid/CellRender/HyperlinkCell.cs b/src/client/spark/view/Controls/DIYGrid/CellRender/HyperlinkCell.cs index 887a379cf..8d9e326cb 100644 --- a/src/client/spark/view/Controls/DIYGrid/CellRender/HyperlinkCell.cs +++ b/src/client/spark/view/Controls/DIYGrid/CellRender/HyperlinkCell.cs @@ -24,7 +24,7 @@ namespace mbts.ui.controls { bool _is_mouse_over_text, _can_execute; Pen _underline_pen; - static HyperlinkCell() => + static HyperlinkCell() { WPFHelper.override_metadata( HorizontalAlignmentProperty, FrameworkPropertyMetadataOptions.AffectsArrange | @@ -33,6 +33,13 @@ namespace mbts.ui.controls { o._horizontal_alignment = v; o._glyph_run = null; }, HorizontalAlignment.Left); + WPFHelper.override_metadata( + WidthProperty, + FrameworkPropertyMetadataOptions.AffectsArrange | + FrameworkPropertyMetadataOptions.AffectsMeasure | + FrameworkPropertyMetadataOptions.AffectsRender, + (o, v) => o._glyph_run = null); + } public HyperlinkCell() { _background = Background; @@ -279,24 +286,50 @@ namespace mbts.ui.controls { drawingContext.DrawGlyphRun(_foreground, _glyph_run); if (_is_mouse_over_text) { // draw underline - var pt = new Point(Padding.Left, 0.5 * (ActualHeight + _font_size) + 1); + Point pt; + switch (_horizontal_alignment) { + case HorizontalAlignment.Left: + pt = new Point(Padding.Left, 0.5 * (ActualHeight + _font_size) + 1); + break; + case HorizontalAlignment.Right: + pt = new Point(ActualWidth - Padding.Right - _glyph_width, 0.5 * (ActualHeight + _font_size) + 1); + break; + case HorizontalAlignment.Center: + default: + pt = new Point(0.5 * (ActualWidth - _glyph_width), 0.5 * (ActualHeight + _font_size) + 1); + break; + } drawingContext.DrawLine(_underline_pen, pt, new Point(pt.X + _glyph_width, pt.Y)); + } drawingContext.Pop(); } - var bgo = GetBackgroundOverlay(this); - if (bgo != null) - drawingContext.DrawRectangle(bgo, null, rect); + if (bg != null) { + var bgo = GetBackgroundOverlay(this); + if (bgo != null) + drawingContext.DrawRectangle(bgo, null, rect); + } + } + + bool _point_horizontally_over_text(double x) { + switch (_horizontal_alignment) { + case HorizontalAlignment.Left: + return x >= _padding.Left && x <= _padding.Left + _glyph_width; + case HorizontalAlignment.Right: + return x >= ActualWidth - Padding.Right - _glyph_width && x <= ActualWidth - _padding.Right; + case HorizontalAlignment.Center: + default: + return x >= 0.5 * (ActualWidth - _glyph_width) && x <= 0.5 * (ActualWidth - _glyph_width) + _glyph_width; + } } void _check_mouse_over_text(MouseEventArgs e) { var pt = e.GetPosition(this); bool over_text = - _glyph_run != null && - pt.X >= _padding.Left && - pt.X <= _padding.Left + _glyph_width && - pt.Y >= _padding.Top && - pt.Y <= _padding.Top + _font_size; + _glyph_run != null && + _point_horizontally_over_text(pt.X) && + pt.Y >= _padding.Top && + pt.Y <= _padding.Top + _font_size; if (_is_mouse_over_text != over_text) _toggle_mouse_over_text(); } diff --git a/src/client/spark/view/Controls/DIYGrid/Columns/ColumnBase.cs b/src/client/spark/view/Controls/DIYGrid/Columns/ColumnBase.cs index 75d8b145c..8b4491cb8 100644 --- a/src/client/spark/view/Controls/DIYGrid/Columns/ColumnBase.cs +++ b/src/client/spark/view/Controls/DIYGrid/Columns/ColumnBase.cs @@ -16,22 +16,39 @@ namespace mbts.ui.controls { partial class DIYGrid { public static class DefaultColumnWidths { public const double - Account = 68, - Exchange = 74, + Account = 80, + Exchange = 86, FillKey = 68, OrderKey = 78, OrderType = 88, - Price = 56, + MViewPrice = 56, + Price = 68, + AveragePrice = 76, + LastPrice = 78, + Settlement = 82, Security = 80, SecurityKey = 90, + SecurityType = 96, Side = 47, Size = 48, - Symbol = 48, - Time = 100, + Filled = 56, + Symbol = 78, + Time = 112, TimeInForce = 47, TradeKey = 76, User = 53, - Aggressor = 80; + Aggressor = 80, + Volume = 74, + FillKind = 70, + Description = 300, + N_Legs = 68, + N_Working_Orders = 123, + Start_Net_Pos = 100, + Total_Net_Pos = 100, + PnL_Generic = 80, + Working = 72, + LastMessage = 100, + ParentKey = 100; } public static class DefaultColumnHeaders { @@ -45,6 +62,7 @@ namespace mbts.ui.controls { Price = "Price", Security = "Security", SecurityKey = "Security Key", + SecurityType = "Security Type", Side = "Side", Size = "Qty", Symbol = "Symbol", @@ -52,7 +70,30 @@ namespace mbts.ui.controls { TradeKey = "Trade Key", User = "User", Time = "Time", - Aggressor = "Aggressor"; + Aggressor = "Aggressor", + Volume = "Volume", + Last = "Last", + Pos = "Pos", + Working = "Working", + Bid = "Bid", + Ask = "Ask", + Settlement = "Settlement", + DayChange = "Change", + SessionLow = "Low", + SessionHigh = "High", + FillKind = "Fill Kind", + Name = "Name", + Description = "Description", + N_Legs = "# Legs", + N_Working_Orders = "# Working Orders", + StartNetPos = "Start Net Pos", + StartPnL = "Start PnL", + DayLong = "Day Long", + DayShort = "Day Short", + DayPnL = "Day PnL", + TotalNetPos = "Total Net Pos", + TotalPnL = "Total PnL", + PnLPrice = "PnL Price"; } public interface IKeyedColumn { @@ -158,7 +199,7 @@ namespace mbts.ui.controls { public virtual void load_from(GridColumnSettings settings) { if (settings == null) return; - using (ReflectionHelper.detect_property_changes(this, raise_prop_changed, nameof(is_visible), nameof(is_collapsed), nameof(font_weight_override), nameof(font_weight), + using (ReflectionHelper.detect_property_changes(this, _raise_prop_changed, nameof(is_visible), nameof(is_collapsed), nameof(font_weight_override), nameof(font_weight), nameof(font_color_override), nameof(font_color), nameof(font_brush), nameof(font_family), nameof(font_size), nameof(horizontal_alignment), nameof(horizontal_offset))) { // id = settings.id; @@ -176,21 +217,24 @@ namespace mbts.ui.controls { Width = original_width; } // - void raise_prop_changed(string prop) => - OnPropertyChanged(new PropertyChangedEventArgs(prop)); } // 'fallback' font-params (aka 'grid-level' font-params) public void set_fallback_font_params(FontFamily font_family, double font_size, Color font_color, FontWeight font_weight) { - _fallback_font_family = font_family; - _fallback_font_size = font_size; - _fallback_font_color = font_color; - _fallback_font_weight = font_weight; + using (ReflectionHelper.detect_property_changes(this, _raise_prop_changed, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight))) { + _fallback_font_family = font_family; + _fallback_font_size = font_size; + _fallback_font_color = font_color; + _fallback_font_weight = font_weight; + } font_params_changed?.Invoke(); } protected virtual void OnSortDirectionChanged(ListSortDirection? new_value) => sort_direction_changed?.Invoke(this, new_value); + + void _raise_prop_changed(string prop) => + OnPropertyChanged(new PropertyChangedEventArgs(prop)); } internal abstract class ColumnBase : ColumnBase, IKeyedColumn, IGridColumn { diff --git a/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/HyperlinkColumn.cs b/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/HyperlinkColumn.cs index f76c24da7..258740e29 100644 --- a/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/HyperlinkColumn.cs +++ b/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/HyperlinkColumn.cs @@ -29,9 +29,13 @@ namespace mbts.ui.controls { background_overlay = selection_fill; public override FrameworkElement view_factory() { - var hl = new HyperlinkCell { Foreground = font_brush, Background = background, FontSize = font_size, Padding = padding }; + var hl = new HyperlinkCell { Foreground = font_brush, Background = background, Padding = padding }; hl.SetBinding(HyperlinkCell.TextProperty, _binding); + hl.SetBinding(HyperlinkCell.FontFamilyProperty, new Binding(nameof(font_family)) { Source = this }); + hl.SetBinding(HyperlinkCell.FontSizeProperty, new Binding(nameof(font_size)) { Source = this }); + hl.SetBinding(HyperlinkCell.HorizontalAlignmentProperty, new Binding(nameof(horizontal_alignment)) { Source = this }); hl.SetBinding(HyperlinkCell.CommandProperty, _command_binding); + hl.SetBinding(HyperlinkCell.WidthProperty, new Binding(nameof(ActualWidth)) { Source = this }); if (background_overlay != null) { hl.SetBinding(BackgroundOverlayProperty, new Binding(nameof(ListBoxItem.IsSelected)) { Converter = new AnonymousValueConverter(val => val ? background_overlay : null), diff --git a/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/TextColumn.cs b/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/TextColumn.cs index 931dae6b6..6f22c40ef 100644 --- a/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/TextColumn.cs +++ b/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/TextColumn.cs @@ -22,8 +22,10 @@ namespace mbts.ui.controls { public VerticalAlignment vertical_alignment { get; set; } = VerticalAlignment.Center; public override FrameworkElement view_factory() { - var v = new TextCell { FontFamily = font_family, FontSize = font_size, Foreground = font_brush, HorizontalAlignment = horizontal_alignment, VerticalAlignment = vertical_alignment }; + var v = new TextCell { Foreground = font_brush, HorizontalAlignment = horizontal_alignment, VerticalAlignment = vertical_alignment }; v.SetBinding(TextCell.TextProperty, _binding); + v.SetBinding(TextCell.FontFamilyProperty, new Binding(nameof(font_family)) { Source = this }); + v.SetBinding(TextCell.FontSizeProperty, new Binding(nameof(font_size)) { Source = this }); try_set_binding(TextCell.BackgroundProperty, background_binding); try_set_binding(TextCell.ForegroundProperty, foreground_binding); try_set_binding(TextCell.FontWeightProperty, font_weight_binding); diff --git a/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/TimeAndSalesColumn.cs b/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/TimeAndSalesColumn.cs index 162cd05f4..dafdbe936 100644 --- a/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/TimeAndSalesColumn.cs +++ b/src/client/spark/view/Controls/DIYGrid/Columns/FrameworkElementColumns/TimeAndSalesColumn.cs @@ -1,21 +1,26 @@ -using mbts.spark; +using bts.shell; +using mbts.spark; using mbts.spark.filters; +using mbts.ui.spark; using mbts.ui.utility.text; using System; using System.Windows.Media; namespace mbts.ui.controls { partial class DIYGrid { - internal class TimeAndSalesColumn : RenderTextColumn { - - readonly Func _get_text; + internal class TimeAndSalesColumn : RenderTextColumn, IKeyedColumn { readonly Func _get_brush; - public TimeAndSalesColumn(Func get_text, Func get_brush, IDataGridColumnFilter filter = null, string filter_template = null, GlyphContext glyph_context = null) : base(get_text, filter, filter_template, glyph_context) { - _get_text = get_text; + public TimeAndSalesColumn(TimeAndSalesView.Columns key, Func get_brush, IDataGridColumnFilter filter = null, string filter_template = null) + : base(r => r[key], filter, filter_template) { + this.key = key; + // id is used for settings (another case of ProtoBufs vs Generics - strings seemed like the easiest way to provide an identifier for the common GridColumnSettings proto) + id = TimeAndSalesWindowFactory.col_id(key); _get_brush = get_brush; } + public TimeAndSalesView.Columns key { get; } + public override void draw(TimeAndSalesVM.Row row, DrawingContext dc, ColumnGeometry position, double row_height) { CellRender.draw_text(dc , glyph_context diff --git a/src/client/spark/view/Controls/DIYGrid/Columns/RenderColumn.cs b/src/client/spark/view/Controls/DIYGrid/Columns/RenderColumn.cs index c3a57b55b..f6bb51015 100644 --- a/src/client/spark/view/Controls/DIYGrid/Columns/RenderColumn.cs +++ b/src/client/spark/view/Controls/DIYGrid/Columns/RenderColumn.cs @@ -20,18 +20,20 @@ namespace mbts.ui.controls { } public class RenderTextColumn : RenderColumnBase, IRenderColumn, IFilterableColumn { - readonly Func _get_text; + protected readonly Func _get_text; public RenderTextColumn( Func get_text, IDataGridColumnFilter filter = null, - string filter_template = null, - GlyphContext glyph_context = null) + string filter_template = null) : base() { _get_text = get_text; this.filter = filter; this.filter_template = filter_template; - this.glyph_context = glyph_context ?? TextUtils.StandardGlyphContext; + font_params_changed += refresh_glyph_context; + refresh_glyph_context(); + void refresh_glyph_context() => + glyph_context = GlyphContext.create(font_family, font_weight); } public IDataGridColumnFilter filter { get; } @@ -39,7 +41,7 @@ namespace mbts.ui.controls { public Brush background { get; set; } public VerticalAlignment vertical_alignment { get; set; } = VerticalAlignment.Center; public Thickness padding { get; set; } = DefaultCellPadding; - public GlyphContext glyph_context { get; set; } = TextUtils.StandardGlyphContext; + public GlyphContext glyph_context { get; private set; } public override void draw(TRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) => CellRender.draw_text( diff --git a/src/client/spark/view/Controls/DIYGrid/DIYGridResources.xaml b/src/client/spark/view/Controls/DIYGrid/DIYGridResources.xaml index 0f60b2b9d..0d77a7a05 100644 --- a/src/client/spark/view/Controls/DIYGrid/DIYGridResources.xaml +++ b/src/client/spark/view/Controls/DIYGrid/DIYGridResources.xaml @@ -335,6 +335,8 @@ + + diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/GridColumnSettings.cs b/src/client/spark/view/Controls/DIYGrid/Settings/GridColumnSettings.cs index 2ad519fc1..e6ed6953f 100644 --- a/src/client/spark/view/Controls/DIYGrid/Settings/GridColumnSettings.cs +++ b/src/client/spark/view/Controls/DIYGrid/Settings/GridColumnSettings.cs @@ -92,7 +92,7 @@ namespace mbts.ui.settings { public GridColumnSettings with( string display_name = null, bool? is_visible = null, bool? is_collapsed = null, NullableFontWeightProto? font_weight_override = null, NullableColorProto? font_color_override = null, - double? width = _default_width, HorizontalAlignment? horizontal_alignment = _default_horizontal_alignment, double? horizontal_offset = null) => + double? width = null, HorizontalAlignment? horizontal_alignment = null, double? horizontal_offset = null) => // new GridColumnSettings(_id, display_name ?? this.display_name, diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/GridColumnSettingsVM.cs b/src/client/spark/view/Controls/DIYGrid/Settings/GridColumnSettingsVM.cs index 3d9db0ad0..8e042dcc6 100644 --- a/src/client/spark/view/Controls/DIYGrid/Settings/GridColumnSettingsVM.cs +++ b/src/client/spark/view/Controls/DIYGrid/Settings/GridColumnSettingsVM.cs @@ -4,7 +4,6 @@ using mbts.ui; using mbts.ui.controls; using mbts.ui.settings; using mbts.ui.shell; -using mbts.ui.utilities; using mbts.ui.utility; using System; using System.Collections.Generic; @@ -28,22 +27,15 @@ namespace mbts.spark { ImmutableFontWeight } - partial class GridSettingsVM { - public interface IColumnVM : IValueFactory<(GridColumnSettings settings, ILookup animations)>, IDisposable { + partial class GridSettingsVM { + public interface IColumnVM : ISimpleColumnVM, IValueFactory<(GridColumnSettings settings, ILookup animations)>, IDisposable { ObservableEditableCollection predicate_animations { get; } - string id { get; } - string display_name { get; set; } - double width { get; set; } - bool is_visible { get; set; } - bool is_font_color_overridden { get; set; } - ColorPickerPopupVM font_color_override { get; } - bool is_font_weight_overridden { get; set; } - FontWeight font_weight_override { get; set; } - void apply_flags(GridColumnSettingsFeatureFlags flags); + new (GridColumnSettings settings, ILookup animations) create(); + new void load_from((GridColumnSettings settings, ILookup animations) args); } public class ColumnVM : DisposableViewModelBase, IColumnVM { - public readonly GridSettingsVM owner; + public readonly GridSettingsVM owner; readonly Property _id, _display_name; readonly Property _width; @@ -52,7 +44,7 @@ namespace mbts.spark { readonly Property _font_weight_override; GridColumnSettings _settings; - public ColumnVM(GridSettingsVM owner) { + public ColumnVM(GridSettingsVM owner) { this.owner = owner; _id = backing(nameof(id), string.Empty); _display_name = backing(nameof(display_name), string.Empty); @@ -107,6 +99,7 @@ namespace mbts.spark { _horizontal_alignment.set(_settings.horizontal_alignment); _is_visible.set(_settings.is_visible); _is_font_weight_overridden.set(_settings.font_weight_override.HasValue); + _is_font_color_overridden.set(_settings.font_color_override.HasValue); if (is_font_weight_overridden) { _font_weight_override.set(_settings.font_weight_override.Value); } @@ -133,6 +126,12 @@ namespace mbts.spark { } public override string ToString() => $"{display_name} ({id})"; + + GridColumnSettings IValueFactory.create() => + create().settings; + + void IValueFactory.load_from(GridColumnSettings value) => + load_from((value, new Dictionary().ToLookup(kv => kv.Key, kv => kv.Value))); } public class PredicateCellAnimationVM : DisposableViewModelBase, IValueFactory<(TPredicateBuilder predicate, IEnumerable animations)> { diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/GridSettings.cs b/src/client/spark/view/Controls/DIYGrid/Settings/GridSettings.cs index 5be7c36a5..fffee8e28 100644 --- a/src/client/spark/view/Controls/DIYGrid/Settings/GridSettings.cs +++ b/src/client/spark/view/Controls/DIYGrid/Settings/GridSettings.cs @@ -1,7 +1,9 @@ using bts.proto; +using bts.utils; using mbts.ui.controls; using mbts.ui.converters; using ProtoBuf; +using System; using System.Collections.Generic; using System.ComponentModel; using System.Windows; @@ -10,7 +12,7 @@ using System.Windows.Media; namespace mbts.ui.settings { [ProtoContract] - class GridSettings { + class GridSettings : IEquatable { const string _default_font_family = "Segoe UI"; const double _default_font_size = 11; const uint _default_font_color = 0; @@ -94,6 +96,7 @@ namespace mbts.ui.settings { public FontWeight font_weight => FontWeight.FromOpenTypeWeight(_font_weight); public ColorOpacityProto selection_highlight => _selection_highlight; public IReadOnlyList columns => _columns; + [Obsolete] // TODO: remove sorting_criterion from GridSettings entirely. This should be part of IShellWindow's StartupData, since it is an instance-specific setting, and prob not part of the shared defaults public IReadOnlyList sorting_criterion => _sorting_criterion; public static GridSettings get_default() => @@ -110,5 +113,32 @@ namespace mbts.ui.settings { sorting_criterion ?? _sorting_criterion); public GridSettings clone() => with(); + + public bool Equals(GridSettings other) => + other != null && + font_family == other.font_family && + font_size == other.font_size && + font_weight == other.font_weight && + font_color == other.font_color && + selection_highlight == other.selection_highlight && + EqualityComparer.safe_sequence_equals(columns, other.columns) && +#pragma warning disable CS0612 // Type or member is obsolete + EqualityComparer.safe_sequence_equals(sorting_criterion, other.sorting_criterion); +#pragma warning restore CS0612 // Type or member is obsolete + + public override bool Equals(object obj) => + Equals(obj as GridSettings); + + public override int GetHashCode() => + EqualityComparer.aggregate_hash( + font_family.GetHashCode(), + font_size.GetHashCode(), + font_weight.GetHashCode(), + font_color.GetHashCode(), + selection_highlight.GetHashCode(), + EqualityComparer.safe_aggregate_hash(columns), +#pragma warning disable CS0612 // Type or member is obsolete + EqualityComparer.safe_aggregate_hash(sorting_criterion)); +#pragma warning restore CS0612 // Type or member is obsolete } } \ No newline at end of file diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsManager.cs b/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsManager.cs index 22e31b721..e384e1c82 100644 --- a/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsManager.cs +++ b/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsManager.cs @@ -1,4 +1,5 @@ -using bts.api; +using bts; +using bts.api; using bts.shell; using mbts.collections; using mbts.spark; @@ -17,10 +18,10 @@ using System.Windows.Media; using static mbts.ui.controls.DIYGrid; namespace mbts.ui { - class GridSettingsManager : - ViewModelBase, IValueFactory<(GridSettings grid, IEnumerable> predicate_animations_data)>, IDisposable + class GridSettingsManager : ViewModelBase, IValueFactory, IObserver, IDisposable where TRow : IRowVM - where TPredicateBuilder : IPredicateBuilder, new() { + where TPredicateBuilder : IPredicateBuilder, new() + where TSettingsData : class, IGridSettingsWithNotifications { // public readonly ObservableCollection live_sorting_props; @@ -39,12 +40,18 @@ namespace mbts.ui { readonly DelegateProperty _font_color, _selection_highlight; readonly SetMap, AnimationDefinitionWrapper> _animation_defs = new SetMap, AnimationDefinitionWrapper>(); readonly TPredicateBuilderArg _arg; + readonly Func> _create_predicate_vm; + readonly Func> _create_column_predicate_vm; + readonly Func>, TSettingsData> _create_settings_obj; + readonly IReadOnlyDictionary _column_flags; IEnumerable> _predicate_animations_data; public GridSettingsManager( IObservable owner_title, IBus bus, INotificationService notification_service, OrdersModel orders_model, ObservableCollection rows, ICollectionView rows_view, ObservableCollection columns, Func> create_predicate_vm, - Func> create_column_predicate_vm, TPredicateBuilderArg arg, IReadOnlyDictionary column_flags = null) { + Func> create_column_predicate_vm, + Func>, TSettingsData> create_settings_obj, + TPredicateBuilderArg arg, IReadOnlyDictionary column_flags = null) { // _bus = bus; _notification_service = notification_service; @@ -52,7 +59,11 @@ namespace mbts.ui { _rows = rows; _rows_view = rows_view; _columns = columns; + _create_predicate_vm = create_predicate_vm; + _create_column_predicate_vm = create_column_predicate_vm; + _create_settings_obj = create_settings_obj; _arg = arg; + _column_flags = column_flags.ToDictionary(kv => kv.Key, kv => ImmutableColumnSettingStore.create(kv.Value)); _grid_settings = backing(nameof(grid_settings), GridSettings.Default); _font_family = backing(nameof(font_family), () => new FontFamily(grid_settings.font_family), nameof(grid_settings)); _font_weight = backing(nameof(font_weight), () => grid_settings.font_weight, nameof(grid_settings)); @@ -72,10 +83,10 @@ namespace mbts.ui { // return pa; //} Window create_settings_view() { - var vm = new GridSettingsVM(owner_title, _bus, _orders_model, create_predicate_vm, create_column_predicate_vm); + var vm = new GridSettingsVM(owner_title, _bus, _orders_model, create_predicate_vm, create_column_predicate_vm, _create_settings_obj); // need to refresh before launching the settings-view bc users can chanve some grid-column-settings properties directly from the grid (e.g. width, is_collapsed, etc.) _grid_settings.set(grid_settings.with(columns.Select(c => c.create()))); - vm.load_from((grid_settings, _predicate_animations_data)); + vm.load(_create_settings_obj(grid_settings, _predicate_animations_data)); if (column_flags != null) { foreach (var col in vm.columns) { if (column_flags.TryGetValue(col.id, out GridColumnSettingsFeatureFlags f)) @@ -84,10 +95,10 @@ namespace mbts.ui { } var window = new GridSettingsDialog { DataContext = vm }; vm.committed += () => { - var (grid, predicate_animations_data) = vm.create(); + TSettingsData sd = vm.get_result(); // IMPORTANT: don't overwrite current sorting - grid = grid.with(sorting_criterion: rows_view.get_sorting_criteria(columns)); - load_from((grid, predicate_animations_data)); + sd = _create_settings_obj(sd.general.with(sorting_criterion: rows_view.get_sorting_criteria(columns)), sd.predicate_animations); + load_from(sd); }; vm.RequestClose += (s, e) => WindowUtilities.force_close(window); return window; @@ -107,20 +118,37 @@ namespace mbts.ui { public SolidColorBrush selection_highlight => _selection_highlight.Value; public IDialogCommand launch_settings_editor { get; } - public (GridSettings grid, IEnumerable> predicate_animations_data) create() { - // IMPORTANT: don't overwrite current sorting - _grid_settings.set(grid_settings.with(_columns.Select(c => c.create()), _rows_view.get_sorting_criteria(_columns))); - return (grid_settings, _predicate_animations_data); + public GridSettingsVM create_vm(GridSettings settings, IObservable title) { + var vm = new GridSettingsVM(title, _bus, _orders_model, _create_predicate_vm, _create_column_predicate_vm, _create_settings_obj, _column_flags.ToDictionary(kv => kv.Key, kv => kv.Value.flags)); + vm.load(_create_settings_obj(settings, _predicate_animations_data)); + return vm; } - public void load_from((GridSettings grid, IEnumerable> predicate_animations_data) value) { + public TSettingsData create() { + // IMPORTANT: don't overwrite current sorting + _grid_settings.set(grid_settings.with(_columns.Select(c => { + var col_settings = c.create(); + if (_column_flags.TryGetValue(c.id, out ImmutableColumnSettingStore store)) + col_settings = store.apply(col_settings); + return col_settings; + }), _rows_view.get_sorting_criteria(_columns))); + return _create_settings_obj(grid_settings, _predicate_animations_data); + } + + public void load_from(TSettingsData value) => _load_from_core(value, true); + + void _load_from_core(TSettingsData value, bool reset_immutables) { Disposable.dispose(_notification_subs); - var grid = value.grid ?? GridSettings.Default; + var grid = value.general ?? GridSettings.Default; _grid_settings.set(grid); foreach (ColumnBase col in _columns) col.set_fallback_font_params(new FontFamily(grid.font_family), grid.font_size, grid.font_color.rgb_color(), grid.font_weight); - _rows_view.set_sort_descriptions(grid.sorting_criterion, _columns); - _predicate_animations_data = value.predicate_animations_data; + if (reset_immutables) { +#pragma warning disable CS0612 // Type or member is obsolete + _rows_view.set_sort_descriptions(grid.sorting_criterion, _columns); +#pragma warning restore CS0612 // Type or member is obsolete + } + _predicate_animations_data = value.predicate_animations; _animation_defs.clear(); foreach (var pa in _predicate_animations_data) { var predicate = pa.predicate_builder.create_predicate(_arg); @@ -140,6 +168,12 @@ namespace mbts.ui { // ** in the unfortunate event that we botch backwards compatability, and the columns don't bear a 1:1 relationship with the column-settings // don't add insult to injury by crashing ** if (grid_settings.columns != null && _columns != null) { + if (reset_immutables) { + foreach (var kv in _column_flags) { + if (grid.columns.TryGetFirst(c => c.id == kv.Key, out GridColumnSettings cs)) + kv.Value.store(cs); + } + } var col_settings_id_map = grid_settings.columns.ToDictionary(cs => id_key(cs.id)); var columns_id_map = _columns.ToDictionary(c => id_key(c.id)); if (col_settings_id_map.Count == columns_id_map.Count && col_settings_id_map.Count == grid_settings.columns.Count) { @@ -159,5 +193,26 @@ namespace mbts.ui { IGridColumn lookup_column(string col_id) => _columns.OfType>().FirstOrDefault(c => string.Equals(col_id, c.id, StringComparison.CurrentCultureIgnoreCase)); } + + void IObserver.OnNext(TSettingsData sd) { + if (sd == null) return; + try { + var grid = sd.general; + if (grid.columns != null) { + var cols_with_applied_flags = grid.columns.ToList(); + for (int i = 0; i < cols_with_applied_flags.Count; i++) { + if (_column_flags.TryGetValue(cols_with_applied_flags[i].id, out ImmutableColumnSettingStore store)) { + cols_with_applied_flags[i] = store.apply(cols_with_applied_flags[i]); + } + } + sd = _create_settings_obj(grid.with(cols_with_applied_flags), sd.predicate_animations); + } + _load_from_core(sd, false); + } catch (Exception ex) { + Log.Ui.error($"Error loading GridSettings (as observer): {ex}"); + } + } + void IObserver.OnError(Exception error) { } + void IObserver.OnCompleted() { } } } \ No newline at end of file diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsVM.cs b/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsVM.cs index aa4264d68..1666e0fc0 100644 --- a/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsVM.cs +++ b/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsVM.cs @@ -1,5 +1,6 @@ using bts.api; using bts.shell; +using bts.utils; using mbts.collections; using mbts.collections.extensions; using mbts.ui; @@ -18,14 +19,38 @@ using CellAnimation = mbts.ui.CellAnimation; using FontFamily = System.Windows.Media.FontFamily; namespace mbts.spark { - interface IGridSettingsFactory : IValueFactory<(GridSettings grid, IEnumerable> predicate_animations_data)> - where TPredicateBuilder : IPredicateBuilder, new() { } + interface IGridSettingsWithNotifications where TPredicateBuilder : IPredicateBuilder { + GridSettings general { get; } + IEnumerable> predicate_animations { get; } + } - partial class GridSettingsVM : DisposableViewModelBase, IGridSettingsFactory, IRequestViewClose - where TPredicateBuilder : IPredicateBuilder, new() { + class GridSettingsWithNotifications : IEquatable> where TPredicateBuilder : IPredicateBuilder { + public readonly GridSettings grid; + public readonly IEnumerable> predicate_animations_data; + public GridSettingsWithNotifications(GridSettings grid, IEnumerable> predicate_animations_data) { + this.grid = grid; + this.predicate_animations_data = predicate_animations_data; + } + + public bool Equals(GridSettingsWithNotifications other) => + other != null && + EqualityComparer.null_check_equals(grid, other.grid) && + EqualityComparer.safe_sequence_equals(predicate_animations_data, other.predicate_animations_data); + + public override bool Equals(object obj) => Equals(obj as GridSettingsWithNotifications); + public override int GetHashCode() => + EqualityComparer.aggregate_hash( + EqualityComparer.safe_get_hash_code(grid), + EqualityComparer.safe_aggregate_hash(predicate_animations_data)); + } + + partial class GridSettingsVM : DisposableViewModelBase, IRequestViewClose, IWindowSettingsVM, ITemplateSelectorKey + where TPredicateBuilder : IPredicateBuilder, new() + where TSettingsData :class, IGridSettingsWithNotifications { // protected readonly Func> _create_predicate_vm; protected readonly Func> _create_column_predicate_vm; + readonly Func>, TSettingsData> _create_settings_obj; readonly Property _current_column; readonly Property _font_family; readonly Property _font_weight; @@ -33,16 +58,22 @@ namespace mbts.spark { readonly Property _font_size; readonly Property _show_predicate_animations, _show_cell_predicate_animations; readonly OrdersModel _orders_model; + readonly IReadOnlyDictionary _column_flags; public GridSettingsVM( IObservable owner_title, IBus bus, OrdersModel orders_model, - Func> create_predicate_vm, Func> create_column_predicate_vm) { + Func> create_predicate_vm, + Func> create_column_predicate_vm, + Func>, TSettingsData> create_settings_obj, + IReadOnlyDictionary column_flags = null) { // _owner_title = backing(nameof(owner_title), owner_title); this.bus = bus; _orders_model = orders_model; _create_predicate_vm = create_predicate_vm; _create_column_predicate_vm = create_column_predicate_vm; + _create_settings_obj = create_settings_obj; + _column_flags = column_flags ?? new Dictionary(); _font_family = backing(nameof(font_family), TextUtils.StandardFontFamily); _font_size = backing(nameof(font_size), FontSizes.Small); _font_weight = backing(nameof(font_weight), FontWeights.Normal); @@ -68,6 +99,7 @@ namespace mbts.spark { public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM(); public ColorPickerPopupVM selection_highlight { get; } = new ColorPickerPopupVM(); public double font_size { get => _font_size.Value; set => _font_size.set(value); } + public bool enable_sound_triggers => true; public bool show_predicate_animations { get => _show_predicate_animations.Value; set => _show_predicate_animations.set(value); } public bool show_cell_predicate_animations { get => _show_cell_predicate_animations.Value; set => _show_cell_predicate_animations.set(value); } public ObservableEditableCollection predicate_animations { get; } @@ -80,7 +112,9 @@ namespace mbts.spark { new DialogSkirtItem("Apply", new DelegateCommand(() => committed?.Invoke())) }; - public (GridSettings grid, IEnumerable> predicate_animations_data) create() { + public string key => ui.spark.GridOrLadderSettingsSelectorKeys.SimpleGrid; + + public TSettingsData get_result() { var animation_map = predicate_animations .Select(p => p.create()) .GroupBy(d => d.predicate) @@ -94,22 +128,22 @@ namespace mbts.spark { foreach (IGrouping g in animations) animation_map.AddOrUpdate(g.Key, p => animation_tuple(c: g), (p, t) => t.cell_animations.AddRange(g)); } - return (new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.to_proto(), col_settings), + return _create_settings_obj(new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.to_proto(), col_settings), animation_map.Select(kv => PredicateAnimations.create_data(kv.Key, kv.Value.cell_animations, kv.Value.row_animations, kv.Value.sounds))); // (List row_animations, List sounds, List cell_animations) animation_tuple(IEnumerable r = null, IEnumerable s = null, IGrouping c = null) => (r?.ToList() ?? new List(), s?.ToList() ?? new List(), c?.ToList() ?? new List()); } - public void load_from((GridSettings grid, IEnumerable> predicate_animations_data) value) { - _font_family.set(new FontFamily(value.grid.font_family)); - _font_weight.set(value.grid.font_weight); - font_color.load_from(value.grid.font_color.rgb_solid_brush()); - selection_highlight.load_from(value.grid.selection_highlight.to_brush()); - _font_size.set(value.grid.font_size); + public void load(TSettingsData sd) { + _font_family.set(new FontFamily(sd.general.font_family)); + _font_weight.set(sd.general.font_weight); + font_color.load_from(sd.general.font_color.rgb_solid_brush()); + selection_highlight.load_from(sd.general.selection_highlight.to_brush()); + _font_size.set(sd.general.font_size); var col_animations = new Dictionary>>(); predicate_animations.Clear(); - foreach (var pa in value.predicate_animations_data) { + foreach (var pa in sd.predicate_animations) { if (pa.row_animations.Count > 0 || pa.sound_notifications.Count > 0) { var vm = new PredicateRowAnimationVM(_create_predicate_vm()); vm.load_from((pa.predicate_builder, pa.row_animations, pa.sound_notifications)); @@ -122,9 +156,14 @@ namespace mbts.spark { } } var curr_col_id = current_column?.id; - columns.SetItems(value.grid.columns.Select(col => { + columns.SetItems(sd.general.columns.Select(col => { var vm = new ColumnVM(this); vm.load_from((col, col_animations.GetValueOrDefault(col.id)?.ToFlatLookup(kv => kv.Key, kv => kv.Value))); + // reapply our favorite column flags + // this comes in handy when performing Load From Factory - the editor will contain + // a consistent view of what can be overridden and what cannot + if (_column_flags.TryGetValue(vm.id, out GridColumnSettingsFeatureFlags flags)) + vm.apply_flags(flags); return vm; })); if (curr_col_id == null || !columns.TryGetFirst(c => c.id == curr_col_id, out IColumnVM curr_column)) diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsView.xaml b/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsView.xaml index a6adf52c3..7fc1656d4 100644 --- a/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsView.xaml +++ b/src/client/spark/view/Controls/DIYGrid/Settings/GridSettingsView.xaml @@ -342,9 +342,8 @@ - - Font - + Font + @@ -359,6 +358,9 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsDialog.xaml.cs b/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsDialog.xaml.cs new file mode 100644 index 000000000..5503a853b --- /dev/null +++ b/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsDialog.xaml.cs @@ -0,0 +1,19 @@ +using System.Windows; + +namespace mbts.ui.spark { + /// + /// Interaction logic for SimpleGridSettingsDialog.xaml + /// + public partial class SimpleGridSettingsDialog : Window { + public SimpleGridSettingsDialog() => + InitializeComponent(); + + public const string ResetToFactoryKey = "Reset", CopyFromOtherKey = "Copy"; + } + + static class GridOrLadderSettingsSelectorKeys { + public const string + SimpleGrid = nameof(SimpleGrid), + Ladder = nameof(Ladder); + } +} \ No newline at end of file diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsManager.cs b/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsManager.cs new file mode 100644 index 000000000..e1b1d22bc --- /dev/null +++ b/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsManager.cs @@ -0,0 +1,121 @@ +using mbts.spark; +using mbts.ui.settings; +using mbts.ui.shell; +using mbts.utils; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Linq; +using System.Windows; +using System.Windows.Media; +using static mbts.ui.controls.DIYGrid; + +namespace mbts.ui { + class SimpleGridSettingsManager : ViewModelBase, IObserver { + readonly ObservableCollection _columns; + readonly IReadOnlyCollection _row_vms; + readonly Property _grid_settings; + readonly DelegateProperty _font_family; + readonly DelegateProperty _font_weight; + readonly DelegateProperty _font_size; + readonly DelegateProperty _font_color, _selection_highlight; + readonly IReadOnlyDictionary _column_flags; + + public SimpleGridSettingsManager(ObservableCollection columns) + : this(columns, new Dictionary(), null) { } + + public SimpleGridSettingsManager(ObservableCollection columns, IReadOnlyDictionary column_flags, IReadOnlyCollection row_vms) { + _columns = columns; + _column_flags = column_flags.ToDictionary(kv => kv.Key, kv => ImmutableColumnSettingStore.create(kv.Value)); + _grid_settings = backing(nameof(grid_settings), GridSettings.Default); + _font_family = backing(nameof(font_family), () => new FontFamily(grid_settings.font_family), nameof(grid_settings)); + _font_weight = backing(nameof(font_weight), () => grid_settings.font_weight, nameof(grid_settings)); + _font_size = backing(nameof(font_size), () => grid_settings.font_size, nameof(font_size)); + _font_color = backing(nameof(font_color), () => grid_settings.font_color.rgb_solid_brush().frozen(), nameof(grid_settings)); + _selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight.to_brush().frozen(), nameof(grid_settings)); + _row_vms = row_vms; + } + + public GridSettings grid_settings => _grid_settings.Value; + public FontFamily font_family => _font_family.Value; + public FontWeight font_weight => _font_weight.Value; + public double font_size => _font_size.Value; + public SolidColorBrush font_color => _font_color.Value; + public SolidColorBrush selection_highlight => _selection_highlight.Value; + + public SimpleGridSettingsVM create_vm(GridSettings settings, IObservable title) { + var vm = new SimpleGridSettingsVM(title, _column_flags.ToDictionary(kv => kv.Key, kv => kv.Value.flags)); + vm.load_from(settings); + return vm; + } + + public GridSettings create() { + // IMPORTANT: don't overwrite current sorting + _grid_settings.set(grid_settings.with(_columns.Select(c => { + var col_settings = c.create(); + if (_column_flags.TryGetValue(c.id, out ImmutableColumnSettingStore store)) + col_settings = store.apply(col_settings); + return col_settings; + }))); + return grid_settings; + } + + /// + /// This will overwrite the columns' immutable-value store (unlike the IObserver<> 'OnNext' implementation) + /// + public void load_from(GridSettings grid) => _load_from_core(grid, true); + + void IObserver.OnCompleted() { } + void IObserver.OnError(Exception error) { } + void IObserver.OnNext(GridSettings grid) { + if (grid.columns != null) { + var cols_with_applied_flags = grid.columns.ToList(); + for (int i = 0; i < cols_with_applied_flags.Count; i++) { + if (_column_flags.TryGetValue(cols_with_applied_flags[i].id, out ImmutableColumnSettingStore store)) { + cols_with_applied_flags[i] = store.apply(cols_with_applied_flags[i]); + } + } + grid = grid.with(cols_with_applied_flags); + } + _load_from_core(grid, false); + } + + void _load_from_core(GridSettings grid, bool reset_immutable_flag_stores) { + grid = grid ?? GridSettings.Default; + _grid_settings.set(grid); + foreach (ColumnBase col in _columns) + col.set_fallback_font_params(new FontFamily(grid.font_family), grid.font_size, grid.font_color.rgb_color(), grid.font_weight); + // ** in the unfortunate event that we botch backwards compatability, and the columns don't bear a 1:1 relationship with the column-settings + // don't add insult to injury by crashing ** + if (grid_settings.columns != null) { + if (reset_immutable_flag_stores) { + foreach (var kv in _column_flags) { + if (grid.columns.TryGetFirst(c => c.id == kv.Key, out GridColumnSettings cs)) + kv.Value.store(cs); + } + } + if (_columns != null) { + var col_settings_id_map = grid_settings.columns.ToDictionary(cs => id_key(cs.id)); + var columns_id_map = _columns.ToDictionary(c => id_key(c.id)); + if (col_settings_id_map.Count == columns_id_map.Count && col_settings_id_map.Count == grid_settings.columns.Count) { + CollectionHelper.sync_with_never_duplicate(_columns, grid_settings.columns, cs => { + throw new InvalidOperationException(); + }, c => id_key(c.id), cs => id_key(cs.id)); + } + // ** don't assume that columns:column_settings have a perfect 1:1 relationship (they should, but it is possible we accidentally break the settings as grids/uis change) ** + foreach (var col in _columns) { + if (col_settings_id_map.TryGetValue(id_key(col.id), out GridColumnSettings cs)) + col.load_from(cs); + } + } + // invalidate all the rows to draw them again to reflect the latest settings + if (_row_vms != null) + foreach (var r in _row_vms) r.raise_invalidated(); + + // + string id_key(string id) => id.ToLower(); + } + } + } +} diff --git a/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsVM.cs b/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsVM.cs new file mode 100644 index 000000000..55c695217 --- /dev/null +++ b/src/client/spark/view/Controls/DIYGrid/Settings/SimpleGridSettingsVM.cs @@ -0,0 +1,102 @@ +using mbts.ui; +using mbts.ui.controls; +using mbts.ui.settings; +using mbts.ui.shell; +using mbts.ui.utility.text; +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Linq; +using System.Windows; +using FontFamily = System.Windows.Media.FontFamily; + +namespace mbts.spark { + interface ISimpleColumnVM : IValueFactory { + string id { get; } + string display_name { get; set; } + double width { get; set; } + bool is_visible { get; set; } + bool is_font_color_overridden { get; set; } + ColorPickerPopupVM font_color_override { get; } + bool is_font_weight_overridden { get; set; } + FontWeight font_weight_override { get; set; } + void apply_flags(GridColumnSettingsFeatureFlags flags); + } + + partial class SimpleGridSettingsVM : DisposableViewModelBase, IValueFactory, IWindowSettingsVM, ITemplateSelectorKey { + readonly Property _current_column; + readonly Property _font_family; + readonly Property _font_weight; + readonly Property _font_display_name, _owner_title; + readonly Property _font_size; + readonly Property _show_predicate_animations, _show_cell_predicate_animations; + readonly IReadOnlyDictionary _column_flags; + + public SimpleGridSettingsVM(IObservable owner_title, IReadOnlyDictionary column_flags = null) { + _column_flags = column_flags ?? new Dictionary(); + _owner_title = backing(nameof(owner_title), owner_title); + _font_family = backing(nameof(font_family), TextUtils.StandardFontFamily); + _font_size = backing(nameof(font_size), FontSizes.Small); + _font_weight = backing(nameof(font_weight), FontWeights.Normal); + _font_display_name = backing(nameof(font_display_name), () => TextUtils.display_name(font_family, font_size), nameof(font_family), nameof(font_size)); + _show_predicate_animations = backing(nameof(show_predicate_animations), true); + _show_cell_predicate_animations = backing(nameof(show_cell_predicate_animations), true); + _current_column = backing(nameof(current_column)); + } + + public event EventHandler RequestClose; + public event Action committed; + + public string owner_title => _owner_title.Value; + public ObservableCollection columns { get; } = new ObservableCollection(); + public ISimpleColumnVM current_column { get => _current_column.Value; set => _current_column.set(value); } + public string font_display_name => _font_display_name.Value; + public FontFamily font_family { get => _font_family.Value; set => _font_family.set(value); } + public FontWeight font_weight { get => _font_weight.Value; set => _font_weight.set(value); } + public ColorPickerPopupVM font_color { get; } = new ColorPickerPopupVM(); + public ColorPickerPopupVM selection_highlight { get; } = new ColorPickerPopupVM(); + public double font_size { get => _font_size.Value; set => _font_size.set(value); } + public bool enable_sound_triggers => false; + public bool show_predicate_animations { get => _show_predicate_animations.Value; set => _show_predicate_animations.set(value); } + public bool show_cell_predicate_animations { get => _show_cell_predicate_animations.Value; set => _show_cell_predicate_animations.set(value); } + public IReadOnlyList dialog_skirt_items => new[] { + new DialogSkirtItem("OK", new DelegateCommand(() => { + committed?.Invoke(); + RequestClose?.Invoke(this, new EventArgs()); + })) { is_default = true }, + new DialogSkirtItem("Cancel", new DelegateCommand(() => RequestClose?.Invoke(this, new EventArgs()))) { is_cancel = true }, + new DialogSkirtItem("Apply", new DelegateCommand(() => committed?.Invoke())) + }; + + public string key => ui.spark.GridOrLadderSettingsSelectorKeys.SimpleGrid; + + public GridSettings create() => + new GridSettings(font_family.name(), font_size, font_weight, font_color.color.rgb_color(), selection_highlight.to_proto(), columns.Select(c => c.create())); + + public void load_from(GridSettings value) { + _font_family.set(new FontFamily(value.font_family)); + _font_weight.set(value.font_weight); + font_color.load_from(value.font_color.rgb_solid_brush()); + selection_highlight.load_from(value.selection_highlight.to_brush()); + _font_size.set(value.font_size); + var curr_col_id = current_column?.id; + columns.SetItems(value.columns.Select(col => { + var vm = new SimpleColumnVM(this); + vm.load_from(col); + // reapply our favorite column flags + // this comes in handy when performing Load From Factory - the editor will contain + // a consistent view of what can be overridden and what cannot + if (_column_flags.TryGetValue(vm.id, out GridColumnSettingsFeatureFlags flags)) + vm.apply_flags(flags); + return vm; + })); + if (curr_col_id == null || !columns.TryGetFirst(c => c.id == curr_col_id, out ISimpleColumnVM curr_column)) + curr_column = columns.FirstOrDefault(); + _current_column.set(curr_column); + } + + void IWindowSettingsVM.load(GridSettings value) => load_from(value); + GridSettings IWindowSettingsVM.get_result() => create(); + } +} \ No newline at end of file diff --git a/src/client/spark/view/Controls/Order/OrderDetailsPopupControl.xaml b/src/client/spark/view/Controls/Order/OrderDetailsPopupControl.xaml index 8084859c9..0e5e41b23 100644 --- a/src/client/spark/view/Controls/Order/OrderDetailsPopupControl.xaml +++ b/src/client/spark/view/Controls/Order/OrderDetailsPopupControl.xaml @@ -47,7 +47,7 @@ - + diff --git a/src/client/spark/view/Controls/Order/OrderTypeItem.xaml b/src/client/spark/view/Controls/Order/OrderTypeItem.xaml index b0c58c496..f84ebc61b 100644 --- a/src/client/spark/view/Controls/Order/OrderTypeItem.xaml +++ b/src/client/spark/view/Controls/Order/OrderTypeItem.xaml @@ -33,7 +33,7 @@ Width="15" Height="10" /> - /// Interaction logic for OrderTypeItem.xaml /// - public partial class OrderTypeItem : Control - { - static OrderTypeItem() { + partial class OrderTypeItem : Control { + static OrderTypeItem() => DefaultStyleKeyProperty.OverrideMetadata(typeof(OrderTypeItem), new FrameworkPropertyMetadata(typeof(OrderTypeItem))); - } public static readonly DependencyProperty OrderTypeProperty = DependencyProperty.Register(nameof(OrderType), typeof(CustomOrderDefinition.CustomOneofCase), typeof(OrderTypeItem), new PropertyMetadata(CustomOrderDefinition.CustomOneofCase.None)); public CustomOrderDefinition.CustomOneofCase OrderType { get => (CustomOrderDefinition.CustomOneofCase)GetValue(OrderTypeProperty); set => SetValue(OrderTypeProperty, value); } + public static readonly DependencyProperty DisplayNameProperty = + WPFHelper.create_dp(nameof(DisplayName), (o, v) => { + o._display_name = v; + o._update_text(); + }); + + public string DisplayName { + get => GetValue(DisplayNameProperty) as string; + set => SetValue(DisplayNameProperty, value); + } + public static readonly DependencyProperty MetadataProperty = DependencyProperty.Register(nameof(Metadata), typeof(OrderMetadata), typeof(OrderTypeItem), new PropertyMetadata(new OrderMetadata { Color = 0, KeyBinding = string.Empty })); public OrderMetadata Metadata { get => GetValue(MetadataProperty) as OrderMetadata; set => SetValue(MetadataProperty, value); } + + string _display_name; + TextBlock _text; + + public override void OnApplyTemplate() { + this.try_get_template_child("text", out _text); + _update_text(); + } + + void _update_text() { + if (_text != null) { + if (string.IsNullOrEmpty(_display_name)) { + _text.ClearValue(TextBlock.TextProperty); + } else { + _text.SetCurrentValue(TextBlock.TextProperty, _display_name); + } + } + } } -} +} \ No newline at end of file diff --git a/src/client/spark/view/Converters/AnonymousValueConverter.cs b/src/client/spark/view/Converters/AnonymousValueConverter.cs index 5b7a396f5..10882f471 100644 --- a/src/client/spark/view/Converters/AnonymousValueConverter.cs +++ b/src/client/spark/view/Converters/AnonymousValueConverter.cs @@ -9,6 +9,9 @@ namespace mbts.ui.converters { public static IMultiValueConverter create(Func convert) => new AnonymousMultiValueConverter(convert); + + public static IMultiValueConverter create(Func convert) => + new AnonymousMultiValueConverter(convert); } public class AnonymousValueConverter : IValueConverter { @@ -50,4 +53,17 @@ namespace mbts.ui.converters { public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException(); } + + public class AnonymousMultiValueConverter : IMultiValueConverter { + readonly Func _convert; + + public AnonymousMultiValueConverter(Func convert) => _convert = convert; + + public TOut ErrorValue { get; set; } + + public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) => + ObjectHelper.try_convert_args(values, out TIn1 a, out TIn2 b, out TIn3 c) ? _convert(a, b, c) : ErrorValue; + + public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) => throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/client/spark/view/Converters/EnumStringConverters/SecurityTypeStringConverter.cs b/src/client/spark/view/Converters/EnumStringConverters/SecurityTypeStringConverter.cs deleted file mode 100644 index 4ba079d4f..000000000 --- a/src/client/spark/view/Converters/EnumStringConverters/SecurityTypeStringConverter.cs +++ /dev/null @@ -1,18 +0,0 @@ -using bts.api; -using mbts.ui.utility; -using System; -using System.Windows.Data; - -namespace mbts.ui.converters -{ - public class SecurityTypeStringConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { - return SecurityDisplay.format_security_type((SecurityType)value); - } - - public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { - throw new NotImplementedException(); - } - } -} diff --git a/src/client/spark/view/Converters/NumericSignBrushConverter.cs b/src/client/spark/view/Converters/NumericSignBrushConverter.cs index f3e434d75..da6e261b3 100644 --- a/src/client/spark/view/Converters/NumericSignBrushConverter.cs +++ b/src/client/spark/view/Converters/NumericSignBrushConverter.cs @@ -4,28 +4,48 @@ using System.Windows.Data; using System.Windows.Media; namespace mbts.ui.converters { - public class NumericSignBrushConverter : IValueConverter { - static readonly SolidColorBrush _error_brush = new SolidColorBrush(Colors.DimGray); + struct SignedDataStore { + public readonly T positive, neutral, negative, error; - SolidColorBrush _negative_brush = new SolidColorBrush(Colors.Red), - _neutral_brush = new SolidColorBrush(Colors.Black), - _positive_brush = new SolidColorBrush(Colors.Green); + public SignedDataStore(T positive, T neutral, T negative, T error) { + this.positive = positive; + this.neutral = neutral; + this.negative = negative; + this.error = error; + } + } - static NumericSignBrushConverter() => - _error_brush.Freeze(); + static class SignedDataStoreExtensions { + public static T get(this SignedDataStore store, double value) { + if (double.IsNaN(value)) return store.error; + else if (value > 0) return store.positive; + else if (value < 0) return store.negative; + else return store.neutral; + } - public Color PositiveColor { get => _positive_brush.Color; set => (_positive_brush = new SolidColorBrush(value) { Opacity = _opacity }).Freeze(); } - public Color NeutralColor { get => _neutral_brush.Color; set => (_neutral_brush = new SolidColorBrush(value) { Opacity = _opacity }).Freeze(); } - public Color NegativeColor { get => _negative_brush.Color; set => (_negative_brush = new SolidColorBrush(value) { Opacity = _opacity }).Freeze(); } + public static SignedDataStore with_positive(this SignedDataStore store, T positive) => new SignedDataStore(positive, store.neutral, store.negative, store.error); + public static SignedDataStore with_neutral(this SignedDataStore store, T neutral) => new SignedDataStore(store.positive, neutral, store.negative, store.error); + public static SignedDataStore with_negative(this SignedDataStore store, T negative) => new SignedDataStore(store.positive, store.neutral, negative, store.error); + } - private double _opacity = 1.0; - public Double Opacity { - get { return _opacity; } + class NumericSignBrushConverter : IValueConverter { + SignedDataStore _brushes = new SignedDataStore( + Brushes.Green, Brushes.Black, Brushes.Red, Brushes.DimGray); + + public Color PositiveColor { get => _brushes.positive.Color; set => _brushes = _brushes.with_positive(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); } + public Color NeutralColor { get => _brushes.neutral.Color; set => _brushes = _brushes.with_neutral(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); } + public Color NegativeColor { get => _brushes.negative.Color; set => _brushes = _brushes.with_negative(new SolidColorBrush(value) { Opacity = _opacity }.frozen()); } + + double _opacity = 1.0; + public double Opacity { + get => _opacity; set { _opacity = value; - (_positive_brush = new SolidColorBrush(_positive_brush.Color) { Opacity = _opacity }).Freeze(); - (_negative_brush = new SolidColorBrush(_negative_brush.Color) { Opacity = _opacity }).Freeze(); - (_neutral_brush = new SolidColorBrush(_neutral_brush.Color) { Opacity = _opacity }).Freeze(); + _brushes = new SignedDataStore( + _brushes.positive.with(value).frozen(), + _brushes.neutral.with(value).frozen(), + _brushes.negative.with(value).frozen(), + _brushes.error.with(value).frozen()); } } public IComparable Zero { get; set; } = 0.0; @@ -33,9 +53,9 @@ namespace mbts.ui.converters { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var comparable = value as IComparable; if (comparable == null || comparable is double && double.IsNaN((double)value)) - return _error_brush; + return _brushes.error; int comp = comparable.CompareTo(Zero); - return comp == 0 ? _neutral_brush : comp > 0 ? _positive_brush : _negative_brush; + return comp == 0 ? _brushes.neutral : comp > 0 ? _brushes.positive : _brushes.negative; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) => diff --git a/src/client/spark/view/Converters/SecurityConverters/SecurityTypeForegroundConverter.cs b/src/client/spark/view/Converters/SecurityConverters/SecurityTypeForegroundConverter.cs deleted file mode 100644 index b800fe108..000000000 --- a/src/client/spark/view/Converters/SecurityConverters/SecurityTypeForegroundConverter.cs +++ /dev/null @@ -1,32 +0,0 @@ -using bts.api; -using System; -using System.Windows; -using System.Windows.Data; -using System.Windows.Media; - -namespace mbts.ui.converters -{ - [ValueConversion(typeof(SecurityType), typeof(Brush))] - public class SecurityTypeForegroundConverter : IValueConverter - { - public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { - if (value is SecurityType) return _convert((SecurityType)value); - //else if (value is SecurityTuple) return _convert(((SecurityTuple)value).security_type); - //else if (value is SecurityExpiration) return _convert(((SecurityExpiration)value).security_type); - else return SystemColors.ControlTextBrush; - } - - public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) { - throw new NotImplementedException(); - } - - private Brush _convert(SecurityType type) { - switch (type) { - case SecurityType.Option: return (Brush)BTSColors.header_text; - case SecurityType.Future: return new SolidColorBrush(Colors.Gold); - case SecurityType.Spread: return new SolidColorBrush(Colors.SlateBlue); - default: return new SolidColorBrush(Colors.PaleGreen); - } - } - } -} diff --git a/src/client/spark/view/Extensions/Command/FileDialogCommand.cs b/src/client/spark/view/Extensions/Command/FileDialogCommand.cs index 21e67e1aa..6e52b47cd 100644 --- a/src/client/spark/view/Extensions/Command/FileDialogCommand.cs +++ b/src/client/spark/view/Extensions/Command/FileDialogCommand.cs @@ -51,6 +51,8 @@ namespace mbts.ui { try { using (var stream = File.Open(file, FileMode.Open, FileAccess.Read)) on_import(ProtoBufSerializationHelper.deserialize(stream)); + } catch (ProtoBuf.ProtoException e) when (e.Message.StartsWith("Invalid wire-type;")) { + BTSMessageBox.Show("This doesn't appear to be the correct settings file for this window.\r\n\r\nPlease try importing a different settings file.", error_msg_title ?? "Import", BTSMessageBoxButtons.OK, System.Windows.MessageBoxImage.Hand, true, false, true); } catch (Exception ex) { BTSMessageBox.Show($"Error importing {type_name ?? "item"} at '{file}': {ex}.", error_msg_title ?? "Import", BTSMessageBoxButtons.OK, System.Windows.MessageBoxImage.Error, true, false, true); } diff --git a/src/client/spark/view/Extensions/WPFExtensions.cs b/src/client/spark/view/Extensions/WPFExtensions.cs index 5aa34a548..127405dc1 100644 --- a/src/client/spark/view/Extensions/WPFExtensions.cs +++ b/src/client/spark/view/Extensions/WPFExtensions.cs @@ -98,6 +98,11 @@ namespace mbts.ui { return brush; } + public static SolidColorBrush with(this SolidColorBrush brush, double opacity) { + brush.Opacity = opacity; + return brush; + } + public static Pen frozen(this Pen pen) { pen.Freeze(); return pen; @@ -296,11 +301,6 @@ namespace mbts.ui { return new Size(ft.Width, ft.Height); } - // TODO: mark this method Obsolete ([John]I decided against doing it right now because this is part of a large change that still needs to be merged, - // and adding all the supress warnings to the calling code may add complications). - public static List GetSortingCriteria(this ICollectionView collection_view) => - collection_view.SortDescriptions.Select(sd => new SortingCriteria(sd)).ToList(); - public static void SetSortingCriteria(this ICollectionView collection_view, IEnumerable sorting_criteria) { collection_view.SortDescriptions.Clear(); if (sorting_criteria != null) diff --git a/src/client/spark/view/Resources/Converters.xaml b/src/client/spark/view/Resources/Converters.xaml index a0e130254..59b3e9707 100644 --- a/src/client/spark/view/Resources/Converters.xaml +++ b/src/client/spark/view/Resources/Converters.xaml @@ -26,8 +26,6 @@ - - diff --git a/src/client/spark/view/Spark/Core/TickUtils.cs b/src/client/spark/view/Spark/Core/TickUtils.cs index fad161d3e..903039173 100644 --- a/src/client/spark/view/Spark/Core/TickUtils.cs +++ b/src/client/spark/view/Spark/Core/TickUtils.cs @@ -1,9 +1,7 @@ using bts.api; using bts.utils; using mbts.co2; -using mbts.spark.utilities; using mbts.user.settings; -using mbts.utils; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -62,16 +60,16 @@ namespace mbts.spark { return high; } - public static (int n_low, int n_high) tick_diff(ContractSpecificationImmutable spec, double x, double y) => - get_regional_ticks(spec).tickdiff(x, y); - - public static int normalized_tick_diff(ContractSpecificationImmutable spec, double x, double y) { - var (n_low, n_high) = tick_diff(spec, x, y); - return n_low + n_high; + public static int? tickdiff(this ContractSpecificationImmutable spec, double x, double y) { + if (get_regional_ticks(spec).try_get_tickdiff(x, y, out int n_low, out int n_high)) + return n_low + n_high; + return null; } - public static int normalized_tick_diff(SecurityImmutable security, double x, double y) => - normalized_tick_diff(security.spec_or_default(), x, y); + public static int tickdiff_unchecked(this ContractSpecificationImmutable spec, double x, double y) { + (int n_low, int n_high) = get_regional_ticks(spec).tickdiff_unchecked(x, y); + return n_low + n_high; + } public static double mid_tick(ContractSpecificationImmutable spec, double bid, double ask) { double raw_mid; diff --git a/src/client/spark/view/Spark/Ladder/LadderGridHost.cs b/src/client/spark/view/Spark/Ladder/LadderGridHost.cs index f2248f4e1..aceefbcf8 100644 --- a/src/client/spark/view/Spark/Ladder/LadderGridHost.cs +++ b/src/client/spark/view/Spark/Ladder/LadderGridHost.cs @@ -1,9 +1,11 @@ using bts; using bts.api; +using bts.api.md.auxiliary; using bts.utils; using mbts.co2; using mbts.ui; using mbts.ui.controls; +using mbts.ui.spark; using mbts.ui.utility.text; using mbts.utils; using System; @@ -38,9 +40,13 @@ namespace mbts.spark { _bid_vis_tup = new VisualGlyphTuple(), _ask_vis_tup = new VisualGlyphTuple(), _price_vis_tup = new VisualGlyphTuple(), - _last_vis_tup = new VisualGlyphTuple(); + _last_vis_tup = new VisualGlyphTuple(), + _volume_vis_tup = new VisualGlyphTuple(); - IDisposable _settings_sub, _tick_sub; + readonly RefValueContainer> _volume_container = + new RefValueContainer>(); + + IDisposable _settings_sub, _volume_sub, _tick_sub; ScrollViewer _scrollViewer; LadderSettings _settings; LadderVM _vm; @@ -53,6 +59,7 @@ namespace mbts.spark { double _last_origin_price; MarketBreakLines _mkt_break_lines; NativePriceSize _last_trade; + Func _fmt_size = MDDisplayHelper.get_formatter_nps(MarketSizeDisplay.get_default()); public LadderGridHost() { AllowDrop = true; @@ -79,7 +86,13 @@ namespace mbts.spark { }; } - enum ColumnType { Order, Bid, Price, Ask, Last, None } + public enum ColumnType { Order, Bid, Price, Ask, Last, Volume, None } + + public event Action visible_columns_changed; + + public IEnumerable visible_columns => + columns.Where(c => c.settings.show) + .Select(c => c.column_type); public SecurityImmutable security => _vm != null ? _vm.security : SecurityDataHelper.Defaults.SecurityImmutable; public ICommand RecenterCommand { get; } @@ -149,6 +162,29 @@ namespace mbts.spark { lgh._adjust_column_width(lgh.columns.First(c => c.column_type == ColumnType.Ask), width_adjustment); } + public static readonly DependencyProperty VolumeColumnWidthProperty = + WPFHelper.create_dp( + nameof(VolumeColumnWidth), + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + (o, oldval, newval) => o._adjust_column_width(o.columns.First(c => c.column_type == ColumnType.Volume), (double)newval - (double)oldval), + 50u); + + public uint VolumeColumnWidth { + get => (uint)GetValue(VolumeColumnWidthProperty); + set => SetValue(VolumeColumnWidthProperty, value); + } + + public static readonly DependencyProperty LastColumnWidthProperty = + WPFHelper.create_dp( + nameof(LastColumnWidth), + FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, + (o, oldval, newval) => o._adjust_column_width(o.columns.First(c => c.column_type == ColumnType.Last), (double)newval - (double)oldval), + 50u); + + public uint LastColumnWidth { + get => (uint)GetValue(LastColumnWidthProperty); + set => SetValue(LastColumnWidthProperty, value); + } #region Command public ICommand Command { @@ -297,6 +333,7 @@ namespace mbts.spark { case ColumnType.Bid: new_col_width = (int)bid_column_width; break; case ColumnType.Price: new_col_width = (int)price_column_width; break; case ColumnType.Ask: new_col_width = (int)ask_column_width; break; + case ColumnType.Volume: new_col_width = (int)VolumeColumnWidth; break; default: new_col_width = (int)Math.Max((double)column.settings.width + adjustment, _column_min_width); break; } int true_adj = new_col_width - (int)column.settings.width; @@ -321,6 +358,10 @@ namespace mbts.spark { security.ContractSpec); break; case ColumnType.Ask: render_market_col(_asks); break; + case ColumnType.Volume: + _volume_container.mark_dirty(); + _refresh_volume_at_price(); + break; } if (Array.IndexOf(columns, column) <= Array.IndexOf(columns, _price_column)) { if (_mkt_break_lines.low_render_index.HasValue) @@ -330,7 +371,7 @@ namespace mbts.spark { } void render_market_col(NativePriceSize[] depth) { var mid = LadderRender.index_of(_price_list, _last_origin_price, _price_comparer); - LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, security.ContractSpec, column.settings); + LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, security.ContractSpec, column.settings, _fmt_size); } } _apply_column_visual_offset(); @@ -340,30 +381,46 @@ namespace mbts.spark { Column _price_column; void _set_columns() { double x = 0; - columns = new[] { + var col_list = new List { new Column(_settings.orders_column, ColumnType.Order, null, x), new Column(_settings.bid_column, ColumnType.Bid, _bid_vis_tup, (x += _settings.orders_column.show ? _settings.orders_column.width : 0)), - _price_column = new Column(_settings.price_column, ColumnType.Price, _price_vis_tup, (x += _settings.bid_column.show ? _settings.bid_column.width : 0)), - new Column(_settings.ask_column, ColumnType.Ask, _ask_vis_tup, (x += _settings.price_column.show ? _settings.price_column.width : 0)), - new Column(_settings.last_column, ColumnType.Last, _last_vis_tup, (x += _settings.ask_column.show ? _settings.ask_column.width : 0)) + (_price_column = new Column(_settings.price_column, ColumnType.Price, _price_vis_tup, (x += _settings.bid_column.show ? _settings.bid_column.width : 0))), + new Column(_settings.ask_column, ColumnType.Ask, _ask_vis_tup, (x += _settings.price_column.show ? _settings.price_column.width : 0)) }; + if (security.ContractCase != Security.ContractOneofCase.Synthetic) { + col_list.Add(new Column(_settings.volume_column.general, ColumnType.Volume, _volume_vis_tup, (x += _settings.ask_column.show ? _settings.ask_column.width : 0))); + col_list.Add(new Column(_settings.last_column, ColumnType.Last, _last_vis_tup, (x += _settings.volume_column.general.show ? _settings.volume_column.general.width : 0))); + } + columns = col_list.ToArray(); } #endregion void _connect_vm() { - Disposable.dispose(ref _settings_sub); - Disposable.dispose(ref _tick_sub); + _dispose_resources(); _vm = DataContext as LadderVM; if (_vm != null) { + _vm.OnDisposed += _on_vm_disposed; _prices_updated = 0; _price_comparer = Comparer.Create((x, y) => security.ContractSpec.tick_equals(x, y) ? 0 : y.CompareTo(x)); _price_dirty = true; _vm.on_tick += _tick; _settings_sub = _vm.settings.OnDispatcher().Subscribe(_settings_updated); + _volume_sub = _vm.model.md_auxiliary_model.security_volume_at_price(security.Id).Subscribe(t => _volume_container.write_value(t.state.Values)); _tick_sub = Disposable.Create(() => _vm.on_tick -= _tick); } } + void _on_vm_disposed(bool obj) => + _dispose_resources(); + + void _dispose_resources() { + Disposable.dispose(ref _settings_sub); + Disposable.dispose(ref _volume_sub); + Disposable.dispose(ref _tick_sub); + if (_vm != null) + _vm.OnDisposed -= _on_vm_disposed; + } + void _scroll_to_center() { if (_scrollViewer != null && _settings != null) { int index = _price_list != null && !double.IsNaN(_last_origin_price) ? @@ -391,7 +448,7 @@ namespace mbts.spark { if (_settings != null) { ulong unused = 0; bool last_trade_changed = _vm.md.get_last_trade(ref unused, out _last_trade, out DateTime _, out ulong __); - bool depth_changed = _vm.md.get_depth(ref _prices_updated, ref _bids, ref _asks); + bool depth_changed = _vm.md.get_depth(ref _prices_updated, ref _bids, ref _asks, NativePriceHelpers.COCONUT_MAX_DEPTH); _mkt_break_lines.refresh(); bool prices_changed = _price_dirty || @@ -409,7 +466,6 @@ namespace mbts.spark { _refresh_session_high_line(); _refresh_session_range(); _refresh_last_trade(); - _refresh_depth(); _refresh_sizes(); } else { if (_mkt_break_lines.bid_dirty) @@ -429,13 +485,10 @@ namespace mbts.spark { if (last_index > 0) LadderRender.render_last(_last_vis_tup.visual, _last_vis_tup.glyph_context, _last_trade, last_index, _settings.row_height, _settings.last_column); } - if (depth_changed) { - Log.assert_d(_price_list != null); - int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer); - LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column); - LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column); - } + if (depth_changed) + _refresh_sizes(); } + _refresh_volume_at_price(); _last_origin_price = _mkt_break_lines.origin; _price_dirty = false; } @@ -474,15 +527,21 @@ namespace mbts.spark { void _refresh_sizes() { Log.assert_d(_price_list != null); int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer); - LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column); - LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column); + LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column, _fmt_size); + LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column, _fmt_size); } - void _refresh_depth() { - Log.assert_d(_price_list != null); - int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer); - LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, security.ContractSpec, _settings.bid_column); - LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, security.ContractSpec, _settings.ask_column); + /// + /// Conditionally refreshes volume at price (if value has changed since last refresh). + /// + void _refresh_volume_at_price() { + if (!_settings.volume_column.general.show) + return; + var vapls = _volume_container.read_new_value(); + if (vapls != null) { + Log.assert_d(_price_list != null); + LadderRender.render_volumes(_volume_vis_tup.visual, _volume_vis_tup.glyph_context, _price_list, vapls, _settings.row_height, security.ContractSpec, _settings.volume_column); + } } static readonly TimeSpan _price_out_of_range_render_interval = TimeSpan.FromSeconds(5); @@ -518,8 +577,8 @@ namespace mbts.spark { SparkUtils.lazy_warn(!double.IsNaN(_mkt_break_lines.origin) && _mkt_break_lines.origin >= low && _mkt_break_lines.origin <= high, () => fmt_log_msg($"market break ({_mkt_break_lines.origin}) is unacceptable for the market range ({low} - {high})")); int prev_row_count = _row_count; - int n_ticks_to_low = TickUtils.normalized_tick_diff(security.ContractSpec, _mkt_break_lines.origin, low); - int n_ticks_to_high = TickUtils.normalized_tick_diff(security.ContractSpec, high, _mkt_break_lines.origin); + int n_ticks_to_low = security.ContractSpec.tickdiff(_mkt_break_lines.origin, low) ?? 0; + int n_ticks_to_high = security.ContractSpec.tickdiff(high, _mkt_break_lines.origin) ?? 0; int desired_row_count = Math.Max(n_ticks_to_low, n_ticks_to_high) * 2 + 1; _row_count = Math.Min(_max_row_count, desired_row_count); if (_row_count != prev_row_count) { @@ -595,18 +654,40 @@ namespace mbts.spark { #endregion void _settings_updated(LadderSettings s) { - bool init = _settings == null; - double prev_row_height = init ? double.NaN : _settings.row_height; + bool init; + double prev_row_height; + if (_settings == null) { + init = true; + prev_row_height = double.NaN; + } else { + init = false; + prev_row_height = _settings.row_height; + // This is a general issue with default-settings: + // "Not every property in the settings object should be shared across windows." + // Column widths are one such property (they are changed from the actual Ladder window, not the settings dialog). + // Below is a hack which prevents overwriting current column widths; until we come up with a better solution this will suffice. + // TODO: fix this by moving such properties from the settings proto into a separate proto (or come up with a better workaround/solution) + copy_col_width(v => v.orders_column); + copy_col_width(v => v.bid_column); + copy_col_width(v => v.price_column); + copy_col_width(v => v.ask_column); + copy_col_width(v => v.volume_column.general); + copy_col_width(v => v.last_column); + void copy_col_width(Func select_col) => + select_col(s).width = select_col(_settings).width; + } // _settings = s; _bid_vis_tup.glyph_context = glyph_context(s.bid_column); _ask_vis_tup.glyph_context = glyph_context(s.ask_column); _price_vis_tup.glyph_context = glyph_context(s.price_column); _last_vis_tup.glyph_context = glyph_context(s.last_column); + _volume_vis_tup.glyph_context = glyph_context(s.volume_column.general); GlyphContext glyph_context(LadderColumnSettings cs) => GlyphContext.create(cs.font_family, cs.font_weight); + _fmt_size = MDDisplayHelper.get_formatter_nps(_settings.size_display); _set_columns(); _apply_column_visual_offset(); if (prev_row_height != _settings.row_height) @@ -619,15 +700,22 @@ namespace mbts.spark { // Orders column orders_column_width = s.orders_column.width; _ordershost.update_settings(_price_list, _price_comparer, s.row_height, s.orders_column); - foreach (var col in columns) - if (col.settings.show && col.visual_tup != null) { - _visualHost.AddVisualChild(col.visual_tup.visual); - switch (col.column_type) { - case ColumnType.Bid: bid_column_width = col.settings.width; break; - case ColumnType.Price: price_column_width = col.settings.width; break; - case ColumnType.Ask: ask_column_width = col.settings.width; break; + foreach (var col in columns) { + if (col.visual_tup != null) { + if (col.settings.show) { + _visualHost.AddVisualChild(col.visual_tup.visual); + switch (col.column_type) { + case ColumnType.Bid: bid_column_width = col.settings.width; break; + case ColumnType.Price: price_column_width = col.settings.width; break; + case ColumnType.Ask: ask_column_width = col.settings.width; break; + case ColumnType.Volume: + VolumeColumnWidth = col.settings.width; + _volume_container.mark_dirty(); + break; + } } } + } _visualHost.AddVisualChild(_mid_mkt_break_vis); _visualHost.AddVisualChild(_ask_mkt_break_vis); _visualHost.AddVisualChild(_bid_mkt_break_vis); @@ -643,6 +731,7 @@ namespace mbts.spark { if (init) { _scroll_to_center(); } + visible_columns_changed?.Invoke(); } void _init_tactile_adorner() { @@ -652,7 +741,7 @@ namespace mbts.spark { } void _render_grid_background() { - LadderRender.render_grid(_gridvis, columns, _row_count, _settings.row_height, _settings.grid_line_color); + LadderRender.render_grid(_gridvis, columns.Where(c => c.settings.show), _row_count, _settings.row_height, _settings.grid_line_color); } class VisualGlyphTuple { @@ -759,7 +848,7 @@ namespace mbts.spark { } int _get_session_range_size() => - TickUtils.normalized_tick_diff(_owner.security.spec_or_default(), _session_high, _session_low); + _owner.security.spec_or_default().tickdiff(_session_high, _session_low) ?? 0; LadderSettings _settings => _owner._settings; diff --git a/src/client/spark/view/Spark/Ladder/LadderModel.cs b/src/client/spark/view/Spark/Ladder/LadderModel.cs index 78af88464..7576b8374 100644 --- a/src/client/spark/view/Spark/Ladder/LadderModel.cs +++ b/src/client/spark/view/Spark/Ladder/LadderModel.cs @@ -6,10 +6,12 @@ using System.Threading.Tasks; namespace mbts.spark { class LadderModel { readonly OrdersModel _order_gateway; + readonly MarketDataAuxiliaryModel _md_auxiliary_model; - public LadderModel(SecurityMD source, OrdersModel ordergw) { + public LadderModel(SecurityMD source, OrdersModel ordergw, MarketDataAuxiliaryModel md_auxiliary_model) { security_md = source; _order_gateway = ordergw; + _md_auxiliary_model = md_auxiliary_model; } public enum CancelType { All, Bids, Asks } @@ -18,6 +20,7 @@ namespace mbts.spark { public SecurityImmutable security => security_md.security; public MDConsumerHandle md => security_md.md; public OrdersModel order_gateway => _order_gateway; + public MarketDataAuxiliaryModel md_auxiliary_model => _md_auxiliary_model; public Task cancel_orders(CancelType ct) { switch (ct) { diff --git a/src/client/spark/view/Spark/Ladder/LadderSettings.cs b/src/client/spark/view/Spark/Ladder/LadderSettings.cs index e71eca84d..39a528650 100644 --- a/src/client/spark/view/Spark/Ladder/LadderSettings.cs +++ b/src/client/spark/view/Spark/Ladder/LadderSettings.cs @@ -5,6 +5,9 @@ using ProtoBuf; using System; using System.Linq; using System.Windows; +using bts.proto; +using mbts.ui.spark; +using System.Collections.Generic; namespace mbts.spark { // IMPORTANT: BE SURE TO ADD ANY NEW PROPERTIES TO THE EQUALS FUNCTION! @@ -74,6 +77,74 @@ namespace mbts.spark { public static bool operator !=(LadderColumnSettings x, LadderColumnSettings y) => !(x == y); } + [ProtoContract] + class HistogramBarAnimation : IEquatable { + [ProtoMember(1)] + public ColorShowProto bars { get; set; } + [ProtoMember(2)] + public bool show_numbers { get; set; } + + public bool active => bars.show || show_numbers; + + public bool Equals(HistogramBarAnimation other) => + other != null && + bars == other.bars && + 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()); + } + + [ProtoContract] + class VolumeColumnSettings : IEquatable { + public static VolumeColumnSettings Default { + get { + var gen = LadderColumnSettings.Default; + gen.show = false; + gen.background = BTSColors.LadderSpecial.default_column_background; + return new VolumeColumnSettings { + general = gen, + buy = new HistogramBarAnimation { bars = new ColorShowProto((uint)BTSColors.MarketData.bid, false) }, + sell = new HistogramBarAnimation { bars = new ColorShowProto((uint)BTSColors.MarketData.ask, false) }, + passive = new HistogramBarAnimation { bars = new ColorShowProto((uint)BTSColors.LadderSpecial.passive_volume_bar, false) }, + total = new HistogramBarAnimation { bars = new ColorShowProto((uint)BTSColors.LadderSpecial.total_volume_bar), show_numbers = true } + }; + } + } + + [ProtoMember(1)] + public LadderColumnSettings general { get; set; } + [ProtoMember(2)] + public HistogramBarAnimation buy { get; set; } + [ProtoMember(3)] + public HistogramBarAnimation sell { get; set; } + [ProtoMember(4)] + public HistogramBarAnimation passive { get; set; } + [ProtoMember(5)] + public HistogramBarAnimation total{ get; set; } + + public bool Equals(VolumeColumnSettings other) => + other != null && + general.Equals(other.general) && + buy.safe_equals(other.buy) && + sell.safe_equals(other.sell) && + passive.safe_equals(other.passive) && + total.safe_equals(other.total); + + 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()); + } + [ProtoContract] class LadderMarketBreakLine : IEquatable { [ProtoMember(1)] @@ -116,12 +187,12 @@ namespace mbts.spark { ask_column = LadderColumnSettings.Default, price_column = LadderColumnSettings.Default, last_column = LadderColumnSettings.Default; - orders_column.background = 0xBABABA; + orders_column.background = BTSColors.LadderSpecial.default_column_background; bid_column.foreground = 0xFFFFFF; bid_column.background = BTSColors.MarketData.bid; ask_column.foreground = 0xFFFFFF; ask_column.background = BTSColors.MarketData.ask; - price_column.background = 0xBABABA; + price_column.background = BTSColors.LadderSpecial.default_column_background; last_column.background = 0xFFFFFF; return new LadderSettings { row_height = 20, @@ -131,6 +202,7 @@ namespace mbts.spark { ask_column = ask_column, price_column = price_column, last_column = last_column, + volume_column = VolumeColumnSettings.Default, calculator_buttons = new[] { 1, 5, 10, 25, 50, 100 }, scroll_bar_visible = false, default_order_type = CustomOrderDefinition.CustomOneofCase.Limit, @@ -143,7 +215,8 @@ namespace mbts.spark { session_low_line = new LadderMarketBreakLine { color = BTSColors.MarketData.session_low, show = true }, session_high_line = new LadderMarketBreakLine { color = BTSColors.MarketData.session_high, show = true }, session_range_fill = new LadderMarketBreakLine { color = BTSColors.MarketData.session_range_fill, show = true }, - alternate_order_size = 0 + alternate_order_size = 0, + size_display = MarketSizeDisplay.get_default() }; } } @@ -200,6 +273,10 @@ namespace mbts.spark { public LadderMarketBreakLine session_high_line { get; set; } [ProtoMember(24)] public LadderMarketBreakLine session_range_fill { get; set; } + [ProtoMember(25)] + public VolumeColumnSettings volume_column { get; set; } + [ProtoMember(26)] + public MarketSizeDisplay size_display { get; set; } public bool Equals(LadderSettings other) => other != null && @@ -223,7 +300,9 @@ namespace mbts.spark { 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); + session_range_fill.safe_equals(other.session_range_fill) && + volume_column.safe_equals(other.volume_column) && + size_display.safe_equals(other.size_display); public override bool Equals(object obj) => Equals(obj as LadderSettings); @@ -249,6 +328,22 @@ namespace mbts.spark { 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(session_range_fill), + EqualityComparer.safe_get_hash_code(volume_column), + EqualityComparer.safe_get_hash_code(size_display)); + } + + static class LadderSettingsExtensions { + public static IEnumerable all_columns(this LadderSettings settings) { + yield return settings.orders_column; + yield return settings.bid_column; + yield return settings.price_column; + yield return settings.ask_column; + yield return settings.volume_column.general; + yield return settings.last_column; + } + + public static IEnumerable visible_columns(this LadderSettings settings) => + settings.all_columns().Where(c => c.show); } } \ No newline at end of file diff --git a/src/client/spark/view/Spark/Ladder/LadderSettingsVM.cs b/src/client/spark/view/Spark/Ladder/LadderSettingsVM.cs index 982e843e1..4047a6ca4 100644 --- a/src/client/spark/view/Spark/Ladder/LadderSettingsVM.cs +++ b/src/client/spark/view/Spark/Ladder/LadderSettingsVM.cs @@ -3,6 +3,7 @@ using mbts.collections; using mbts.ui; using mbts.ui.controls.color_picker; using mbts.ui.shell; +using mbts.ui.spark; using System; using System.Collections.Generic; using System.Collections.ObjectModel; @@ -12,7 +13,7 @@ using System.Windows.Media; using TextUtils = mbts.ui.utility.text.TextUtils; namespace mbts.spark { - class LadderSettingsVM : ViewModelBase, IWindowSettingsVM { + class LadderSettingsVM : ViewModelBase, IWindowSettingsVM, ITemplateSelectorKey { readonly Property _badge_style; readonly Property _alternate_order_size; readonly DelegateProperty _row_height; @@ -22,7 +23,8 @@ namespace mbts.spark { _bid = new LadderColumnSettingsVM("Bid"), _price = new LadderColumnSettingsVM("Price"), _ask = new LadderColumnSettingsVM("Ask"), - _last = new LadderColumnSettingsVM("Last"); + _last = new LadderColumnSettingsVM("Last", true); + VolumeColumnVM _volume = new VolumeColumnVM(); LadderMarketBreakLineVM _bid_mkt_line = new LadderMarketBreakLineVM("Bid"), _ask_mkt_line = new LadderMarketBreakLineVM("Ask"), @@ -31,7 +33,8 @@ namespace mbts.spark { _session_high_mkt_line = new LadderMarketBreakLineVM("Session High", 2, 1, false), _session_range_fill = new LadderMarketBreakLineVM("Session Range", 0, 0, false); - public LadderSettingsVM() { + public LadderSettingsVM(bool is_synthetic) { + this.is_synthetic = is_synthetic; size_presets = new ObservableEditableCollection>(_create_size_preset_vm); size_presets.CollectionChanged += (s, e) => RaisePropertyChanged(nameof(size_presets_text)); _badge_style = backing(nameof(badge_style)); @@ -65,7 +68,10 @@ namespace mbts.spark { set => _alternate_order_size.Value = value; } + public MarketSizeDisplayVM size_display { get; } = new MarketSizeDisplayVM(); + public double row_height => _row_height.Value; + public bool is_synthetic { get; } public IEnumerable column_items { get { @@ -73,7 +79,10 @@ namespace mbts.spark { yield return _bid; yield return _price; yield return _ask; - yield return _last; + if (!is_synthetic) { + yield return _volume.general; + yield return _last; + } } } @@ -81,7 +90,9 @@ namespace mbts.spark { public LadderColumnSettingsVM bid_column => _bid; public LadderColumnSettingsVM price_column => _price; public LadderColumnSettingsVM ask_column => _ask; + public LadderColumnSettingsVM volume_column => _volume.general; public LadderColumnSettingsVM last_column => _last; + public VolumeColumnVM volume => _volume; public LadderMarketBreakLineVM session_range_fill => _session_range_fill; public ObservableEditableCollection> size_presets { get; } public string size_presets_text => string.Join(", ", size_presets); @@ -97,15 +108,19 @@ namespace mbts.spark { } } + public string key => GridOrLadderSettingsSelectorKeys.Ladder; + public void load(LadderSettings settings) { _current = settings; badge_style = settings.badge_style; alternate_order_size = settings.alternate_order_size; + size_display.load_from(settings.size_display); size_presets.SetItems(settings.calculator_buttons.empty_if_null().Select(s => _create_size_preset_vm((uint)s))); _orders.assign_from(_current.orders_column); _bid.assign_from(_current.bid_column); _ask.assign_from(_current.ask_column); _price.assign_from(_current.price_column); + _volume.assign_from(_current.volume_column); _last.assign_from(_current.last_column); _bid_mkt_line.assign_from(_current.bid_break_line); _ask_mkt_line.assign_from(_current.ask_break_line); @@ -119,11 +134,13 @@ namespace mbts.spark { var clone = SparkUtils.clone_proto(_current); clone.badge_style = badge_style; clone.alternate_order_size = alternate_order_size; + clone.size_display = size_display.create(); clone.calculator_buttons = size_presets.Select(vm => (int)vm.value).ToArray(); clone.orders_column = _orders.get_settings(); clone.bid_column = _bid.get_settings(); clone.price_column = _price.get_settings(); clone.ask_column = _ask.get_settings(); + clone.volume_column = _volume.get_settings(); clone.last_column = _last.get_settings(); clone.bid_break_line = _bid_mkt_line.get_settings(); clone.ask_break_line = _ask_mkt_line.get_settings(); @@ -142,18 +159,22 @@ namespace mbts.spark { return vm; } - public class LadderColumnSettingsVM : ViewModelBase { + public class LadderColumnSettingsVM : ViewModelBase, ITemplateSelectorKey { readonly Property _font_size; readonly Property _foreground, _background; readonly Property _typeface; readonly DelegateProperty _font_family; readonly DelegateProperty _font_weight; readonly DelegateProperty _font_display_name; + readonly Property _show; + readonly bool _can_toggle_visibility; - public LadderColumnSettingsVM(string name) { + public LadderColumnSettingsVM(string name, bool can_toggle_visibility = false) { this.name = name; + _can_toggle_visibility = can_toggle_visibility; foreground_palettes = new ObservableCollection(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p))); background_palettes = new ObservableCollection(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p))); + _show = backing(nameof(show), true); _typeface = backing(nameof(typeface), TextUtils.StandardTypeface); _font_family = backing(nameof(font_family), () => typeface.FontFamily, nameof(typeface)); _font_weight = backing(nameof(font_weight), () => typeface.Weight, nameof(typeface)); @@ -172,7 +193,7 @@ namespace mbts.spark { public string font_display_name => _font_display_name.Value; public double font_size { get => _font_size.Value; set => _font_size.Value = value; } public uint width { get; set; } - public bool show { get; set; } + public bool show { get => _show.Value; set => _show.Value = value; } public Color foreground { get => _foreground.Value; set => _foreground.Value = value; } public Color background { get => _background.Value; set => _background.Value = value; } public ObservableCollection foreground_palettes { get; } @@ -180,6 +201,7 @@ namespace mbts.spark { public event Action font_size_changed; public virtual string sample_text => "999-56789.01234"; + string ITemplateSelectorKey.key => _can_toggle_visibility ? ui.spark.LadderSettingsView.ColumnVisibilityToggleTemplateKey : null; public void assign_from(LadderColumnSettings settings) { typeface = new Typeface( @@ -253,5 +275,119 @@ namespace mbts.spark { show = show }; } + + public class VolumeColumnVM : ViewModelBase { + readonly DelegateProperty _any_visible; + public VolumeColumnVM() { + buy = new VolumeAtPriceAnimationVM("Buy", new VolumeAtPriceSampleData(this, 112, 62, 174)); + sell = new VolumeAtPriceAnimationVM("Sell", new VolumeAtPriceSampleData(this, 93, 78, 155)); + passive = new VolumeAtPriceAnimationVM("Passive", new VolumeAtPriceSampleData(this, 72, 127, 198)); + total = new VolumeAtPriceAnimationVM("Total", new VolumeAtPriceSampleData(this, 81, 100, 144)); + _any_visible = backing(nameof(any_visible), () => volume_components.Any(vc => vc.show_numbers || vc.bars.enabled)); + foreach (var vc in volume_components) { + vc.show_numbers_changed += _any_visible.refresh; + vc.bars.enabled_changed += _any_visible.refresh; + foreach (var _vc in volume_components) { + vc.show_numbers_changed += _vc.sample_data.refresh_numbers_text; + } + } + } + + public LadderColumnSettingsVM general { get; } = new LadderColumnSettingsVM("Volume", true); + public VolumeAtPriceAnimationVM buy { get; } + public VolumeAtPriceAnimationVM sell { get; } + public VolumeAtPriceAnimationVM passive { get; } + public VolumeAtPriceAnimationVM total { get; } + public bool any_visible => volume_components.Any(vc => vc.show_numbers || vc.bars.enabled); + + public IEnumerable volume_components { + get { + yield return buy; + yield return sell; + yield return passive; + yield return total; + } + } + public double max_total_volume => volume_components.Max(v => v.sample_data.total); + + public void assign_from(VolumeColumnSettings settings) { + general.assign_from(settings.general); + buy.assign_from(settings.buy); + sell.assign_from(settings.sell); + passive.assign_from(settings.passive); + total.assign_from(settings.total); + } + + public VolumeColumnSettings get_settings() => + new VolumeColumnSettings { + general = general.get_settings(), + buy = buy.get_settings(), + sell = sell.get_settings(), + passive = passive.get_settings(), + total = total.get_settings(), + }; + } + + internal class VolumeAtPriceSampleData : ViewModelBase { + readonly VolumeColumnVM _owner; + + public VolumeAtPriceSampleData(VolumeColumnVM owner, uint b, uint s, uint p) { + _owner = owner; + buy = b; + sell = s; + passive = p; + } + + public uint buy { get; } + public uint sell { get; } + public uint passive { get; } + public uint total => buy + sell + passive; + public string numbers_text => string.Join("/", _visible_number); + + IEnumerable _visible_number { + get { + if (_owner.buy.show_numbers) + yield return buy; + if (_owner.sell.show_numbers) + yield return sell; + if (_owner.passive.show_numbers) + yield return passive; + if (_owner.total.show_numbers) + yield return total; + } + } + + public void refresh_numbers_text() => RaisePropertyChanged(nameof(numbers_text)); + } + + public class VolumeAtPriceAnimationVM : ViewModelBase { + readonly Property _show_numbers; + + internal VolumeAtPriceAnimationVM(string name, VolumeAtPriceSampleData sample_data) { + this.name = name; + this.sample_data = sample_data; + _show_numbers = backing(nameof(show_numbers), false); + _show_numbers.Changed += (oldval, newval) => show_numbers_changed?.Invoke(); + } + + public event Action show_numbers_changed; + public string name { get; } + public EnabledValueVM bars { get; } = new EnabledValueVM(); + public ObservableCollection palettes { get; } = new ObservableCollection(ColorPalette.all_presets().Select(p => new ColorPaletteVM(p))); + public bool show_numbers { get => _show_numbers.Value; set => _show_numbers.set(value); } + public VolumeAtPriceSampleData sample_data { get; } + + public void assign_from(HistogramBarAnimation settings) { + bars.enabled = settings.bars.show; + bars.value = settings.bars.color; + show_numbers = settings.show_numbers; + } + + public HistogramBarAnimation get_settings() => + new HistogramBarAnimation { + bars = new bts.proto.ColorShowProto(bars.value, bars.enabled), + show_numbers = show_numbers + }; + } } } \ No newline at end of file diff --git a/src/client/spark/view/Spark/Ladder/LadderSettingsView.xaml b/src/client/spark/view/Spark/Ladder/LadderSettingsView.xaml index fa2b377ba..eeceec542 100644 --- a/src/client/spark/view/Spark/Ladder/LadderSettingsView.xaml +++ b/src/client/spark/view/Spark/Ladder/LadderSettingsView.xaml @@ -15,12 +15,15 @@ 4 8 + 24 8 24 4 6 8 24 + + @@ -30,6 +33,24 @@ + + @@ -67,6 +88,25 @@ + + + + + + + + + + + + + + + + + + + @@ -93,261 +133,465 @@ Alternate Order Qty - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/client/spark/view/Spark/Ladder/LadderSettingsView.xaml.cs b/src/client/spark/view/Spark/Ladder/LadderSettingsView.xaml.cs index c95184598..44108667c 100644 --- a/src/client/spark/view/Spark/Ladder/LadderSettingsView.xaml.cs +++ b/src/client/spark/view/Spark/Ladder/LadderSettingsView.xaml.cs @@ -1,4 +1,5 @@ using mbts.ui.converters; +using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Media; @@ -8,12 +9,26 @@ namespace mbts.ui.spark { /// Interaction logic for LadderSettingsView.xaml /// partial class LadderSettingsView : UserControl { - public const string OrdersColumnSampleTemplateKey = "OrdersColumnSampleTemplate"; + public const string + OrdersColumnSampleTemplateKey = nameof(OrdersColumnSampleTemplateKey), + ColumnVisibilityToggleTemplateKey = nameof(ColumnVisibilityToggleTemplateKey); public LadderSettingsView() => InitializeComponent(); public static IMultiValueConverter MktBreakLineBrushConverter = ValueConverter.create((color, show) => show ? new SolidColorBrush(color) : Brushes.Transparent); + + public static IMultiValueConverter VolumeBarWidthConverter => + ValueConverter.create((value, max, container_width) => container_width * (double)value / (double)max); + + public static readonly DependencyProperty VolumeComponentValueProperty = + DependencyProperty.RegisterAttached("VolumeComponentValue", + typeof(uint), + typeof(LadderSettingsView), + new PropertyMetadata(0u)); + + public static uint GetVolumeComponentValue(DependencyObject d) => (uint)d.GetValue(VolumeComponentValueProperty); + public static void SetVolumeComponentValue(DependencyObject d, uint value) => d.SetValue(VolumeComponentValueProperty, value); } } \ No newline at end of file diff --git a/src/client/spark/view/Spark/Ladder/LadderVM.cs b/src/client/spark/view/Spark/Ladder/LadderVM.cs index 3bc639b4a..905b5c072 100644 --- a/src/client/spark/view/Spark/Ladder/LadderVM.cs +++ b/src/client/spark/view/Spark/Ladder/LadderVM.cs @@ -19,7 +19,7 @@ using System.Windows.Input; using System.Windows.Media; namespace mbts.spark { - class LadderVM : DisposableViewModelBase, IKeyListener { + class LadderVM : DisposableViewModelBase, IWillLetYouKnowWhenIDispose, IKeyListener { readonly IBus _bus; readonly LadderModel _model; readonly Property _high, _low, _change, _open, _settlement, _position; @@ -36,7 +36,8 @@ namespace mbts.spark { _model = model; _equality = TickUtils.tick_equality_comparer(_model.security.ContractSpec); _slot_price_vs_orders = new Dictionary(_equality); - _settings_manager = settings_manager; + var non_shared_wsm = settings_manager.as_non_shared(); + _settings_manager = non_shared_wsm; order_properties_vm = new OrderPropertiesEditorVM(_model.order_gateway, Observable.Return(_model.security_md), _settings_manager.subscribe_current().Select(s => new InlineOrderProperties(_model.order_gateway, s))); _high = backing(nameof(high), double.NaN); _low = backing(nameof(low), double.NaN); @@ -64,8 +65,14 @@ namespace mbts.spark { show_book_command = new DelegateCommand(_show_book); show_ticket_command = new DelegateCommand(_show_ticket); settings_command = new DialogCommand(() => { - var vm = new WindowSettingsDialogVM(settings_manager, s => s.ladder, UserSettings.FileExtensions.LADDER); - var dialog = new ui.spark.LadderSettingsDialog { + var vm = new WindowSettingsDialogVM( + settings_manager, s => s.ladder, UserSettings.FileExtensions.LADDER, + s => { + var svm = new LadderSettingsVM(security.ContractCase == Security.ContractOneofCase.Synthetic); + svm.load(s); + return svm; + }); + var dialog = new ui.spark.SimpleGridSettingsDialog { DataContext = vm, Title = $"Ladder Settings - '{security}'" }; @@ -77,8 +84,9 @@ namespace mbts.spark { _dispose = Disposable.Create( settings_command, + non_shared_wsm, order_properties_vm, - order_properties_vm.observe_account(true).Select(a => a.name).Subscribe(account_positions), + order_properties_vm.observe_account(true).Select(a => a?.name ?? string.Empty).Subscribe(account_positions), account_positions, size_calculator = new CalculatorLogic(0, _settings_manager.subscribe_current().Select(s => s.calculator_buttons)), bus.subscribe(SparkTags.LogActivity).OnDispatcher().Subscribe(status_update), @@ -167,7 +175,8 @@ namespace mbts.spark { orders.sync_with(_slot_price_vs_orders.Values); }), - WorkLoop.Start($"Ladder '{security}'", _tick) + WorkLoop.Start($"Ladder '{security}'", _tick), + Disposable.Create(() => _on_disposed_impl?.Invoke(true)) ); } @@ -198,6 +207,11 @@ namespace mbts.spark { } } + event Action _on_disposed_impl; + event Action IWillLetYouKnowWhenIDispose.OnDisposed { + add => _on_disposed_impl += value; + remove => _on_disposed_impl -= value; + } public SecurityImmutable security => _model.security; public double high => _high.Value; public double low => _low.Value; diff --git a/src/client/spark/view/Spark/Ladder/LadderView.xaml b/src/client/spark/view/Spark/Ladder/LadderView.xaml index 53164d14e..68c43b28f 100644 --- a/src/client/spark/view/Spark/Ladder/LadderView.xaml +++ b/src/client/spark/view/Spark/Ladder/LadderView.xaml @@ -422,33 +422,7 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - + _column_headers_presenter.Columns.SetItems( + _ladder.visible_columns.Select(t => ColumnFactory.create(_ladder, t))); this.vm = vm; DataContext = vm; DataContextChanged += datacontext_changed; @@ -28,5 +33,49 @@ namespace mbts.spark { e.Handled = true; e.UseDefaultCursors = true; } + + static class ColumnFactory { + public static GridViewColumn create(LadderGridHost host, LadderGridHost.ColumnType type) { + string header = null, width_prop = null; + switch (type) { + case LadderGridHost.ColumnType.Order: + header = "Orders"; + width_prop = nameof(LadderGridHost.orders_column_width); + break; + case LadderGridHost.ColumnType.Bid: + header = "Bid"; + width_prop = nameof(LadderGridHost.bid_column_width); + break; + case LadderGridHost.ColumnType.Price: + header = "Price"; + width_prop = nameof(LadderGridHost.price_column_width); + break; + case LadderGridHost.ColumnType.Ask: + header = "Ask"; + width_prop = nameof(LadderGridHost.ask_column_width); + break; + case LadderGridHost.ColumnType.Volume: + header = "Volume"; + width_prop = nameof(LadderGridHost.VolumeColumnWidth); + break; + case LadderGridHost.ColumnType.Last: + header = "Last"; + width_prop = nameof(LadderGridHost.LastColumnWidth); + break; + default: throw ExceptionBuilder.Argument.unexpected_case(type, nameof(type)); + } + return _create(host, header, width_prop); + } + + static GridViewColumn _create(LadderGridHost host, string header, string width_prop) { + var gvc = new GridViewColumn { Header = header }; + BindingOperations.SetBinding(gvc, GridViewColumn.WidthProperty, new Binding(width_prop) { + Source = host, + UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged, + Mode = BindingMode.TwoWay + }); + return gvc; + } + } } } \ No newline at end of file diff --git a/src/client/spark/view/Spark/Ladder/LaderRender.cs b/src/client/spark/view/Spark/Ladder/LaderRender.cs index 337c3fffd..4a8ddf334 100644 --- a/src/client/spark/view/Spark/Ladder/LaderRender.cs +++ b/src/client/spark/view/Spark/Ladder/LaderRender.cs @@ -1,8 +1,10 @@ using bts; using bts.api; +using bts.api.md.auxiliary; using mbts.co2; using mbts.ui; using mbts.ui.controls; +using mbts.ui.spark; using mbts.ui.utility.text; using System; using System.Collections.Generic; @@ -42,13 +44,8 @@ namespace mbts.spark { // IMPORTANT: this method ONLY WORKS if Badges use the BadgeRender.margin/border_thickness/padding values. // TODO: elminate the dependency on BadgeRender.margin/border_thickness/padding by integrating them into settings. public static double measure_row_height(LadderSettings settings) => - Math.Max(new[] { - settings.bid_column.font_size, - settings.price_column.font_size, - settings.ask_column.font_size, - settings.last_column.font_size - }.Max() + row_padding.Top + row_padding.Bottom, - BadgeRender.measure_height( + Math.Max(settings.visible_columns().Max(c => c.font_size, 0) + row_padding.Top + row_padding.Bottom, + BadgeRender.measure_height( settings.badge_style, Badge.DefaultBorderThickness, Badge.DefaultPadding, settings.orders_column.font_size ) + Badge.DefaultMargin.Top + Badge.DefaultMargin.Bottom); @@ -128,7 +125,8 @@ namespace mbts.spark { NativePriceSize[] rungs, double row_height, ContractSpecificationImmutable spec, - LadderColumnSettings settings + LadderColumnSettings settings, + Func fmt ) { //var background = new SolidColorBrush(settings.background.rgb_color()); using (var dc = dv.RenderOpen()) @@ -139,22 +137,114 @@ namespace mbts.spark { var foreground = settings.foreground.rgb_brush(); int curr_index = origin_index; double curr_price = prices[curr_index]; - foreach (var nps in rungs) { - if (nps.is_nan) break; - double price = nps.price; - uint size = nps.size; + for (int i = 0; i < rungs.Length; i++) { + if (rungs[i].is_nan) break; + double price = rungs[i].price; // prices are in descending order, so get diff from curr_price - price - int tick_diff = TickUtils.normalized_tick_diff(spec, curr_price, price); + int tick_diff = spec.tickdiff_unchecked(curr_price, price); curr_index += tick_diff; curr_price = price; if (curr_index >= 0 && curr_index < prices.Length) { - var gr = format_text(size.ToString(), curr_index, row_height, settings, glyph_context); + var gr = format_text(fmt(rungs[i]), curr_index, row_height, settings, glyph_context); dc.DrawGlyphRun(foreground, gr); } else break; } } } + public static void render_volumes( + DrawingVisual dv, + GlyphContext glyph_context, + double[] prices, + IEnumerable data, + double row_height, + ContractSpecificationImmutable spec, + VolumeColumnSettings settings + ) { + if (!data.Any()) { + _clear_visual(dv); + return; + } + /// create a list of 'render components' (buy/sell/passive). This does not include 'total volume,' which is treated a little differently + /// ( for details) + var components = new List<(Func get_volue, Brush bars, bool show_numbers)>(); + if (settings.buy.active) + components.Add((v => v.BuyVolume, settings.buy.bars.result?.to_brush(), settings.buy.show_numbers)); + if (settings.sell.active) + components.Add((v => v.SellVolume, settings.sell.bars.result?.to_brush(), settings.sell.show_numbers)); + if (settings.passive.active) + components.Add((v => v.PassiveVolume, settings.passive.bars.result?.to_brush(), settings.passive.show_numbers)); + /// + Brush total_brush = null; + if (settings.total.bars.result.HasValue) { + total_brush = new SolidColorBrush(settings.total.bars.result.Value); + } else if (components.Count == 0) { + _clear_visual(dv); + return; + } + double max = data.Max(v => v.Volume); + Brush text_brush = settings.general.foreground.rgb_brush(); + _render_cells(dv, row_height, data.Select(d => (d.Price, d)), settings.general, prices, spec, render_cell); + /// local methods + void render_cell(DrawingContext dc, int r, VolumeAtPriceLevelImmutable vap) { + double y = row_height * r; + // render the total first; each component (buy/sell/passive) will be rendered on top of the total + if (total_brush != null) { + render_bar(dc, 0, y, vap.Volume, total_brush, out double _); + } + List numbers_text = new List(); + double x_bar = 0; + foreach ((var get_volume, Brush bars, bool show_numbers) in components) { + uint v = get_volume(vap); + render_bar(dc, x_bar, y, v, bars, out double width); + x_bar += width; + if (show_numbers) + numbers_text.Add(v.ToString()); + } + if (settings.total.show_numbers) { + numbers_text.Add(vap.Volume.ToString()); + } + if (numbers_text.Count > 0) { + string str = string.Join("/", numbers_text); + GlyphRun gr = glyph_context.get_glyph_run(str, settings.general.font_size, settings.general.width, row_height, HorizontalAlignment.Center, 0, VerticalAlignment.Center, y, true); + dc.DrawGlyphRun(text_brush, gr); + } + } + + void render_bar(DrawingContext dc, double x, double y, double v, Brush brush, out double width) { + if (v == 0) { + width = 0; + return; + } + double pct = v / max; + width = pct * settings.general.width; + dc.DrawRectangle(brush, null, new Rect(x, y, width, row_height - 1)); + } + } + + public delegate void RenderCell(DrawingContext dc, int row, T data_item); + + static void _render_cells(DrawingVisual dv, double row_height, IEnumerable<(double price, T value)> data, LadderColumnSettings settings, double[] prices, ContractSpecificationImmutable spec, RenderCell render_cell) { + using (var dc = dv.RenderOpen()) + using (_draw_with_clip(dc, settings, row_height, prices.Length)) { + if (data.is_null_or_empty()) + return; + int curr_index = 0; + double curr_price = prices[curr_index]; + foreach (var (price, value) in data) { + if (double.IsNaN(price)) + continue; + // prices are in descending order, so get diff from curr_price - price + int tick_diff = spec.tickdiff(curr_price, price) ?? 0; + curr_index += tick_diff; + curr_price = price; + if (curr_index >= 0 && curr_index < prices.Length) { + render_cell(dc, curr_index, value); + } else break; + } + } + } + internal static void render_last(DrawingVisual dv, GlyphContext glyph_context, NativePriceSize last, int index, double row_height, LadderColumnSettings settings) { using (var dc = dv.RenderOpen()) { var gr = format_text(last.size.ToString(), index, row_height, settings, glyph_context); @@ -182,5 +272,9 @@ namespace mbts.spark { dc.PushClip(new RectangleGeometry(new Rect(0, 0, settings.width, row_height * row_count))); return Disposable.Create(dc.Pop); } + + static void _clear_visual(DrawingVisual dv) { + using (dv.RenderOpen()) { } + } } } \ No newline at end of file diff --git a/src/client/spark/view/Spark/PAndL/PAndLVM.cs b/src/client/spark/view/Spark/PAndL/PAndLVM.cs index 6a3305db3..a5ca17a3a 100644 --- a/src/client/spark/view/Spark/PAndL/PAndLVM.cs +++ b/src/client/spark/view/Spark/PAndL/PAndLVM.cs @@ -11,14 +11,19 @@ using System.Linq; using System.Reactive.Linq; using System.Windows.Data; using static bts.shell.PAndLWindowFactory; -using static bts.api.orderman.Fill.Types; +using static mbts.ui.controls.DIYGrid; +using static mbts.spark.PAndLView; +using System.Windows.Media; +using System.Windows; +using mbts.persist; +using mbts.ui.settings; namespace mbts.spark { class PAndLVM : DisposableViewModelBase { - - public PAndLVM(IBus bus, PAndLModel model, PAndLStartupData startup_data) { + public PAndLVM(IBus bus, PAndLModel model, PAndLStartupData startup_data, IWindowSettingsManager settings_manager) { _bus = bus; _model = model; + _settings_manager = settings_manager; _selected_pnl_calc_type = backing(nameof(selected_pnl_calc_type)); // get initial value from settings // else default to midpoint @@ -60,22 +65,51 @@ namespace mbts.spark { pnl_price_filter.load_state(fs.pnl_price); security_type_filter.load_state(fs.security_types); } - // restore sort state or supply default value - if (startup_data?.sort_criteria?.Any() == true) { - pnl_view.SetSortingCriteria(startup_data.sort_criteria); - } else { - pnl_view.SetSortingCriteria(new List() { - new SortingCriteria(nameof(PnLSummaryRow.account), true) - , new SortingCriteria(nameof(PnLSummaryRow.exchange_string), true) - , new SortingCriteria(nameof(PnLSummaryRow.symbol), true) - , new SortingCriteria(nameof(PnLSummaryRow.security_text), true) - }); - } pnl_view.CollectionChanged += Pnl_view_CollectionChanged; + add_col(Columns.Account, DefaultColumnHeaders.Account, r => r.account, nameof(PnLSummaryRow.account), DefaultColumnWidths.Account, account_filter, DataGridFilterKnownTemplates.Regex); + add_col(Columns.Exchange, DefaultColumnHeaders.Exchange, r => r.exchange_string, nameof(PnLSummaryRow.exchange), DefaultColumnWidths.Exchange, exchange_string_filter, DataGridFilterKnownTemplates.Regex); + add_col(Columns.Symbol, DefaultColumnHeaders.Symbol, r => r.symbol, nameof(PnLSummaryRow.symbol), DefaultColumnWidths.Symbol, symbol_filter, DataGridFilterKnownTemplates.Regex); + add_col(Columns.Security, DefaultColumnHeaders.Security, r => r.security_text, nameof(PnLSummaryRow.security), DefaultColumnWidths.Security, security_filter, DataGridFilterKnownTemplates.Regex); + add_col(Columns.SecurityType, DefaultColumnHeaders.SecurityType, r => r.security_type.ToString(), nameof(PnLSummaryRow.security_type), DefaultColumnWidths.SecurityType, security_type_filter, DataGridFilterKnownTemplates.DiscreteValues); + add_col(Columns.StartNetPos, DefaultColumnHeaders.StartNetPos, r => r.start_net_position.ToString(), nameof(PnLSummaryRow.start_net_position), DefaultColumnWidths.Start_Net_Pos, start_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, HorizontalAlignment.Right); + add_col(Columns.StartPNL, DefaultColumnHeaders.StartPnL, r => r.start_pnl_text, nameof(PnLSummaryRow.start_pnl), DefaultColumnWidths.PnL_Generic, start_pnl_filter, DataGridFilterKnownTemplates.DoubleMinMax, HorizontalAlignment.Right, r => r.start_pnl_background); + add_col(Columns.DayLong, DefaultColumnHeaders.DayLong, r => r.day_long_quantity.ToString(), nameof(PnLSummaryRow.day_long_quantity), DefaultColumnWidths.PnL_Generic, day_long_quantity_filter, DataGridFilterKnownTemplates.UIntMinMax, HorizontalAlignment.Right); + add_col(Columns.DayShort, DefaultColumnHeaders.DayShort, r => r.day_short_quantity.ToString(), nameof(PnLSummaryRow.day_short_quantity), DefaultColumnWidths.PnL_Generic, day_short_quantity_filter, DataGridFilterKnownTemplates.UIntMinMax, HorizontalAlignment.Right); + add_col(Columns.DayPNL, DefaultColumnHeaders.DayPnL, r => r.day_pnl_text, nameof(PnLSummaryRow.day_pnl), 90, day_pnl_filter, DataGridFilterKnownTemplates.DoubleMinMax, HorizontalAlignment.Right, r => r.day_pnl_background); + add_col(Columns.TotalNetPos, DefaultColumnHeaders.TotalNetPos, r => r.total_net_position.ToString(), nameof(PnLSummaryRow.total_net_position), DefaultColumnWidths.Total_Net_Pos, total_net_position_filter, DataGridFilterKnownTemplates.DoubleMinMax, HorizontalAlignment.Right); + add_col(Columns.TotalPNL, DefaultColumnHeaders.TotalPnL, r => r.total_pnl_text, nameof(PnLSummaryRow.total_pnl), DefaultColumnWidths.PnL_Generic, total_pnl_filter, DataGridFilterKnownTemplates.DoubleMinMax, HorizontalAlignment.Right, r => r.total_pnl_background); + add_col(Columns.PNLPrice, DefaultColumnHeaders.PnLPrice, r => r.pnl_price_text, nameof(PnLSummaryRow.pnl_price), DefaultColumnWidths.PnL_Generic, pnl_price_filter, DataGridFilterKnownTemplates.DoubleMinMax, HorizontalAlignment.Right); + void add_col(Columns key, string header, Func get_text, string sort_prop, double width, IDataGridColumnFilter filter, string filter_template, HorizontalAlignment h_align = HorizontalAlignment.Left, Func get_background = null) { + var col = get_background == null ? + new RenderTextColumn(key, get_text, filter, filter_template) : + new PnLDelegateBackgroundColumn(key, get_text, get_background, filter, filter_template); + col.Header = header; + col.Width = width; + col.sort_member_path = sort_prop; + col.horizontal_alignment = h_align; + col.id = col_id(key); + columns.Add(col); + } + + // restore sort state or supply default value + pnl_view.set_sort_descriptions(startup_data.sorting_criterion, columns); + + var col_settings_flags = new Dictionary { + { Columns.StartPNL.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }, + { Columns.DayPNL.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }, + { Columns.TotalPNL.ToString(), GridColumnSettingsFeatureFlags.ImmutableFontColor }, + }; + + grid_settings_manager = new SimpleGridSettingsManager(columns, col_settings_flags, _pnl_data); + // 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()); + + // setup the main Disposable and // hook up grid filters _dispose = Disposable.Create(pnl, + settings_manager.subscribe_current().OnDispatcher().Subscribe(grid_settings_manager), DataGridFilterHelper.hook_filters(pnl_view, account_filter, exchange_string_filter, @@ -96,6 +130,15 @@ namespace mbts.spark { ); } + public ObservableCollection columns { get; } = new ObservableCollection(); + public SimpleGridSettingsManager grid_settings_manager { get; } + public GridSettings get_grid_save_state() { + if (_settings_manager.instance_settings == null) + return null; + else + return grid_settings_manager.create(); + } + private void Pnl_view_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { _update_totals(); } @@ -221,12 +264,13 @@ namespace mbts.spark { #endregion - public List get_sorting_states() => pnl_view.GetSortingCriteria(); + public List get_sorting_states() => pnl_view.get_sorting_criteria(columns); #endregion readonly IBus _bus; readonly PAndLModel _model; + IWindowSettingsManager _settings_manager; static readonly TimeSpan _UPDATE_PERIOD = TimeSpan.FromSeconds(1); static readonly TimeSpan _POSITION_UPDATE_PERIOD = TimeSpan.FromMilliseconds(333); readonly Property _selected_pnl_calc_type; @@ -235,6 +279,14 @@ namespace mbts.spark { DateTime _last_pnl_update_time = DateTime.MinValue; ObservableCollection _pnl_data = new ObservableCollection(); - } + class PnLDelegateBackgroundColumn : RenderTextColumn { + readonly Func _get_brush; -} + public PnLDelegateBackgroundColumn(Columns key, Func get_text, Func get_brush, IDataGridColumnFilter filter, string filter_template) + : base(key, get_text, filter, filter_template) => _get_brush = get_brush; + + public override void draw(PnLSummaryRow row, DrawingContext dc, ColumnGeometry position, double row_height) => + CellRender.draw_text(dc, glyph_context, font_size, _get_text(row), position, row_height, font_brush, _get_brush.Invoke(row), horizontal_alignment, VerticalAlignment.Center, DefaultCellPadding); + } + } +} \ No newline at end of file diff --git a/src/client/spark/view/Spark/PAndL/PAndLView.xaml b/src/client/spark/view/Spark/PAndL/PAndLView.xaml index 3f75eaa5a..82f340436 100644 --- a/src/client/spark/view/Spark/PAndL/PAndLView.xaml +++ b/src/client/spark/view/Spark/PAndL/PAndLView.xaml @@ -1,240 +1,122 @@ - - - - - - - - - - - - + + + + + + + + + + + + + - - - - + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + + \ No newline at end of file diff --git a/src/client/spark/view/Spark/PAndL/PAndLView.xaml.cs b/src/client/spark/view/Spark/PAndL/PAndLView.xaml.cs index 631b73e7d..19871786a 100644 --- a/src/client/spark/view/Spark/PAndL/PAndLView.xaml.cs +++ b/src/client/spark/view/Spark/PAndL/PAndLView.xaml.cs @@ -1,11 +1,15 @@ -using mbts.spark.filters; +using mbts.ui.controls; using System.Windows.Controls; namespace mbts.spark { internal partial class PAndLView : UserControl { + public enum Columns { Account, Exchange, Symbol, Security, SecurityType, StartNetPos, StartPNL, DayLong, DayShort, DayPNL, TotalNetPos, TotalPNL, PNLPrice } + public PAndLView(PAndLVM vm) { InitializeComponent(); DataContext = vm; } } + + class PnLRowView : DIYGrid.HighPerformanceRow { } } \ No newline at end of file diff --git a/src/client/spark/view/Spark/PAndL/PAndLWindow.cs b/src/client/spark/view/Spark/PAndL/PAndLWindow.cs index 0fa58bd01..ec1b37d34 100644 --- a/src/client/spark/view/Spark/PAndL/PAndLWindow.cs +++ b/src/client/spark/view/Spark/PAndL/PAndLWindow.cs @@ -1,7 +1,11 @@ using bts.api; using bts.shell; using bts.spark; +using mbts.persist; using mbts.ui; +using mbts.ui.settings; +using mbts.ui.spark; +using mbts.ui.utility; using System; using System.Collections.Generic; using System.Linq; @@ -12,17 +16,21 @@ using System.Windows.Media; namespace mbts.spark { internal class PAndLWindow : SparkWindow, ISharedWindow { - const string _window_type = "PAndL", PNL_THREAD_ID = "PnL"; + readonly IWindowSettingsManager _settings_manager; PAndLWindowFactory.PAndLStartupData _startup_data; PAndLModel _model; PAndLView _view; PAndLVM _vm; PAndLWindowFactory _factory; - public PAndLWindow(PAndLModel model, PAndLWindowFactory factory, IBus bus, PAndLWindowFactory.PAndLStartupData startup_data, ImageSource Icon ) : base("P&L", bus, factory, SparkCommands.ShowPAndL.icon) { + IReadOnlyList _commands; + + public PAndLWindow(PAndLModel model, PAndLWindowFactory factory, IBus bus, PAndLWindowFactory.PAndLStartupData startup_data, ImageSource Icon, WindowSettingsManager settings_manager) : base("P&L", bus, factory, SparkCommands.ShowPAndL.icon) { _model = model; _factory = factory; _startup_data = startup_data; + _settings_manager = settings_manager; + Icon.Freeze(); } public override string window_type => _window_type; @@ -30,20 +38,48 @@ namespace mbts.spark { new PAndLWindowFactory.PAndLStartupData( _vm.get_sorting_states(), _vm.get_filters_states(), - _vm.selected_pnl_calc_type + _vm.selected_pnl_calc_type, + _vm.get_grid_save_state() ); public override UIElement Content => _view; - public override double MinWidth => 894; + public override IEnumerable Commands => _commands; + + public override double MinWidth => + PAndLWindowFactory.PAndLStartupData.DefaultGridSettings.columns.Sum(gcs => gcs.width) + + SPARK_MENU_COLLAPSED_WIDTH + + LAUNCHER_BUTTON_BORDER_WIDTH + + EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH; + public string thread_id => PNL_THREAD_ID; public void thread_init() { - _vm = new PAndLVM(_bus, _model, _startup_data); + _vm = new PAndLVM(_bus, _model, _startup_data, _settings_manager); _view = new PAndLView(_vm); _view.DataContext = _vm; + _commands = new List { + SparkCommands.window_settings_command(new DialogCommand(() => { + var vm = new WindowSettingsDialogVM( + _settings_manager, + us => us.pnl_grid, + UserSettings.FileExtensions.GRID, + s => _vm.grid_settings_manager.create_vm( + // we need to get the most up to date GridSettings when launching b/c users may + // have changed some settings on the grid directly, like column widths. + s.with(_vm.columns.Select( c => c.create())), + this.observe_title() + ) + ); + // TODO: bind to main window's Title + var dialog = new SimpleGridSettingsDialog { Title = "PnL Grid Settings", DataContext = vm }; + vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog); + return dialog; + })) + }; } public override void Dispose() { Disposable.dispose(ref _vm); + Commands.OfType().DisposeAll(); } diff --git a/src/client/spark/view/Spark/PAndL/PnLSummaryRow.cs b/src/client/spark/view/Spark/PAndL/PnLSummaryRow.cs index b2de339a5..0f509ece0 100644 --- a/src/client/spark/view/Spark/PAndL/PnLSummaryRow.cs +++ b/src/client/spark/view/Spark/PAndL/PnLSummaryRow.cs @@ -1,36 +1,46 @@ using bts.api; using bts.api.orderman; using mbts.co2; +using mbts.ui; +using mbts.ui.controls; +using mbts.ui.converters; using mbts.ui.shell; using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Windows.Media; namespace mbts.spark { - - - internal class PnLSummaryRow : ViewModelBase, IEquatable { + class PnLSummaryRow : ViewModelBase, IEquatable, DIYGrid.IRowVM { + static readonly SignedDataStore _brushes; + readonly PnLDoubleBrushCell _start_pnl, _day_pnl, _total_pnl; + readonly PnLPriceCell _pnl_price, _weighted_average_buy, _weighted_average_sell; (string account, Venue? exchange, string symbol, SecurityImmutable? security, SecurityMD? sec_md, SecurityType? sec_type) key; - int _start_net_position, _total_net_position; - uint _day_long_quantity, _day_short_quantity; - double _total_pnl, _sod_pnl, _start_pnl, _pnl_price, _weighted_average_buy, _weighted_average_sell; IEnumerable _fills; double _driver; static readonly Dictionary cpp_dict = config.Configuration.get().cpp_entry; + static PnLSummaryRow() { + _brushes = new SignedDataStore(mk_brush(Colors.Green), mk_brush(Colors.Black), mk_brush(Colors.Red), mk_brush(Colors.Black)); + Brush mk_brush(Color c) => + new SolidColorBrush(c) { Opacity = 0.33 }.frozen(); + } public PnLSummaryRow(pnl_calc_type calc_type, (string, Venue?, string, SecurityImmutable?, SecurityMD?, SecurityType?) key, uint day_buy_quant, uint day_sell_quant, int net_pos, int sod_pos, double wavg_buy, double wavg_sell, IEnumerable fills) { this.key = key; + _start_pnl = new PnLDoubleBrushCell(this, nameof(start_pnl)); + _day_pnl = new PnLDoubleBrushCell(this, nameof(day_pnl)); + _total_pnl = new PnLDoubleBrushCell(this, nameof(total_pnl)); day_long_quantity = day_buy_quant; day_short_quantity = day_sell_quant; total_net_position = net_pos; start_net_position = sod_pos; - weighted_average_buy = wavg_buy; - weighted_average_sell = wavg_sell; + _pnl_price = new PnLPriceCell(this, nameof(pnl_price)); + _weighted_average_buy = new PnLPriceCell(this, nameof(weighted_average_buy), wavg_buy); + _weighted_average_sell = new PnLPriceCell(this, nameof(weighted_average_sell), wavg_sell); _fills = fills; // set the initial pnl and pnl_price when there has been a @@ -66,9 +76,9 @@ namespace mbts.spark { } } - public void update_pnl(pnl_calc_type pnl_request) { if (key.sec_md.HasValue) { + bool dirty = false; double cpp = _get_cpp_for_security(key.security.Value); if (pnl_request.HasFlag(pnl_calc_type.bid_or_ask)) { ulong bbo_ref = 0; @@ -86,10 +96,10 @@ namespace mbts.spark { start_pnl_total += _tick_pnl; } } - total_pnl = summary_pnl; - day_pnl = day_pnl_total; - start_pnl = start_pnl_total; - pnl_price = Double.NaN; + _total_pnl.on_next_distinct(summary_pnl, ref dirty); + _day_pnl.on_next_distinct(day_pnl_total, ref dirty); + _start_pnl.on_next_distinct(start_pnl_total, ref dirty); + _pnl_price.on_next_distinct(double.NaN, ref dirty); } else { if (PnLCalculator.get_driver(key.sec_md.Value, pnl_request, ref _driver)) { double summary_pnl = 0.0; @@ -104,17 +114,17 @@ namespace mbts.spark { start_pnl_total += _tick_pnl; } } - total_pnl = summary_pnl; - day_pnl = day_pnl_total; - start_pnl = start_pnl_total; - pnl_price = _driver; + _total_pnl.on_next_distinct(summary_pnl, ref dirty); + _day_pnl.on_next_distinct(day_pnl_total, ref dirty); + _start_pnl.on_next_distinct(start_pnl_total, ref dirty); + _pnl_price.on_next_distinct(_driver, ref dirty); } } + if (dirty) + raise_invalidated(); } } - void ignore() {/*These setters are just for WPF databinding :(*/ throw new NotImplementedException(); } - // probably need to come up with a GetHashCode() while we're in here public bool Equals(PnLSummaryRow other) { return other != null && @@ -140,145 +150,64 @@ namespace mbts.spark { return $"account: {account} exchange: {exchange_string} symbol: {symbol} security: {security_text} buy_quantity: {day_long_quantity} sell_quantity: {day_short_quantity} pnl: {total_pnl}"; } - public string account { - get => key.account; - set => ignore(); - } - - public Venue exchange { - get { - return key.exchange.HasValue ? key.exchange.Value : Venue.Unset; - } - set => ignore(); - } - - public string exchange_string { - get { - return exchange.ToString(); - } - set => ignore(); - } - - public string symbol { - get { - return key.symbol; - } - set => ignore(); - } - + public event Action invalidated; + // non-numeric + public string account => key.account; + public Venue exchange => key.exchange ?? Venue.Unset; + public string exchange_string => exchange.ToString(); + public string symbol => key.symbol; public SecurityImmutable? security => key.security; public SecurityType security_type => key.sec_type.GetValueOrDefault(); + public string security_text => security.display_name(); + // positions + public int start_net_position { get; } + public int total_net_position { get; } + public uint day_long_quantity { get; } + public uint day_short_quantity { get; } + // start pnl + public double start_pnl => _start_pnl.value; + public string start_pnl_text => _start_pnl.text; + public Brush start_pnl_background => _start_pnl.background; + // day pnl + public double day_pnl => _day_pnl.value; + public string day_pnl_text => _day_pnl.text; + public Brush day_pnl_background => _day_pnl.background; + // total pnl + public double total_pnl => _total_pnl.value; + public string total_pnl_text => _total_pnl.text; + public Brush total_pnl_background => _total_pnl.background; + // pnl price + public double pnl_price => _pnl_price.value; + public string pnl_price_text => _pnl_price.text; + // pnl weighted averages + public double weighted_average_buy => _weighted_average_buy.value; + public string weighted_average_buy_text => _weighted_average_buy.text; + public double weighted_average_sell => _weighted_average_sell.value; + public string weighted_average_sell_text => _weighted_average_sell.text; - public string security_text { - get => security.HasValue ? security.Value.ToString() : "Unknown Security"; - set => ignore(); - } - - - public int start_net_position { - get => _start_net_position; - set { - if (_start_net_position != value) { - _start_net_position = value; - RaisePropertyChanged(nameof(start_net_position)); - } - } - } - public double start_pnl { - get => _start_pnl; - set { - if (_start_pnl != value) { - _start_pnl = value; - RaisePropertyChanged(); - } - } - } - public uint day_long_quantity { - get => _day_long_quantity; - set { - if (_day_long_quantity != value) { - _day_long_quantity = value; - RaisePropertyChanged(nameof(day_long_quantity)); - } - } - } - public uint day_short_quantity { - get => _day_short_quantity; - set { - if (_day_short_quantity != value) { - _day_short_quantity = value; - RaisePropertyChanged(nameof(day_short_quantity)); - } - } - } - public double day_pnl { - get => _sod_pnl; - set { - if (_sod_pnl != value) { - _sod_pnl = value; - RaisePropertyChanged(); - } - } - } - - - public int total_net_position { - get => _total_net_position; - set { - if (_total_net_position != value) { - _total_net_position = value; - RaisePropertyChanged(nameof(total_net_position)); - } - } - } - - public double total_pnl { - get => _total_pnl; - set { - if (_total_pnl != value) { - _total_pnl = value; - RaisePropertyChanged(nameof(total_pnl)); - } - } - } - - - - - public double pnl_price { - get => _pnl_price; - set { - if (_pnl_price != value) { - _pnl_price = value; - RaisePropertyChanged(nameof(pnl_price)); - } - } - } - - public double weighted_average_buy { - get => _weighted_average_buy; - set { - if (_weighted_average_buy != value) { - _weighted_average_buy = value; - RaisePropertyChanged(nameof(weighted_average_buy)); - } - } - } - - public double weighted_average_sell { - get => _weighted_average_sell; - set { - if (_weighted_average_sell != value) { - _weighted_average_sell = value; - RaisePropertyChanged(nameof(weighted_average_sell)); - } - } - } + public void raise_invalidated() => invalidated?.Invoke(); + ObservableCollection DIYGrid.IRowVM.row_animations { get; } = new ObservableCollection(); + IReadOnlyList DIYGrid.IRowVM.cell_stores => new DIYGrid.IGridCellStore[0]; //public IEnumerable sod_fills => _fills.Where(f => f.Timestamp.ToDateTime() >= _start_of_day); //public IEnumerable pos_fills => _fills.Where(f => f.Timestamp.ToDateTime() < _start_of_day); + class PnLDoubleBrushCell : CellProperty { + Brush _background; + + public PnLDoubleBrushCell(PnLSummaryRow owner, string prop) : base(d => double.IsNaN(d) ? string.Empty : d.ToString("N2"), double.NaN) => + _value_set += _ => { + _background = null; + owner.RaisePropertyChanged(prop); + }; + + public Brush background => _background ?? (_background = _brushes.get(value)); + } + + class PnLPriceCell : CellProperty { + public PnLPriceCell(PnLSummaryRow owner, string prop, double init = 0) + : base(price => owner.security.spec_or_default().format(price), init) => + _value_set += _ => owner.RaisePropertyChanged(prop); + } } - - -} +} \ No newline at end of file diff --git a/src/client/spark/view/Spark/SparkWindow.cs b/src/client/spark/view/Spark/SparkWindow.cs index 1432a42c8..a65b7d983 100644 --- a/src/client/spark/view/Spark/SparkWindow.cs +++ b/src/client/spark/view/Spark/SparkWindow.cs @@ -15,6 +15,7 @@ namespace bts.spark { protected const double SPARK_MENU_COLLAPSED_WIDTH = 52, LAUNCHER_BUTTON_BORDER_WIDTH = 5, + EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH = 18, TITLE_PANEL_HEIGHT = 30, COLUMN_HEADER_HEIGHT = 27, SPARK_MENU_ITEM_HEIGHT = 45, diff --git a/src/client/spark/view/Spark/Utilities/MDHandleExtensions.cs b/src/client/spark/view/Spark/Utilities/MDHandleExtensions.cs index 7b4c3a68d..379235c88 100644 --- a/src/client/spark/view/Spark/Utilities/MDHandleExtensions.cs +++ b/src/client/spark/view/Spark/Utilities/MDHandleExtensions.cs @@ -9,8 +9,8 @@ namespace mbts.co2 { public static double mid(this MDConsumerHandle handle) { ulong unused = 0; handle.get_bbo(ref unused, out NativePriceSize bid, out NativePriceSize ask); - if (bid == NativePriceSize.no_market) return ask.price; - if (ask == NativePriceSize.no_market) return bid.price; + if (bid == NativePriceSize.NO_MARKET) return ask.price; + if (ask == NativePriceSize.NO_MARKET) return bid.price; return (ask.price + bid.price) / 2; } diff --git a/src/client/spark/view/Spark/Utilities/OrderDataHelper.cs b/src/client/spark/view/Spark/Utilities/OrderDataHelper.cs index 37a15903b..d837e076a 100644 --- a/src/client/spark/view/Spark/Utilities/OrderDataHelper.cs +++ b/src/client/spark/view/Spark/Utilities/OrderDataHelper.cs @@ -110,6 +110,29 @@ namespace mbts.spark.utilities { return defs; } + public Dictionary default_order_type_templates(SecurityImmutable security, bool is_child = false) { + const int name_ref_seed = 1; + // standard types + var defs = default_order_types(security, is_child) + .Except(new[] { CustomOrderDefinition.CustomOneofCase.Dynamic }) + .ToDictionary(ot => new OrderTemplateHeader(new OrderTemplateKey(ot), OrderUtils.order_type_display_name(ot)), ot => new CustomOrderDefinitionImmutable(custom_order(ot))); + // TODO: replace with better naming scheme (right now, the order type ref count 'n' is appended to the order-type name) + var order_type_ref_count = defs.ToDictionary(kv => kv.Key, kv => name_ref_seed); + var dynamic_defs = new List(); + DynamicOrderHelper.add_dynamic_order_defs(dynamic_defs, ot => custom_order(ot)); + foreach (var ddef in dynamic_defs) { + var header = new OrderTemplateHeader(new OrderTemplateKey(ddef.CustomCase, ddef.Dynamic.DynamicOrderType), ddef.Dynamic.DynamicOrderType ); + // if (order_type_ref_count.TryGetValue(header, out int count)) { + // order_type_ref_count[header]++; + // header = new OrderTemplateHeader(new OrderTemplateKey(header.key.type, $"{header.key.name}{count}"), $"{header.display_name}{count}"); + // } else { + // order_type_ref_count[header] = name_ref_seed; + // } + defs.Add(header, ddef); + } + return defs; + } + public IEnumerable default_order_types(SecurityImmutable? security, bool is_child=false) => security.HasValue ? default_order_types(security.Value, is_child) : default_order_types(); diff --git a/src/client/spark/view/Spark/Utilities/WindowSettingsDialogVM.cs b/src/client/spark/view/Spark/Utilities/WindowSettingsDialogVM.cs index ccc6a5bf1..5c5867fb3 100644 --- a/src/client/spark/view/Spark/Utilities/WindowSettingsDialogVM.cs +++ b/src/client/spark/view/Spark/Utilities/WindowSettingsDialogVM.cs @@ -11,66 +11,70 @@ namespace mbts.spark { T get_result(); } - class WindowSettingsDialogVM : ViewModelBase, IRequestViewClose where T : class, IEquatable where TVM : IWindowSettingsVM, new() { - readonly Property _default_tab_selected, _use_default; - readonly DialogSkirtItem _sync_to_factory_item, _sync_to_default_item, _sync_to_instance_item, _ok_item, _apply_item, _cancel_item; + class WindowSettingsDialogVM : ViewModelBase, IRequestViewClose where T : class, IEquatable where TVM : IWindowSettingsVM { + readonly Property _use_default; + readonly DialogSkirtItem _ok_item, _apply_item, _cancel_item, _apply_to_defaults_item; readonly DelegateProperty> _left_skirt_items; + readonly List _resources = new List(2); // initialize with 2 capacity bc the only potential disposables are the instance/default vms - public WindowSettingsDialogVM(IWindowSettingsManager settings_manager, Func get_factory_settings, string settings_file_ext) { - var settings = settings_manager.current_settings(); - _default_tab_selected = backing(nameof(default_tab_selected), settings_manager.instance_settings == null); - _use_default = backing(nameof(use_default), default_tab_selected); - default_vm = new TVM(); - default_vm.load(settings_manager.default_settings); - instance_vm = new TVM(); - instance_vm.load(settings_manager.current_settings()); + public WindowSettingsDialogVM(IWindowSettingsManager settings_manager, Func get_factory_settings, string settings_file_ext, Func vm_factory) { + // by default, each new window will use their own instance settings + _use_default = backing(nameof(use_default), settings_manager.instance_settings == null); + default_vm = vm_factory(settings_manager.default_settings); + // instance vm is what will always be visible on the screen + instance_vm = vm_factory(settings_manager.current_settings()); // items const string import_group = "i", export_group = "e"; (string name, string[] extensions) file_filter = ($"{settings_file_ext} settings files", new[] { settings_file_ext }); - var import_to_custom_cmd = FileDialogCommand.import_proto(s => instance_vm.load(s), () => !use_default, settings_file_ext, filter_items: file_filter); - var export_to_custom_cmd = FileDialogCommand.export_proto(() => instance_vm.get_result(), () => !use_default, filter_items: file_filter); - _use_default.Changed += (_, __) => { - import_to_custom_cmd.RaiseCanExecuteChanged(); - export_to_custom_cmd.RaiseCanExecuteChanged(); - }; + var import_to_custom_cmd = FileDialogCommand.import_proto(s => instance_vm.load(s), type_name: settings_file_ext, filter_items: file_filter); + var export_to_custom_cmd = FileDialogCommand.export_proto(() => instance_vm.get_result(), filter_items: file_filter); action_menu_items = new IMenuItemVM[] { - new MenuItemVM("Import File to Default...", FileDialogCommand.import_proto(s => default_vm.load(s), null, settings_file_ext), Icons.Import, import_group), - new MenuItemVM("Import File to Custom...", import_to_custom_cmd, Icons.Import, import_group), - new MenuItemVM("Export Default to File...", FileDialogCommand.export_proto(() => default_vm.get_result(), filter_items: file_filter), Icons.Export, export_group), - new MenuItemVM("Export Custom to File...", export_to_custom_cmd, Icons.Export, export_group) + new MenuItemVM("Load Settings File ...", import_to_custom_cmd, Icons.Import, import_group), + new MenuItemVM("Save Settings File ...", export_to_custom_cmd, Icons.Export, export_group), + new MenuItemVM("Load Factory Defaults", + new DelegateCommand(() => instance_vm.load(get_factory_settings(UserSettings.factory_defaults))), + Icons.Factory + ) }; - _left_skirt_items = backing(nameof(left_skirt_items), left_skirt_factory, nameof(default_tab_selected)); - _sync_to_factory_item = new DialogSkirtItem( - "Reset to Factory", - new DelegateCommand(() => (default_tab_selected ? default_vm : instance_vm).load(get_factory_settings(UserSettings.factory_defaults))), - ui.spark.LadderSettingsDialog.ResetToFactoryKey); - _sync_to_default_item = new DialogSkirtItem( - "Copy from Default", - new DelegateCommand(() => instance_vm.load(settings_manager.default_settings)), - ui.spark.LadderSettingsDialog.CopyFromOtherKey); - _sync_to_instance_item = new DialogSkirtItem( - "Copy from Custom", - new DelegateCommand(() => default_vm.load(instance_vm.get_result())), - ui.spark.LadderSettingsDialog.CopyFromOtherKey); - _apply_item = new DialogSkirtItem("Apply", new DelegateCommand(apply)); + _left_skirt_items = backing(nameof(left_skirt_items), left_skirt_factory); + _apply_item = new DialogSkirtItem("Apply", new DelegateCommand(() => { + use_default = false; + apply(); + })); _ok_item = new DialogSkirtItem("OK", new DelegateCommand(() => { + use_default = false; apply(); close(); })) { is_default = true }; _cancel_item = new DialogSkirtItem("Cancel", new DelegateCommand(close)) { is_cancel = true }; + _apply_to_defaults_item = new DialogSkirtItem("Save As Defaults", new DelegateCommand(() => { + use_default = true; + apply(); + close(); + }), ui.spark.LadderSettingsDialog.ResetToFactoryKey); + // maybe dispose vms + if (instance_vm is IDisposable dispose_ivm) + _resources.Add(dispose_ivm); + if (default_vm is IDisposable dispose_dvm) + _resources.Add(dispose_dvm); // local methods void apply() { - var my_default = default_vm.get_result(); - if (!my_default.safe_equals(settings_manager.default_settings)) - settings_manager.save_default_settings(my_default).ignore(); if (!use_default) { settings_manager.instance_settings = instance_vm.get_result(); - } else settings_manager.instance_settings = null; + } else { + settings_manager.instance_settings = null; + + // copy the current instance settings and persist + var my_default = instance_vm.get_result(); + if (!my_default.safe_equals(settings_manager.default_settings)) + settings_manager.save_default_settings(my_default).ignore(); + } } + void close() => RequestClose?.Invoke(this, new EventArgs()); + IEnumerable left_skirt_factory() { - yield return _sync_to_factory_item; - yield return default_tab_selected ? _sync_to_instance_item : _sync_to_default_item; + yield return _apply_to_defaults_item; }; } @@ -87,14 +91,12 @@ namespace mbts.spark { yield return _cancel_item; } } - public bool default_tab_selected { get => _default_tab_selected.Value; set => _default_tab_selected.Value = value; } public bool use_default { get => _use_default.Value; - set { - _use_default.Value = value; - if (value) - default_tab_selected = true; - } + set => _use_default.Value = value; } + + public void Dispose() => + Disposable.dispose(_resources); } } \ No newline at end of file diff --git a/src/client/spark/view/Utility/EnabledValueVM.cs b/src/client/spark/view/Utility/EnabledValueVM.cs index e3acce1d5..db174bcb4 100644 --- a/src/client/spark/view/Utility/EnabledValueVM.cs +++ b/src/client/spark/view/Utility/EnabledValueVM.cs @@ -1,4 +1,5 @@ using mbts.ui.shell; +using System; namespace mbts.ui { public class EnabledValueVM : ViewModelBase { @@ -8,10 +9,13 @@ namespace mbts.ui { public EnabledValueVM() { _enabled = backing(nameof(enabled)); _value = backing(nameof(value)); + _enabled.Changed += (_, __) => enabled_changed?.Invoke(); } public bool enabled { get => _enabled.Value; set => _enabled.set(value); } public T value { get => _value.Value; set => _value.set(value); } + + public event Action enabled_changed; } public class NamedValueVM { diff --git a/src/client/spark/view/Utility/MDDisplayHelper.cs b/src/client/spark/view/Utility/MDDisplayHelper.cs new file mode 100644 index 000000000..f0ae41665 --- /dev/null +++ b/src/client/spark/view/Utility/MDDisplayHelper.cs @@ -0,0 +1,284 @@ +using bts.utils; +using mbts.co2; +using mbts.spark; +using mbts.ui.shell; +using mbts.utils; +using ProtoBuf; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace mbts.ui.spark { + [ProtoContract] + class MarketSizeDisplay : IEquatable { + [ProtoMember(1)] + public readonly SizeFormattingElement[] normal; + [ProtoMember(2)] + public readonly SizeFormattingElement[] implied_present; + + public MarketSizeDisplay() { } + public MarketSizeDisplay(SizeFormattingElement[] normal, SizeFormattingElement[] implied_present) { + this.normal = normal; + this.implied_present = implied_present; + } + + public enum SizeFormattingElementType { + Merged, + Direct, + Implied, + Const + } + + public static MarketSizeDisplay get_default() => + new MarketSizeDisplay(SizeFormattingElement.merged, SizeFormattingElement.merged_asterisk); + + public MarketSizeDisplay with(SizeFormattingElement[] normal = null, SizeFormattingElement[] implied_present = null) => + new MarketSizeDisplay(normal ?? this.normal, implied_present ?? this.implied_present); + + public bool Equals(MarketSizeDisplay other) => + other != null && + EqualityComparer.safe_sequence_equals(normal, other.normal) && + EqualityComparer.safe_sequence_equals(implied_present, other.implied_present); + + public override bool Equals(object obj) => + Equals(obj as MarketSizeDisplay); + + public override int GetHashCode() => + EqualityComparer.aggregate_hash(implied_present.GetHashCode(), normal.GetHashCode()); + + [ProtoContract] + public struct SizeFormattingElement : IEquatable { + [ProtoMember(1)] + public readonly SizeFormattingElementType type; + [ProtoMember(2)] + public readonly string data; + + SizeFormattingElement(SizeFormattingElementType type, string data = null) { + this.type = type; + this.data = data; + } + + public bool Equals(SizeFormattingElement other) => other.type == type && string.Equals(data, other.data); + public override bool Equals(object obj) => obj is SizeFormattingElement other && Equals(other); + public override int GetHashCode() => EqualityComparer.aggregate_hash(type.GetHashCode(), EqualityComparer.safe_get_hash_code(data)); + public override string ToString() => type == SizeFormattingElementType.Const ? data ?? string.Empty : type.ToString(); + + public static bool operator ==(SizeFormattingElement a, SizeFormattingElement b) => a.Equals(b); + public static bool operator !=(SizeFormattingElement a, SizeFormattingElement b) => !a.Equals(b); + + public static SizeFormattingElement[] direct => new[] { new SizeFormattingElement(SizeFormattingElementType.Direct) }; + public static SizeFormattingElement[] implied => new[] { new SizeFormattingElement(SizeFormattingElementType.Implied) }; + public static SizeFormattingElement[] direct_implied => new[] { + new SizeFormattingElement(SizeFormattingElementType.Direct), + new SizeFormattingElement(SizeFormattingElementType.Const, " ("), + new SizeFormattingElement(SizeFormattingElementType.Implied), + new SizeFormattingElement(SizeFormattingElementType.Const, ")") + }; + public static SizeFormattingElement[] merged => new[] { new SizeFormattingElement(SizeFormattingElementType.Merged) }; + public static SizeFormattingElement[] merged_asterisk => new[] { new SizeFormattingElement(SizeFormattingElementType.Merged), new SizeFormattingElement(SizeFormattingElementType.Const, "*") }; + public static SizeFormattingElement[] merged_implied => new[] { + new SizeFormattingElement(SizeFormattingElementType.Merged), + new SizeFormattingElement(SizeFormattingElementType.Const, " ("), + new SizeFormattingElement(SizeFormattingElementType.Implied), + new SizeFormattingElement(SizeFormattingElementType.Const, ")") + }; + public static SizeFormattingElement[] merged_direct => new[] { + new SizeFormattingElement(SizeFormattingElementType.Merged), + new SizeFormattingElement(SizeFormattingElementType.Const, " ("), + new SizeFormattingElement(SizeFormattingElementType.Direct), + new SizeFormattingElement(SizeFormattingElementType.Const, ")") + }; + public static SizeFormattingElement[] merged_direct_implied => new[] { + new SizeFormattingElement(SizeFormattingElementType.Merged), + new SizeFormattingElement(SizeFormattingElementType.Const, " ("), + new SizeFormattingElement(SizeFormattingElementType.Direct), + new SizeFormattingElement(SizeFormattingElementType.Const, ":"), + new SizeFormattingElement(SizeFormattingElementType.Implied), + new SizeFormattingElement(SizeFormattingElementType.Const, ")") + }; + } + } + + static class MDDisplayHelper { + /// This delegate should be created by calling + public delegate string SizeFormatter(uint merged, uint implied); + /// The private delegates below use refs bc the returned by + /// internally uses several s to add different text components to the resultant string. + /// The refs, therefor, avoid making extra copies of the variables. + /// These are private members, so any code-readability cost is confined to a limited scope. + delegate void _SizeFormatter(ref uint merged, ref uint implied, ref StringBuilder sb); + delegate void _SizeFormatterNPS(ref NativePriceSize nps, ref StringBuilder sb); + + public static Func get_formatter_nps(MarketSizeDisplay size_display) { + _SizeFormatterNPS implied_absent = _get_formatter_nps(size_display.normal); + _SizeFormatterNPS implied_present = _get_formatter_nps(size_display.implied_present ?? size_display.normal); + return nps => { + StringBuilder sb = new StringBuilder(); + (nps.implied_size > 0 ? implied_present : implied_absent)(ref nps, ref sb); + return sb.ToString(); + }; + } + + public static SizeFormatter get_formatter(MarketSizeDisplay size_display) { + _SizeFormatter implied_absent = _get_formatter(size_display.normal); + _SizeFormatter implied_present = _get_formatter(size_display.implied_present ?? size_display.normal); + return (merged, implied) => { + StringBuilder sb = new StringBuilder(); + (implied > 0 ? implied_present : implied_absent)(ref merged, ref implied, ref sb); + return sb.ToString(); + }; + } + + static _SizeFormatterNPS _get_formatter_nps(MarketSizeDisplay.SizeFormattingElement[] fmt_elements) { + _SizeFormatterNPS[] formatters = fmt_elements.is_null_or_empty() ? new _SizeFormatterNPS[0] : CollectionHelper.convert_array(fmt_elements, _get_formatter_nps); + return new _SizeFormatterNPS((ref NativePriceSize nps, ref StringBuilder sb) => { + foreach (var f in formatters) + f(ref nps, ref sb); + }); + } + + static _SizeFormatter _get_formatter(MarketSizeDisplay.SizeFormattingElement[] fmt_elements) { + _SizeFormatter[] formatters = fmt_elements.is_null_or_empty() ? new _SizeFormatter[0] : CollectionHelper.convert_array(fmt_elements, _get_formatter); + return new _SizeFormatter((ref uint m, ref uint i, ref StringBuilder sb) => { + foreach (var f in formatters) + f(ref m, ref i, ref sb); + }); + } + + static _SizeFormatterNPS _get_formatter_nps(MarketSizeDisplay.SizeFormattingElement fmt_element) { + switch (fmt_element.type) { + case MarketSizeDisplay.SizeFormattingElementType.Const: return SizeTokenFormatters.char_formatter_nps(fmt_element.data ?? string.Empty); + case MarketSizeDisplay.SizeFormattingElementType.Direct: return SizeTokenFormatters.direct_formatter_nps(); + case MarketSizeDisplay.SizeFormattingElementType.Implied: return SizeTokenFormatters.implied_formatter_nps(); + case MarketSizeDisplay.SizeFormattingElementType.Merged: return SizeTokenFormatters.merged_formatter_nps(); + default: throw ExceptionBuilder.Argument.unexpected_case(fmt_element.type, nameof(fmt_element.type)); + } + } + + static _SizeFormatter _get_formatter(MarketSizeDisplay.SizeFormattingElement fmt_element) { + switch (fmt_element.type) { + case MarketSizeDisplay.SizeFormattingElementType.Const: return SizeTokenFormatters.char_formatter(fmt_element.data ?? string.Empty); + case MarketSizeDisplay.SizeFormattingElementType.Direct: return SizeTokenFormatters.direct_formatter(); + case MarketSizeDisplay.SizeFormattingElementType.Implied: return SizeTokenFormatters.implied_formatter(); + case MarketSizeDisplay.SizeFormattingElementType.Merged: return SizeTokenFormatters.merged_formatter(); + default: throw ExceptionBuilder.Argument.unexpected_case(fmt_element.type, nameof(fmt_element.type)); + } + } + + static class SizeTokenFormatters { + public static _SizeFormatter char_formatter(string data) { + string local = data.ToString(); + return new _SizeFormatter((ref uint m, ref uint i, ref StringBuilder sb) => sb.Append(local)); + } + public static _SizeFormatter merged_formatter() => new _SizeFormatter((ref uint m, ref uint i, ref StringBuilder sb) => sb.Append(m)); + public static _SizeFormatter direct_formatter() => new _SizeFormatter((ref uint m, ref uint i, ref StringBuilder sb) => sb.Append(m - i)); + public static _SizeFormatter implied_formatter() => new _SizeFormatter((ref uint m, ref uint i, ref StringBuilder sb) => sb.Append(i)); + + public static _SizeFormatterNPS char_formatter_nps(string data) { + string local = data.ToString(); + return new _SizeFormatterNPS((ref NativePriceSize _, ref StringBuilder sb) => sb.Append(local)); + } + public static _SizeFormatterNPS merged_formatter_nps() => new _SizeFormatterNPS((ref NativePriceSize nps, ref StringBuilder sb) => sb.Append(nps.size)); + public static _SizeFormatterNPS direct_formatter_nps() => new _SizeFormatterNPS((ref NativePriceSize nps, ref StringBuilder sb) => sb.Append(nps.direct_size)); + public static _SizeFormatterNPS implied_formatter_nps() => new _SizeFormatterNPS((ref NativePriceSize nps, ref StringBuilder sb) => sb.Append(nps.implied_size)); + } + } + + class MarketSizeDisplayVM : ViewModelBase, IValueFactory { + readonly IEqualityComparer _comp = EqualityComparer.Create((a, b) => EqualityComparer.safe_sequence_equals(a, b)); + readonly Property _selected_format, _selected_alternate_implied_format; + readonly DelegateProperty _can_choose_alternate_implied_format; + + public MarketSizeDisplayVM() { + _selected_format = backing(nameof(selected_format)); + _selected_alternate_implied_format = backing(nameof(selected_alternate_implied_format)); + _can_choose_alternate_implied_format = backing(nameof(can_choose_alternate_implied_format), () => + _comp.Equals(MarketSizeDisplay.SizeFormattingElement.merged, selected_format?.value)); + sample_items = new[] { + new SampleItem(90, 0, this), + new SampleItem(75, 25, this) + }; + _selected_format.Changed += refresh_sample_items; + _selected_alternate_implied_format.Changed += refresh_sample_items; + void refresh_sample_items(FormattingVM oldval, FormattingVM newval) { + _can_choose_alternate_implied_format.refresh(); + foreach (var si in sample_items) + si.refresh(); + } + } + + public IEnumerable format_options { get; } = new[] { + MarketSizeDisplay.SizeFormattingElement.direct, + MarketSizeDisplay.SizeFormattingElement.implied, + MarketSizeDisplay.SizeFormattingElement.direct_implied, + MarketSizeDisplay.SizeFormattingElement.merged_implied, + MarketSizeDisplay.SizeFormattingElement.merged_direct_implied, + MarketSizeDisplay.SizeFormattingElement.merged + }.Select(ts => new FormattingVM(ts)).ToArray(); + + public IEnumerable alternate_implied_format_options { get; } = new[] { + MarketSizeDisplay.SizeFormattingElement.merged, + MarketSizeDisplay.SizeFormattingElement.merged_asterisk, + MarketSizeDisplay.SizeFormattingElement.merged_implied, + MarketSizeDisplay.SizeFormattingElement.merged_direct, + MarketSizeDisplay.SizeFormattingElement.merged_direct_implied + }.Select(ts => new FormattingVM(ts)).ToArray(); + + public FormattingVM selected_format { + get => _selected_format.Value; + set => _selected_format.Value = value; + } + public FormattingVM selected_alternate_implied_format { + get => _selected_alternate_implied_format.Value; + set => _selected_alternate_implied_format.Value = value; + } + + public bool can_choose_alternate_implied_format => _can_choose_alternate_implied_format.Value; + + public IEnumerable sample_items { get; } + + public MarketSizeDisplay create() { + var normal_fmt = selected_format?.value; + var alt_implied_fmt = can_choose_alternate_implied_format ? selected_alternate_implied_format?.value : normal_fmt; + return new MarketSizeDisplay(normal_fmt, alt_implied_fmt); + } + + public void load_from(MarketSizeDisplay msd) { + selected_format = format_options.FirstOrDefault(f => _comp.Equals(f.value, msd.normal)); + if (can_choose_alternate_implied_format) + selected_alternate_implied_format = alternate_implied_format_options.FirstOrDefault(f => _comp.Equals(f.value, msd.implied_present)); + } + + public class FormattingVM : ViewModelBase, IEquatable { + public FormattingVM(MarketSizeDisplay.SizeFormattingElement[] tokens) => + value = tokens; + + public MarketSizeDisplay.SizeFormattingElement[] value { get; } + + public bool Equals(FormattingVM other) => + other != null && + EqualityComparer.safe_sequence_equals(value, other.value); + + public override bool Equals(object obj) => Equals(obj as FormattingVM); + public override int GetHashCode() => EqualityComparer.safe_aggregate_hash(value); + public override string ToString() => string.Join(string.Empty, value); + } + + public class SampleItem : ViewModelBase { + readonly DelegateProperty _display; + + public SampleItem(uint direct, uint implied, MarketSizeDisplayVM owner) { + this.direct = direct; + this.implied = implied; + _display = backing(nameof(display), () => MDDisplayHelper.get_formatter(owner.create())(merged, implied)); + } + + public uint direct { get; } + public uint implied { get; } + public uint merged => direct + implied; + public string display => _display.Value; + public void refresh() => _display.refresh(); + } + } +} \ No newline at end of file diff --git a/src/client/spark/view/Utility/SecurityUtils.cs b/src/client/spark/view/Utility/SecurityUtils.cs deleted file mode 100644 index 0e732c297..000000000 --- a/src/client/spark/view/Utility/SecurityUtils.cs +++ /dev/null @@ -1,90 +0,0 @@ -using bts.api; -using mbts.utils; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -namespace mbts.ui.utility -{ - public enum DateFormat { Standard, DayMonthAbvDiff2000, DayMonthCodeDiff2000 } - static class SecurityDisplay - { - /// - /// Returns a shorthand string representation of a security type appropriate for UIs. - /// - public static string format_security_type(SecurityType type) { - switch (type) { - case SecurityType.Option: return "Opt"; - case SecurityType.Future: return "Fut"; - case SecurityType.Stock: return "Stk"; - case SecurityType.Spread: return "Spd"; - default: return type.ToString(); - } - } - - /// - /// Returns a concise string representation of a security expiration appropriate for UIs (e.g. es March 22, 2019: es 22 Mar 19). - /// - public static string format_expiration(SecurityType sec_type, string symbol, DateTime exp_date) { return format_expiration(sec_type, symbol, exp_date, false, true, true, DateFormat.DayMonthAbvDiff2000); } - - /// - /// Returns a concise string representation of a security expiration (using symbol-mapped expiration dates, and single-letter month codes) appropriate for UIs (e.g. es March 22, 2019: [Opt] [es] 22 Mar 19). - /// - public static string format_expiration(SecurityType sec_type, string symbol, DateTime exp_date, bool show_type, bool show_symbol, bool use_mapped_exp) { return format_expiration(sec_type, symbol, exp_date, show_type, show_symbol, use_mapped_exp, DateFormat.Standard); } - - /// - /// Returns a concise string representation of a security expiration (using symbol-mapped expiration dates, and single-letter month codes) appropriate for UIs (e.g. es March 22, 2019: [Opt] [es] 22 Mar 19, or [Opt] [es] 22 H19). - /// - public static string format_expiration(SecurityType sec_type, string symbol, DateTime exp_date, bool show_type, bool show_symbol, bool use_mapped_exp, DateFormat date_format) { - if (use_mapped_exp) exp_date = ExpirationFormatter.convert_real_to_mapped(sec_type, symbol, exp_date); - string result = format_date(exp_date); - if (show_symbol) result = (symbol + " " + result); - if (show_type) result = (format_security_type(sec_type) + " " + result); - return result; - } - - - /// - /// Returns a concise string representation of a future trade appropriate for UIs. - /// - public static string format_future_trade(string symbol, DateTime exp_date, int size, double marginal_price) { return "{0} {1} lots @ {2:C}".fmt(format_expiration(SecurityType.Future, symbol, exp_date, false, true, true), size, marginal_price); } - - /// - /// Returns a concise string representation of an option trade appropriate for UIs. - /// - public static string format_option_trade(string symbol, DateTime exp_date, double strike, bool is_call, int size, double marginal_price) { - return "{0} {1} {2} {3} lots @ {4:C}".fmt(format_expiration(SecurityType.Option, symbol, exp_date, false, true, true), strike, is_call ? "C" : "P", size, marginal_price); - } - - /// - /// Returns a concise string representation of a date (using standard month abbreviations) appropriate for UIs (e.g. March 22, 2019: 22 Mar 19). - /// - public static string format_date(DateTime date) { return format_date(date.Year, date.Month, date.Day, DateFormat.DayMonthAbvDiff2000); } - - /// - /// Returns a concise string representation of a date (using single-letter month codes) appropriate for UIs (e.g. March 22, 2019: 22 Mar 19, or 22 H19). - /// - public static string format_date(int year, int month, int day, DateFormat format) { - switch(format) { - case DateFormat.DayMonthAbvDiff2000: return "{0} {1} {2}".fmt(day, get_month_abv(month), year - 2000); - case DateFormat.DayMonthCodeDiff2000: return "{0} {1}{2}".fmt(day, get_month_code(month), year - 2000); - case DateFormat.Standard: goto default; - default: return "{0}/{1}/{2}".fmt(day, month, year); - } - } - /// - /// Returns the option industry standard month code. - /// - public static string get_month_code(int month) { _check_month(month); return _month_codes[month - 1]; } - /// - /// Returns the standard month abbreviation (Jan, Feb, Mar, etc.). - /// - public static string get_month_abv(int month) { _check_month(month); return _month_abvs[month - 1]; } - #region Private Members - private static void _check_month(int month) { if (month < 1 || month > 12) throw new ArgumentException("There are only twelve months in a year", "month"); } - private static string[] _month_codes = new string[] { "F", "G", "H", "J", "K", "M", "N", "Q", "U", "V", "X", "Z" }; - private static string[] _month_abvs = new string[] { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; - #endregion - } -} \ No newline at end of file diff --git a/src/client/spark/view/Utility/WPFHelper.cs b/src/client/spark/view/Utility/WPFHelper.cs index dad292484..275ac1ea2 100644 --- a/src/client/spark/view/Utility/WPFHelper.cs +++ b/src/client/spark/view/Utility/WPFHelper.cs @@ -38,6 +38,9 @@ namespace mbts.ui { public static DependencyProperty create_dp(string name, PropertyChangedCallback callback, TValue default_value = default) => DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), new PropertyMetadata(default_value, prop_changed_callback(callback))); + public static DependencyProperty create_dp(string name, FrameworkPropertyMetadataOptions options, PropertyChangedCallback callback, TValue default_value = default) => + DependencyProperty.Register(name, typeof(TValue), typeof(TOwner), new FrameworkPropertyMetadata(default_value, options, prop_changed_callback(callback))); + public static DependencyPropertyKey create_ro_dp(string name, TValue default_value = default) => DependencyProperty.RegisterReadOnly(name, typeof(TValue), typeof(TOwner), new PropertyMetadata(default_value)); diff --git a/src/client/spark_api/api/Update.cs b/src/client/spark_api/api/Update.cs index 2853882db..6f9604a01 100644 --- a/src/client/spark_api/api/Update.cs +++ b/src/client/spark_api/api/Update.cs @@ -129,6 +129,7 @@ namespace bts.api.core { public IEnumerable removed { get; } public TKey key(T t) { return _key(t); } + internal Func key_extractor => _key; #if DEBUG public override string ToString() { @@ -347,5 +348,72 @@ namespace bts.api.core { return source.Select(t => new LegacyTableUpdate(t)); } + public static IObservable> Filter(this IObservable> source, IObservable> predicate) where TKey : IEquatable + { + return Observable.Create>(observer => + { + Update unfiltered_cache = null, filtered_cache = null; + Func pred_cache = null; + Log.DataFlow.diag($"Create Filter {typeof(T).Name}"); + + return source.CombineLatest(predicate, (upd, pred) => (upd, pred)).Subscribe( tup => + { + bool pred_changed = !object.ReferenceEquals(tup.pred, pred_cache) && (pred_cache != null); + pred_cache = tup.pred; + unfiltered_cache = tup.upd; + if (pred_changed && filtered_cache != null) + { + //clear out existing + var clear = new Update( + (IReadOnlyCollection)Array.Empty(), + Enumerable.Empty(), + Enumerable.Empty>(), + filtered_cache.all, unfiltered_cache.key_extractor); + observer.OnNext(clear); + + var refilter = unfiltered_cache.all.Where(tup.pred).ToList(); + var readd = new Update( + refilter, refilter, + Enumerable.Empty>(), + Enumerable.Empty(), unfiltered_cache.key_extractor); + observer.OnNext(readd); + filtered_cache = readd; + } + else + { + IReadOnlyCollection all; IEnumerable added, removed; IEnumerable> changed; + apply_predicate(tup.pred, tup.upd, out all, out added, out changed, out removed); + var next = new Update(all, added, changed, removed, tup.upd.key_extractor); + observer.OnNext(next); + filtered_cache = next; + } + }, + () => observer.OnCompleted()); + }); + } + + private static void apply_predicate(Func predicate, Update cache, + out IReadOnlyCollection all, out IEnumerable added, + out IEnumerable> changed, out IEnumerable removed) where TKey : IEquatable + { + var adds = new List(); + adds.AddRange(cache.added.Where(predicate)); + var rems = new List(); + rems.AddRange(cache.removed.Where(predicate)); + var diffs = new List>(); + foreach (var change in cache.changes) + { + if(!predicate(change.Old) && predicate(change.New)) + adds.Add(change.New); + else if(predicate(change.Old) && !predicate(change.New)) + rems.Add(change.Old); + else if(predicate(change.Old) && predicate(change.New)) + diffs.Add(change); + } + all = cache.all.Where(predicate).ToList(); + added = adds; + changed = diffs; + removed = rems; + } } } \ No newline at end of file diff --git a/src/client/spark_core/Co2/AtomicPriceSize.cs b/src/client/spark_core/Co2/AtomicPriceSize.cs new file mode 100644 index 000000000..2c6922602 --- /dev/null +++ b/src/client/spark_core/Co2/AtomicPriceSize.cs @@ -0,0 +1,42 @@ +using System; +using System.Threading; + +namespace mbts.co2 { + struct TimeStamp { + Int64 _value; + public UInt64 time => (UInt64)Interlocked.Read(ref _value); + public static UInt64 invalid_value => UInt64.MaxValue; + } + + // This type cannot be generic because we will get error CS0208 + //internal struct Optimistic where T : struct {... + internal struct AtomicPriceSize { + TimeStamp _time; + NativePriceSize _value; + void store(NativePriceSize nps) { + _time = new TimeStamp(); + _value = new NativePriceSize(); + throw new NotImplementedException(); + } + public (UInt64 time, NativePriceSize value) load() { + UInt64 temp; + NativePriceSize result; + int attempts = 0; + do { + ++attempts; + temp = _time.time; + result = _value; + } while ((temp != _time.time || temp == TimeStamp.invalid_value) && attempts < 1619200); + return (temp, result); + } + public bool load_if_new(ref UInt64 time, out NativePriceSize t) { + if (_time.time > time || _time.time == TimeStamp.invalid_value) { + (time, t) = load(); + return (time != TimeStamp.invalid_value); + } + t = NativePriceSize.NO_MARKET; + return false; + } + } + +} diff --git a/src/client/spark_core/Co2/Co2NativeMethods.cs b/src/client/spark_core/Co2/Co2NativeMethods.cs index 5d1be7e52..1d07490ef 100644 --- a/src/client/spark_core/Co2/Co2NativeMethods.cs +++ b/src/client/spark_core/Co2/Co2NativeMethods.cs @@ -56,8 +56,6 @@ namespace mbts.co2 [DllImport("libspark")] internal static extern void md_consumer_set_on_security(IntPtr consumer, raw_security_callback on_security); internal unsafe delegate void raw_just_security_callback(security_key key, byte* buffer, UInt64 buffer_size); - [DllImport("libspark")] - internal unsafe static extern void md_consumer_get_single_security(MDConsumerHandle.ColdEntry* entry, raw_just_security_callback on_security); [DllImport("libspark")] internal unsafe static extern bool md_consumer_is_heart_beating(MDConsumerHandle.ColdHeader* header); @@ -104,16 +102,34 @@ namespace mbts.co2 [DllImport("libspark")] internal static extern void co2_get_sample_cold_header(ref MDConsumerHandle.ColdHeader entry); + public class TestMDHandle : IDisposable { + IntPtr _ptr; + internal TestMDHandle(IntPtr p) { _ptr = p; } + public void Dispose() { + if (_ptr != IntPtr.Zero) { + co2_destroy_test_md(_ptr); + _ptr = IntPtr.Zero; + } + } + } + public static TestMDHandle produce_test_md() { + return new TestMDHandle(co2_produce_test_md()); + } [DllImport("libspark")] - internal static extern IntPtr co2_produce_test_md(); + private static extern IntPtr co2_produce_test_md(); [DllImport("libspark")] - internal static extern void co2_destroy_test_md(IntPtr handle); + private static extern void co2_destroy_test_md(IntPtr handle); [DllImport("libspark")] internal static extern void co2_get_zero_and_nan_book(ref NativePriceLevel nps); + [DllImport("libspark")] + internal static extern void co2_get_nan_price_size(out NativePriceSize ps); [DllImport("libspark")] internal static extern void co2_get_sample_datetime(ref utils.LLDateTime now, ref utils.LLDateTime utc_now); + [DllImport("libspark")] + internal static extern void co2_test_get_constants(out ulong max_depth, out ulong max_implied_depth, out sbyte nan_exponent); + } } diff --git a/src/client/spark_core/Co2/IntPrice.cs b/src/client/spark_core/Co2/IntPrice.cs new file mode 100644 index 000000000..989fa4063 --- /dev/null +++ b/src/client/spark_core/Co2/IntPrice.cs @@ -0,0 +1,58 @@ +using System; +using System.Runtime.InteropServices; + +namespace mbts.co2 { + + [StructLayout(LayoutKind.Explicit)] + public readonly struct IntPrice : IEquatable { + [FieldOffset(0)] + readonly long _mantissa; + [FieldOffset(7)] + readonly sbyte _exponent; + + public IntPrice(long mantissa, sbyte exponent) { + _mantissa = mantissa; + _exponent = exponent; + } + public long mantissa => NativePriceHelpers.decode(_mantissa); + public sbyte exponent => _exponent; + public bool is_nan => _exponent == NativePriceHelpers.COCONUT_NAN_EXPONENT; + public double price() { + return NativePriceHelpers.to_double(mantissa, exponent); + } + + public bool Equals(IntPrice other) { + return _exponent == other._exponent && _mantissa == other._mantissa; + } + public override bool Equals(object obj) { + return base.Equals(obj); + } + public static bool operator ==(IntPrice a, IntPrice b) { + return a.Equals(b); + } + public static bool operator !=(IntPrice a, IntPrice b) { + return !a.Equals(b); + } + public static bool operator < (IntPrice a, IntPrice b) { + if (a.exponent == b.exponent) { + return a.mantissa < b.mantissa; + } + return a.price() < b.price(); + } + public static bool operator > (IntPrice a, IntPrice b) { + if (a.exponent == b.exponent) { + return a.mantissa > b.mantissa; + } + return a.price() > b.price(); + } + public override string ToString() { + return $"{price():0.00}"; + } + public override int GetHashCode() { + return _mantissa.GetHashCode(); + } + + public static IntPrice NaN = new IntPrice(0,127); + } + +} diff --git a/src/client/spark_core/Co2/MDConsumerHandle.cs b/src/client/spark_core/Co2/MDConsumerHandle.cs index 1b2f77b64..a5454b6f2 100644 --- a/src/client/spark_core/Co2/MDConsumerHandle.cs +++ b/src/client/spark_core/Co2/MDConsumerHandle.cs @@ -1,4 +1,6 @@ -using bts.co2; +using bts; +using bts.api; +using bts.co2; using System; using System.Collections.Generic; using System.IO; @@ -8,11 +10,8 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using security_key = System.UInt32; -using bts.api; -using bts; -namespace mbts.co2 -{ +namespace mbts.co2 { unsafe class MDConsumerHandle { // Once is_initialized goes true it never goes false @@ -23,11 +22,6 @@ namespace mbts.co2 // The exchange key can change at CBOE. This numeric_id will always be correct public ulong numeric_id { get { return _cold->numeric_id; } } - public Security get_security() { - Security result = null; - Co2NativeMethods.md_consumer_get_single_security(_cold, (key, buffer, length) => result = ProtoBuf.Serializer.Deserialize(new UnmanagedMemoryStream(buffer, (long)length))); - return result; - } // This property exists for specific performance optimization purposes // Most callers should just use get_bbo/get_depth @@ -37,35 +31,97 @@ namespace mbts.co2 var updated = market_timestamp; if (updated > time && updated != TimeStamp.invalid_value) { if (has_depth) { - bid = _hot_depth->bid(0); - ask = _hot_depth->ask(0); + var bids = new NativePriceSize[1]; + var asks = new NativePriceSize[1]; + var result = get_depth(ref time, ref bids, ref asks, 1); + bid = bids[0]; + ask = asks[0]; } else { + Log.assert_d(false); bid = _hot->book.bid; ask = _hot->book.ask; } time = updated; return true; } - bid = ask = NativePriceSize.no_market; + bid = ask = NativePriceSize.NO_MARKET; return false; } - public void get_depth(ref NativePriceSize[] bids, ref NativePriceSize[] asks) { - ulong unused = 0; - get_depth(ref unused, ref bids, ref asks); + private unsafe static void copy_merged(in HotDepthEntry hot, uint count, NativePriceSize[] bid, NativePriceSize[] ask) { + Log.assert_d(bid.Length >= count); + Log.assert_d(ask.Length >= count); + uint xda = 0, xdb = 0, xia = 0, xib = 0; + if (hot.implied(xib).bid.is_nan) xib = NativePriceHelpers.COCONUT_MAX_IMPLIED_DEPTH; + if (hot.implied(xia).ask.is_nan) xia = NativePriceHelpers.COCONUT_MAX_IMPLIED_DEPTH; + for (uint x = 0; x < count; ++x) { + if (xib < NativePriceHelpers.COCONUT_MAX_IMPLIED_DEPTH) { + var direct = hot.direct(xdb).bid; + var implied = hot.implied(xib).bid; + + // Compare bids + if (direct.int_price == implied.int_price) { + bid[x] = new NativePriceSize(direct.int_price, direct.size, implied.size); + xdb += 1; + xib += 1; + } + // Bids grow down, take the higher price + else if (implied.is_nan || implied.int_price < direct.int_price) { + bid[x] = direct; + xdb += 1; + } + else { + bid[x] = implied; + xib += 1; + } + } + else { + bid[x] = hot.direct(xdb).bid; + xdb += 1; + } + + if (xia < NativePriceHelpers.COCONUT_MAX_IMPLIED_DEPTH) { + var direct = hot.direct(xda).ask; + var implied = hot.implied(xia).ask; + + // Compare asks + if (direct.int_price == implied.int_price) { + ask[x] = new NativePriceSize( direct.int_price, direct.size, implied.size); + xda += 1; + xia += 1; + } + // asks grow up, take the lower price + else if (implied.is_nan || direct.int_price < implied.int_price) { + ask[x] = direct; + xda += 1; + } + else { + ask[x] = implied; + xia += 1; + } + } + else { + ask[x] = hot.direct(xda).ask; + xda += 1; + } + } } - public bool get_depth(ref ulong time, ref NativePriceSize[] bids, ref NativePriceSize[] asks) { + + public bool get_depth(ref ulong time, ref NativePriceSize[] bids, ref NativePriceSize[] asks, uint depth) { if (!has_depth) { throw new InvalidOperationException("Cannot get depth data from a handle that does not offer depth!"); } var updated = market_timestamp; - if (updated > time && updated != UInt64.MaxValue) { - if (bids == null || bids.Length < 10) bids = new NativePriceSize[10]; - if (asks == null || asks.Length < 10) asks = new NativePriceSize[10]; - for (int depth = 0; depth < 10; ++depth) { - bids[depth] = _hot_depth->bid(depth); - asks[depth] = _hot_depth->ask(depth); + Log.assert(depth <= NativePriceHelpers.COCONUT_MAX_DEPTH); + if (updated > time && updated != TimeStamp.invalid_value) { + if (bids == null || bids.Length < depth) { + bids = new NativePriceSize[depth]; } + + if (asks == null || asks.Length < depth) { + asks = new NativePriceSize[depth]; + } + copy_merged(*_hot_depth, depth, bids, asks); time = updated; return true; } @@ -77,32 +133,26 @@ namespace mbts.co2 total_volume = _cold->total_volume; return (_cold->last_trade.load_if_new(ref time, out ps)); } - public ulong misc_timestamp { get { return _cold->misc_timestamp; } } + public ulong misc_timestamp { get { return _cold->misc_timestamp; } } - public MiscMessage get_misc() { - return new MiscMessage() { - SettlementPrice = _cold->settlement_price, - }; - } + public ulong total_volume { get { return _cold->total_volume; } } - public ulong total_volume { get { return _cold->total_volume; } } + public double settlement_price { get { return has_depth ? _hot_depth->settlement.price() : _hot->settlement.price(); } } + public DateTime settlement_date { get { return _cold->settlement_date; } } + public double previous_settlement_price { get { return _cold->previous_settlement_price.price(); } } + public DateTime previous_settlement_date { get { return _cold->previous_settlement_date; } } - public double settlement_price { get { return _cold->settlement_price;} } - public DateTime settlement_date { get { return _cold->settlement_date;} } - public double previous_settlement_price { get { return _cold->previous_settlement_price;} } - public DateTime previous_settlement_date { get { return _cold->previous_settlement_date;} } + public double open_price { get { return _cold->open_price; } } + public DateTime open_date { get { return _cold->open_date; } } - public double open_price { get { return _cold->open_price;} } - public DateTime open_date { get { return _cold->open_date; } } - - public double close_price { get { return _cold->close_price;} } - public DateTime close_date { get { return _cold->close_date; } } - public double session_high { get { return _cold->session_high;} } - public double session_low { get { return _cold->session_low; } } - public double open_interest { get { return _cold->open_interest; } } + public double close_price { get { return _cold->close_price; } } + public DateTime close_date { get { return _cold->close_date; } } + public double session_high { get { return _cold->session_high; } } + public double session_low { get { return _cold->session_low; } } + public double open_interest { get { return _cold->open_interest; } } // public double session_high_bid { get { return _cold->session_high_bid; } } // public double session_low_offer { get { return _cold->session_low_offer; } } - public double fixing_price { get { return _cold->fixing_price; } } + public double fixing_price { get { return _cold->fixing_price; } } // // Internal @@ -131,25 +181,25 @@ namespace mbts.co2 #region structures [StructLayout(LayoutKind.Sequential)] - internal struct HotEntry - { + internal struct HotEntry { // we don't actually need to decode timestamp. If it goes up data has changed public readonly TimeStamp timestamp; // 8 public readonly NativePriceLevel book; // 24 public readonly security_key key; // 4 public readonly UInt32 sequence; // 4 + public readonly IntPrice settlement; // 8 public readonly byte _status; // 1 public CoconutSecurityStatus status { get { return (CoconutSecurityStatus)_status; } } } [StructLayout(LayoutKind.Sequential)] - internal struct HotDepthEntry - { + internal struct HotDepthEntry { public readonly TimeStamp timestamp; // 8 public readonly security_key key; // 4 public readonly UInt32 sequence; // 4 - + public readonly IntPrice settlement; // 8 + // The ByValArray type would be more elegant but it does not allocate inline. // [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)] @@ -165,48 +215,38 @@ namespace mbts.co2 private readonly NativePriceLevel direct8; private readonly NativePriceLevel direct9; // 2 implied levels - private readonly NativePriceLevel implied0; - private readonly NativePriceLevel implied1; + private readonly ImpliedPriceLevel implied0; + private readonly ImpliedPriceLevel implied1; private readonly byte _status; public CoconutSecurityStatus status { get { return (CoconutSecurityStatus)_status; } } - public NativePriceSize bid(int depth) { + public NativePriceLevel direct(uint depth) { switch (depth) { - case 0: return direct0.bid; - case 1: return direct1.bid; - case 2: return direct2.bid; - case 3: return direct3.bid; - case 4: return direct4.bid; - case 5: return direct5.bid; - case 6: return direct6.bid; - case 7: return direct7.bid; - case 8: return direct8.bid; - case 9: return direct9.bid; + case 0: return direct0; + case 1: return direct1; + case 2: return direct2; + case 3: return direct3; + case 4: return direct4; + case 5: return direct5; + case 6: return direct6; + case 7: return direct7; + case 8: return direct8; + case 9: return direct9; default: throw new ArgumentException("depth"); } } - public NativePriceSize ask(int depth) { + public ImpliedPriceLevel implied(uint depth) { switch (depth) { - case 0: return direct0.ask; - case 1: return direct1.ask; - case 2: return direct2.ask; - case 3: return direct3.ask; - case 4: return direct4.ask; - case 5: return direct5.ask; - case 6: return direct6.ask; - case 7: return direct7.ask; - case 8: return direct8.ask; - case 9: return direct9.ask; + case 0: return implied0; + case 1: return implied1; default: throw new ArgumentException("depth"); } } - } - [StructLayout(LayoutKind.Sequential, Size = 392)] - internal struct ColdEntry - { + [StructLayout(LayoutKind.Sequential, Size = 384)] + internal struct ColdEntry { public int exchange_status; // Last trade public AtomicPriceSize last_trade; @@ -215,9 +255,8 @@ namespace mbts.co2 //// Settlement public UInt64 misc_timestamp; - public double settlement_price; public utils.LLDate settlement_date; - public double previous_settlement_price; + public IntPrice previous_settlement_price; public utils.LLDate previous_settlement_date; //// Opening/closing @@ -242,8 +281,7 @@ namespace mbts.co2 } [StructLayout(LayoutKind.Sequential)] - internal struct ColdHeader - { + internal struct ColdHeader { internal uint version; internal ulong count; internal ulong segment_count; diff --git a/src/client/spark_core/Co2/NativePriceHelpers.cs b/src/client/spark_core/Co2/NativePriceHelpers.cs new file mode 100644 index 000000000..40fc131ed --- /dev/null +++ b/src/client/spark_core/Co2/NativePriceHelpers.cs @@ -0,0 +1,43 @@ +using bts; + +namespace mbts.co2 { + public static class NativePriceHelpers { + static double[] s_powers10 = { + 1E-20, 1E-19, 1E-18, 1E-17, 1E-16, 1E-15, 1E-14, 1E-13, 1E-12, 1E-11, 1E-10, 1E-9, 1E-8, 1E-7, 1E-6, 1E-5, 1E-4, 1E-3, 1E-2, 1E-1, + 1E0, + 1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8, 1E9, 1E10, 1E11, 1E12, 1E13, 1E14, 1E15, 1E16, 1E17, 1E18, 1E19, 1E20 + }; + static int s_max_abs_pow10 = s_powers10.Length/2; + + public static double pow10(int power) { + int offset = power + s_max_abs_pow10; + Log.assert_d(offset >= 0 && offset < s_powers10.Length); + if (offset >= 0 && offset < s_powers10.Length) { + return s_powers10[offset]; + } + return double.NaN; + } + internal static long decode(long m) { + long mantissa = m & 0xFFFFFFFFFFFFFF; + // If the 56th bit is set this is negative + const long bit56 = 0x80000000000000; + if ((mantissa & bit56) != 0) { + // Compute 2's compliment manually + mantissa = -1 * (((~mantissa) & (bit56 - 1)) + 1); + } + return mantissa; + } + + public static double to_double(long mantissa, sbyte exponent) { + if (exponent == COCONUT_NAN_EXPONENT) { + return double.NaN; + } + return mantissa * pow10(exponent); + } + + public const uint COCONUT_MAX_DEPTH = 10; + public const uint COCONUT_MAX_IMPLIED_DEPTH = 2; + internal const sbyte COCONUT_NAN_EXPONENT = 127; + } + +} diff --git a/src/client/spark_core/Co2/PriceSize.cs b/src/client/spark_core/Co2/PriceSize.cs index 8f1e14e9f..3b91b089c 100644 --- a/src/client/spark_core/Co2/PriceSize.cs +++ b/src/client/spark_core/Co2/PriceSize.cs @@ -1,107 +1,110 @@ using bts; using System; -using System.Collections.Generic; -using System.Linq; using System.Runtime.InteropServices; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -namespace mbts.co2 -{ +namespace mbts.co2 { - [StructLayout(LayoutKind.Explicit)] + [StructLayout(LayoutKind.Sequential)] public readonly struct NativePriceLevel { - [FieldOffset(0)] - readonly Int64 _mantissa_b; // : 56; - [FieldOffset(7)] - readonly sbyte _exponent_b; // : 8; - [FieldOffset(8)] - readonly Int64 _mantissa_a; // : 56; - [FieldOffset(15)] - readonly sbyte _exponent_a; //: 8; - [FieldOffset(16)] - readonly UInt32 _size_b; - [FieldOffset(20)] - readonly UInt32 _size_a; - - long decode(long m) { - long mantissa = m & 0xFFFFFFFFFFFFFF; - // If the 56th bit is set this is negative - const long bit56 = 0x80000000000000; - if ((mantissa & bit56) != 0) { - // Compute 2's compliment manually - mantissa = -1 * (((~mantissa) & (bit56 - 1)) + 1); - } - return mantissa; + readonly IntPrice _bid; + readonly IntPrice _ask; + readonly uint _bid_size; + readonly uint _ask_size; + public NativePriceLevel(IntPrice bid, uint bid_size, IntPrice ask, uint ask_size) { + _bid = bid; + _bid_size = bid_size; + _ask = ask; + _ask_size = ask_size; } - public Int64 bid_mantissa => decode(_mantissa_b); - public sbyte bid_exponent => _exponent_b; - public Int64 ask_mantissa => decode(_mantissa_a); - public sbyte ask_exponent => _exponent_a; - public UInt32 bid_size => _size_b; - public UInt32 ask_size => _size_a; + public NativePriceSize bid => new NativePriceSize(_bid, _bid_size); + public NativePriceSize ask => new NativePriceSize(_ask, _ask_size); - public NativePriceSize bid => new NativePriceSize(bid_mantissa, bid_exponent, bid_size); - public NativePriceSize ask => new NativePriceSize(ask_mantissa, ask_exponent, ask_size); - } + public void Deconstruct(out NativePriceSize bid, out NativePriceSize ask) { + bid = this.bid; + ask = this.ask; + } - public static class NativePriceSizeHelpers { - static double[] s_powers10 = { - 1E-20, 1E-19, 1E-18, 1E-17, 1E-16, 1E-15, 1E-14, 1E-13, 1E-12, 1E-11, 1E-10, 1E-9, 1E-8, 1E-7, 1E-6, 1E-5, 1E-4, 1E-3, 1E-2, 1E-1, - 1E0, - 1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8, 1E9, 1E10, 1E11, 1E12, 1E13, 1E14, 1E15, 1E16, 1E17, 1E18, 1E19, 1E20 - }; - static int s_max_abs_pow10 = s_powers10.Length/2; - - public static double pow10(int power) { - int offset = power + s_max_abs_pow10; - Log.assert_d(offset >= 0 && offset < s_powers10.Length); - if (offset >= 0 && offset < s_powers10.Length) { - return s_powers10[offset]; - } - return double.NaN; + internal static NativePriceLevel empty => new NativePriceLevel(IntPrice.NaN, 0, IntPrice.NaN, 0); + public override string ToString() { + return $"b [{bid.price} {bid.size}] a [{ask.price} {ask.size}]"; + } + } + public readonly struct ImpliedPriceLevel { + readonly IntPrice _bid; + readonly IntPrice _ask; + readonly uint _bid_size; + readonly uint _ask_size; + public ImpliedPriceLevel(IntPrice bid, uint bid_size, IntPrice ask, uint ask_size) { + _bid = bid; + _bid_size = bid_size; + _ask = ask; + _ask_size = ask_size; + } + public NativePriceSize bid => new NativePriceSize(_bid, 0, _bid_size); + public NativePriceSize ask => new NativePriceSize(_ask, 0, _ask_size); + public void Deconstruct(out NativePriceSize bid, out NativePriceSize ask) { + bid = this.bid; + ask = this.ask; } } - public readonly struct NativePriceSize : IEquatable { - public NativePriceSize(Int64 mantissa, sbyte exponent, uint size) { - this.mantissa = mantissa; - this.exponent = exponent; - this.size = size; + readonly IntPrice _price; + readonly uint _size; + readonly uint _direct_size; + + static NativePriceSize() { + Log.assert(Marshal.SizeOf() == 16); } - public static NativePriceSize no_market = new NativePriceSize(0, 0, 0); + public NativePriceSize(IntPrice price, uint size) { + _price = price; + _size = size; + _direct_size = size; + } + public NativePriceSize(long mantissa, sbyte exponent, uint size) { + _price = new IntPrice(mantissa, exponent); + _size = size; + _direct_size = size; + } + public NativePriceSize(IntPrice price, uint direct_size, uint implied_size) { + _price = price; + _size = direct_size + implied_size; + _direct_size = direct_size; + } - public Int64 mantissa { get; } - public sbyte exponent {get;} + public static NativePriceSize NO_MARKET = new NativePriceSize(IntPrice.NaN, 0); - public double price => (is_nan) ? double.NaN : mantissa * NativePriceSizeHelpers.pow10(exponent); + public IntPrice int_price => _price; + public long mantissa => _price.mantissa; + public sbyte exponent => _price.exponent; + public uint size => _size; + public uint direct_size => _direct_size; + public uint implied_size => _size - _direct_size; + public double price => _price.price(); + public bool is_nan => _price.is_nan; - public bool is_nan => this == no_market; - - public uint size { get; } public override string ToString() { - return "{0:0.00} {1}".fmt(price, size); + return "{0} {1} ({2})".fmt(price, size, implied_size); } public override bool Equals(object obj) { if (obj is NativePriceSize p) return Equals(p); return base.Equals(obj); } + public override int GetHashCode() { - return bts.utils.EqualityComparer.aggregate_hash(mantissa.GetHashCode(), exponent.GetHashCode(), size.GetHashCode()); + return bts.utils.EqualityComparer.aggregate_hash(_price.GetHashCode(), size.GetHashCode(), _direct_size.GetHashCode()); } - public bool Equals(NativePriceSize other) { - return size == other.size && - mantissa == other.mantissa && - exponent == other.exponent; - } + public bool Equals(NativePriceSize other) => + size == other.size && + _direct_size == other._direct_size && + _price.Equals(other._price); + public static bool operator ==(NativePriceSize a, NativePriceSize b) { return a.Equals(b); } @@ -109,86 +112,5 @@ namespace mbts.co2 return !a.Equals(b); } } - - struct TimeStamp { - Int64 _value; - public UInt64 time => (UInt64)Interlocked.Read(ref _value); - public static UInt64 invalid_value => UInt64.MaxValue; - } - - // This type cannot be generic because we will get error CS0208 - //internal struct Optimistic where T : struct {... - internal struct AtomicPriceSize { - TimeStamp _time; - NativePriceSize _value; - void store(NativePriceSize nps) { - _time = new TimeStamp(); - _value = new NativePriceSize(); - throw new NotImplementedException(); - } - public (UInt64 time, NativePriceSize value) load() { - UInt64 temp; - NativePriceSize result; - int attempts = 0; - do { - ++attempts; - temp = _time.time; - result = _value; - } while ((temp != _time.time || temp == TimeStamp.invalid_value) && attempts < 1619200); - return (temp, result); - } - public bool load_if_new(ref UInt64 time, out NativePriceSize t) { - if (_time.time > time || _time.time == TimeStamp.invalid_value) { - (time, t) = load(); - return (time != TimeStamp.invalid_value); - } - t = NativePriceSize.no_market; - return false; - } - } - - public struct PriceSize : IEquatable - { - public PriceSize(double price, uint size) - { - this.price = price; - this.size = size; - } - - public PriceSize(NativePriceSize nps) { - price = nps.price; - size = nps.size; - } - - public static implicit operator PriceSize(NativePriceSize nps) { - return new PriceSize(nps); - } - - public static PriceSize no_market = NativePriceSize.no_market; - - public double price { get; } - public uint size { get; } - - public override string ToString() { - return "{0:0.00} {1}".fmt(price, size); - } - - public override bool Equals(object obj) { - if (obj is PriceSize p) return Equals(p); - return base.Equals(obj); - } - public override int GetHashCode() { - return bts.utils.EqualityComparer.aggregate_hash(price.GetHashCode(), size.GetHashCode()); - } - - public bool Equals(PriceSize other) { - return price == other.price && size == other.size; - } - public static bool operator ==(PriceSize a, PriceSize b) { - return a.Equals(b); - } - public static bool operator !=(PriceSize a, PriceSize b) { - return !a.Equals(b); - } - } + } diff --git a/src/client/spark_core/TestRunner/TestAssert.cs b/src/client/spark_core/TestRunner/TestAssert.cs index 9351f1d79..5bc09ae02 100644 --- a/src/client/spark_core/TestRunner/TestAssert.cs +++ b/src/client/spark_core/TestRunner/TestAssert.cs @@ -29,6 +29,11 @@ namespace bts.test { throw new AssertFailture($"Expected: 'false' got 'true'. {msg}", file, line); } } + public static void IsNaN(double value, string msg = "", [CallerFilePath] string file = "", [CallerLineNumber] int line = 0) { + if (!double.IsNaN(value)) { + throw new AssertFailture($"Expected: NaN got {value}", file, line); + } + } public static void AreEqual(T a, T b, string msg="", [CallerFilePath] string file="", [CallerLineNumber] int line=0) { if (!a.Equals(b)) { throw new AssertFailture( diff --git a/src/client/spark_core/TestRunner/TestResultReceiver.cs b/src/client/spark_core/TestRunner/TestResultReceiver.cs index 72a052c84..9063a0c59 100644 --- a/src/client/spark_core/TestRunner/TestResultReceiver.cs +++ b/src/client/spark_core/TestRunner/TestResultReceiver.cs @@ -50,7 +50,9 @@ namespace bts.test { var unused = wr.GetResponseAsync(); } + public void print_result(TestResult result) { + ResultCount += 1; if (_defer) { _deferred.Add(result); return; @@ -93,6 +95,10 @@ namespace bts.test { _fail++; ConsoleEx.Write("TIMEOUT", ConsoleColor.DarkYellow); break; + case TestResult.Types.Step.Crash: + _fail++; + ConsoleEx.Write("CRASH", ConsoleColor.Red); + break; } Console.WriteLine(); } @@ -112,6 +118,7 @@ namespace bts.test { } public int ErrorCount => _fail; + public int ResultCount { get; set; } public void Dispose() { _running = false; diff --git a/src/client/spark_core/TestRunner/TestRunner.cs b/src/client/spark_core/TestRunner/TestRunner.cs index be3f09bb4..9a1e6f436 100644 --- a/src/client/spark_core/TestRunner/TestRunner.cs +++ b/src/client/spark_core/TestRunner/TestRunner.cs @@ -21,6 +21,7 @@ namespace bts.test { var listening = trr.listen(); trr.print_begin(); foreach (var t in tests) { + int count = trr.ResultCount; var res = run_one(t, TimeSpan.FromSeconds(timeout)); if (res == TIMEOUT) { trr.print_result(new TestResult() { @@ -28,6 +29,15 @@ namespace bts.test { Status = TestResult.Types.Step.Timeout }); } + else { + // If we didn't time out and no results were reported we crashed or something + if (count == trr.ResultCount) { + trr.print_result(new TestResult() { + TestName = t, + Status = TestResult.Types.Step.Crash + }); + } + } } trr.print_summary(); return trr.ErrorCount; diff --git a/src/client/spark_core/utils/EqualityComparer.cs b/src/client/spark_core/utils/EqualityComparer.cs index 21710ba6e..2fc8bd237 100644 --- a/src/client/spark_core/utils/EqualityComparer.cs +++ b/src/client/spark_core/utils/EqualityComparer.cs @@ -27,6 +27,9 @@ namespace bts.utils { public static IEqualityComparer CreateNullable(Func equals) where T : struct => new AnonymousEqualityComparer(nullable_equals); + public static IEqualityComparer null_check_wrapper(IEqualityComparer comparer) where T : class => + new AnonymousEqualityComparer((a, b) => NullCheckEquals(a, b, comparer.Equals), comparer.GetHashCode); + public static bool safe_sequence_equals(IEnumerable source, IEnumerable other) => NullCheckEquals(source, other, (a, b) => a.SequenceEqual(b)); diff --git a/src/client/spark_core/utils/GlobalExtensions.cs b/src/client/spark_core/utils/GlobalExtensions.cs index 8a094fdb6..5948cb21d 100644 --- a/src/client/spark_core/utils/GlobalExtensions.cs +++ b/src/client/spark_core/utils/GlobalExtensions.cs @@ -1,4 +1,5 @@ -using System; +using bts; +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -32,5 +33,39 @@ namespace mbts { const int kind_shift = 62; return new DateTime((long)(ll & ticks_mask), (DateTimeKind)(ll >> kind_shift)); } + + //this overloaded declaration, (as opposed to one function with comp = null) allows observable_collection.sync_with to be passed into functions as a parameterless Action + public static void sync_with(this IList dest, IEnumerable src) { dest.sync_with(src, EqualityComparer.Default); } + public static void sync_with(this IList dest, IEnumerable src, IEqualityComparer comp) { + Log.notnull(dest); + if (src == null) { + dest.Clear(); + return; + } + var srcList = src as IList ?? src.ToList(); + var changes = bts.utils.Diff.diff(dest, srcList, comp).ToList(); + // Apply the diff + for(int i = 0; i < changes.Count; ++i) { + var c = changes[i]; + // removes + for (var x = 0; x < c.deletedLeft; ++x) { + dest.RemoveAt(c.StartLeft); + } + // inserts + for (var x = 0; x < c.insertedRight; ++x) { + dest.Insert(c.StartLeft + x, srcList[c.StartRight + x]); + } + // Adjust start index of remaining changes + for (int ii = i + 1; ii < changes.Count; ++ii) { + var cc = changes[ii]; + var delta = c.insertedRight - c.deletedLeft; + if (cc.StartLeft >= c.StartLeft) { + cc.StartLeft += delta; + changes[ii] = cc;//remember: struct + } + } + } + } + } } \ No newline at end of file diff --git a/src/client/spark_sdk/samples/MarketDataClient.cs b/src/client/spark_sdk/samples/MarketDataClient.cs index 82a787022..19d1442bc 100644 --- a/src/client/spark_sdk/samples/MarketDataClient.cs +++ b/src/client/spark_sdk/samples/MarketDataClient.cs @@ -22,6 +22,13 @@ namespace bts.sdk public long last_update_time; public ChannelUpdate data; } + + const UInt32 _all_flags = (uint)( + MDRequest.Types.Flags.Security | + MDRequest.Types.Flags.Bbo | + MDRequest.Types.Flags.Direct | + MDRequest.Types.Flags.Implied | + MDRequest.Types.Flags.LastTrade); readonly ConcurrentDictionary _data = new ConcurrentDictionary(); readonly static Lazy _md = new Lazy(() => locate(), true); @@ -63,8 +70,9 @@ namespace bts.sdk public async Task find_security_async(Func filter, TimeSpan? timeout = null) { - if(!timeout.HasValue) + if(!timeout.HasValue) { timeout = TimeSpan.FromMilliseconds(int.MaxValue-1); + } CancellationTokenSource src = new CancellationTokenSource(timeout.Value); Security security = await Task.Run(async () => { @@ -85,10 +93,7 @@ namespace bts.sdk public IObservable subscribe(Product product, TimeSpan? update_interval = null, IScheduler scheduler = null) { - return subscribe(product, - (System.UInt32)(MDRequest.Types.Flags.Security | MDRequest.Types.Flags.Bbo | - MDRequest.Types.Flags.Depth | MDRequest.Types.Flags.LastTrade), - update_interval, scheduler); + return subscribe(product, _all_flags, update_interval, scheduler); } @@ -135,10 +140,7 @@ namespace bts.sdk public IObservable subscribe(Security security, TimeSpan? update_interval = null, IScheduler scheduler = null) { - return subscribe(security, - (System.UInt32)(MDRequest.Types.Flags.Security | MDRequest.Types.Flags.Bbo | - MDRequest.Types.Flags.Depth | MDRequest.Types.Flags.LastTrade), - update_interval, scheduler); + return subscribe(security, _all_flags, update_interval, scheduler); } /// @@ -215,8 +217,8 @@ namespace bts.sdk if (_data.TryGetValue(update.Security.Id, out data)) { //mergefrom concats repeated fields - if (update.Book != null) - data.data.Book = null; + if (update.Direct != null) + data.data.Direct = null; //this can tear, but the book won't tear, so maybe it's ok data.data.MergeFrom(update); data.last_update_time = now; diff --git a/src/client/spark_test/Co2/Co2ImpliedTests.cs b/src/client/spark_test/Co2/Co2ImpliedTests.cs new file mode 100644 index 000000000..5b9b22cee --- /dev/null +++ b/src/client/spark_test/Co2/Co2ImpliedTests.cs @@ -0,0 +1,18 @@ +using bts.test; +using mbts.co2; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading.Tasks; + +namespace bts.test { + + [TestGroup] + class Co2ImpliedTests { + [Test] + public void co2_test_implied_merge() { + } + } +} diff --git a/src/client/spark_test/Co2/Co2Tests.cs b/src/client/spark_test/Co2/Co2Tests.cs index d9a3640bf..a1e17e81f 100644 --- a/src/client/spark_test/Co2/Co2Tests.cs +++ b/src/client/spark_test/Co2/Co2Tests.cs @@ -43,9 +43,15 @@ namespace mbts.co2.tests Assert.AreEqual(ps.price, 2030); Assert.IsTrue(ps.size == 1); - ps = NativePriceSize.no_market; + ps = NativePriceSize.NO_MARKET; Assert.IsTrue(double.IsNaN(ps.price)); Assert.IsTrue(ps.size == 0); + + ps = new NativePriceSize(100, 100, 100); + Co2NativeTestMethods.co2_get_nan_price_size(out ps); + Assert.IsTrue(double.IsNaN(ps.price)); + Assert.AreEqual(ps.size, 0u); + Assert.AreEqual(ps.exponent, NativePriceSize.NO_MARKET.exponent); } [Test] @@ -66,6 +72,7 @@ namespace mbts.co2.tests Assert.IsTrue(entry.status == CoconutSecurityStatus.PacketLoss); Assert.IsTrue(entry.key == 1234); Assert.IsTrue(entry.sequence == 777); + Assert.AreEqual(entry.settlement.price(), 98.6); } string fmt(double a) { @@ -80,13 +87,19 @@ namespace mbts.co2.tests Assert.IsTrue(entry_size == (ulong)Marshal.SizeOf(entry)); Assert.IsTrue(entry.status == CoconutSecurityStatus.PacketLoss); Assert.IsTrue(entry.sequence == 888); - for (int x = 0; x < 10; ++x) { - var bid = entry.bid(x); + Assert.AreEqual(entry.settlement.price(), 98.6); + for (uint x = 0; x < NativePriceHelpers.COCONUT_MAX_DEPTH; ++x) { + (var bid, var ask) = entry.direct(x); Assert.IsTrue(bid.price == x + 0.5); Assert.IsTrue(bid.size == x + 1); - var ask = entry.ask(x); Assert.AreEqual(fmt(ask.price), fmt(x+0.6)); - //Assert.IsTrue(ask.price == x + 0.6); + Assert.IsTrue(ask.size == x + 2); + } + for (uint x = 0; x < NativePriceHelpers.COCONUT_MAX_IMPLIED_DEPTH; ++x) { + (var bid, var ask) = entry.implied(x); + Assert.AreEqual(77, (bid.price)); + Assert.IsTrue(bid.size == x + 1); + Assert.AreEqual(88, ask.price); Assert.IsTrue(ask.size == x + 2); } } @@ -114,9 +127,8 @@ namespace mbts.co2.tests Assert.IsTrue(entry.total_volume == 12345); Assert.IsTrue(compare_date(entry.last_trade_time, DateTime.UtcNow)); //// Settlement - Assert.AreEqual(entry.settlement_price, 3.3); Assert.IsTrue(entry.settlement_date == new DateTime(2015, 9, 3)); - Assert.AreEqual(entry.previous_settlement_price, 3.3); + Assert.AreEqual(entry.previous_settlement_price.price(), 3.3); Assert.IsTrue(entry.previous_settlement_date == new DateTime(2015, 9, 2)); //// Open/close Assert.IsTrue(entry.open_price == 4.4); @@ -151,21 +163,19 @@ namespace mbts.co2.tests Assert.IsTrue(header.gap_count == 104); } - [Test,Ignore("")] + [Test] public void enumerate_securities() { - // config.Configuration.initialize_for_test(); - IntPtr test_md = Co2NativeTestMethods.co2_produce_test_md(); - using (Disposable.Create(() => Co2NativeTestMethods.co2_destroy_test_md(test_md))) { + using (var test_md = Co2NativeTestMethods.produce_test_md()) { using (var consumer = new MDConsumer(new[] { - new UserSymbol("FOOM", Venue.Unset, SecurityType.Future), - new UserSymbol("FOOXM", Venue.Unset, SecurityType.Option) + new UserSymbol("FOOM", Venue.Bts, SecurityType.Future), + new UserSymbol("FOOXM", Venue.Bts, SecurityType.Option), }, false)) { var securities = consumer.all_securities().ToList(); foreach (var s in securities) { Assert.IsTrue(s.security.Id == s.handle.key); //Assert.IsTrue(s.security.numeric_id == s.handle.numeric_id); } - Assert.IsTrue(securities.Count() == 24); + Assert.AreEqual(securities.Count(), 24); var dict = securities.Select(s => s.security).GroupBy(s => s.Symbol).ToDictionary(group => group.Key, group => group.ToList()); Assert.IsTrue(dict.Count == 2); Assert.IsTrue(dict.All(kvp => kvp.Value.Count == 12)); @@ -173,16 +183,14 @@ namespace mbts.co2.tests } } - [Test,Ignore("")] + [Test] public void on_securities() { - config.Configuration.initialize_for_test(); - IntPtr test_md = Co2NativeTestMethods.co2_produce_test_md(); - using (Disposable.Create(() => Co2NativeTestMethods.co2_destroy_test_md(test_md))) { + using (var test_md = Co2NativeTestMethods.produce_test_md()) { using (var consumer = new MDConsumer(new[] { - new UserSymbol("FOOM", Venue.Unset, SecurityType.Future), - new UserSymbol("FOOXM", Venue.Unset, SecurityType.Option) + new UserSymbol("FOOM", Venue.Bts, SecurityType.Future), + new UserSymbol("FOOXM", Venue.Bts, SecurityType.Option) }, false)) { - var securities = new List(); + var securities = new List(); consumer.on_security.Subscribe(secs => { foreach (var s in secs) { Assert.IsTrue(s.security.Id == s.handle.key); @@ -198,11 +206,10 @@ namespace mbts.co2.tests foreach (var sec in securities) { var handle = consumer.try_get_handle(sec.Id); Assert.IsNotNull(handle); - Assert.IsTrue(handle.get_security().Id == sec.Id); Assert.AreEqual(sec.Id, handle.key); Assert.IsTrue(handle.market_timestamp > 0); ulong time = 0; - NativePriceSize bid,ask; + NativePriceSize bid, ask; handle.get_bbo(ref time, out bid, out ask); Assert.IsTrue(bid.price == 1.1); Assert.IsTrue(bid.size == 101); @@ -218,7 +225,7 @@ namespace mbts.co2.tests Assert.IsTrue(trade_date.Year == 2015 && trade_date.Month == 9 && trade_date.Day == 22); Assert.IsTrue(total_volume == 4200); Assert.IsTrue(handle.settlement_date.Year == 2000 && handle.settlement_date.Day == 9 && handle.settlement_date.Month == 8); - Assert.IsTrue(handle.settlement_price == 6.6); + Assert.AreEqual(handle.settlement_price, 6.6); Assert.IsTrue(handle.open_price == 7.7); Assert.IsTrue(handle.open_date.Year == 2001 && handle.open_date.Month == 1 && handle.open_date.Day == 2); Assert.IsTrue(handle.close_price == 8.8); @@ -237,20 +244,28 @@ namespace mbts.co2.tests [Test] public void price_size_nan() { - NativePriceLevel npl = new NativePriceLevel(); + NativePriceLevel npl = NativePriceLevel.empty; Assert.IsTrue(npl.bid.is_nan); - Assert.IsTrue(double.IsNaN(npl.bid.price)); + Assert.IsNaN(npl.bid.price); Assert.IsTrue(npl.ask.is_nan); - Assert.IsTrue(double.IsNaN(npl.ask.price)); + Assert.IsNaN(npl.ask.price); Co2NativeTestMethods.co2_get_zero_and_nan_book(ref npl); Assert.AreEqual(npl.ask.price, 0); Assert.AreEqual(npl.ask.size, 1u); Assert.AreEqual(npl.bid.size, 0u); Assert.IsTrue(npl.bid.is_nan); - Assert.IsTrue(double.IsNaN(npl.bid.price)); + Assert.IsNaN(npl.bid.price); } + [Test] + public void co2_constants_test() { + Co2NativeTestMethods.co2_test_get_constants(out ulong max_depth, out ulong max_implied_depth, out sbyte nan); + Assert.AreEqual(max_depth, NativePriceHelpers.COCONUT_MAX_DEPTH); + Assert.AreEqual(max_implied_depth, NativePriceHelpers.COCONUT_MAX_IMPLIED_DEPTH); + Assert.AreEqual(nan, NativePriceHelpers.COCONUT_NAN_EXPONENT); + } + [Test] public void lldatetime_test() { utils.LLDateTime now = DateTime.MinValue, utc_now = DateTime.MinValue; @@ -271,5 +286,7 @@ namespace mbts.co2.tests assert_about_equal(dt_utc_now.Minute, DateTime.UtcNow.Minute); assert_about_equal(dt_utc_now.Second, DateTime.UtcNow.Second); } + + } } diff --git a/src/client/spark/Tests/mcore/SyncronizeCollectionTests.cs b/src/client/spark_test/Collections/SyncronizeCollectionTests.cs similarity index 96% rename from src/client/spark/Tests/mcore/SyncronizeCollectionTests.cs rename to src/client/spark_test/Collections/SyncronizeCollectionTests.cs index 5eb1d2d79..7389f39e0 100644 --- a/src/client/spark/Tests/mcore/SyncronizeCollectionTests.cs +++ b/src/client/spark_test/Collections/SyncronizeCollectionTests.cs @@ -1,5 +1,4 @@ -using NUnit.Framework; -using System; +using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; @@ -8,10 +7,11 @@ using System.Text; using System.Threading.Tasks; using mbts; using System.Threading; +using bts.test; namespace bts.mcore { - [TestFixture] + [TestGroup] class SyncronizeCollectionTests { private void swap(List list, int a, int b) { diff --git a/src/client/spark/Tests/mcore/SecurityTests.cs b/src/client/spark_test/Security/SecurityTests.cs similarity index 73% rename from src/client/spark/Tests/mcore/SecurityTests.cs rename to src/client/spark_test/Security/SecurityTests.cs index 041b0f644..63104702a 100644 --- a/src/client/spark/Tests/mcore/SecurityTests.cs +++ b/src/client/spark_test/Security/SecurityTests.cs @@ -1,14 +1,14 @@ -using NUnit.Framework; -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using bts.api; +using bts.test; namespace bts.mcore { - [TestFixture] + [TestGroup] class SecurityTests { [Test] public void test_security_equal() { @@ -16,10 +16,10 @@ namespace bts.mcore SecurityImmutable b = new Security() { Id = 42 }; SecurityImmutable c = new Security() { Id = 4 }; - Assert.True(a == b); - Assert.True(a.Equals(b)); - Assert.False(a == c); - Assert.False(a.Equals(c)); + Assert.IsTrue(a == b); + Assert.IsTrue(a.Equals(b)); + Assert.IsFalse(a == c); + Assert.IsFalse(a.Equals(c)); Dictionary dict = new Dictionary(); dict[a] = 1; diff --git a/src/client/spark/Tests/MakePublic.cs b/src/client/spark_test/Utils/MakePublic.cs similarity index 100% rename from src/client/spark/Tests/MakePublic.cs rename to src/client/spark_test/Utils/MakePublic.cs diff --git a/src/client/spark_test/api/ApiUpdateTests.cs b/src/client/spark_test/api/ApiUpdateTests.cs index dd65b8edb..791d9a773 100644 --- a/src/client/spark_test/api/ApiUpdateTests.cs +++ b/src/client/spark_test/api/ApiUpdateTests.cs @@ -6,6 +6,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using System.Linq; using System.Reactive.Concurrency; +using System.Reactive.Subjects; namespace mbts.api.Test { [TestGroup] @@ -104,6 +105,121 @@ namespace mbts.api.Test { } + void validate_filter(IObservable> raw, IObservable> filtered, Func pred) + { + var total = raw.ToEnumerable().Count(); + + // Validate that update is right at each point + for (var count = 1; count < total; ++count) + { + var orig = raw.Take(count).reconstitute(id).ToEnumerable().Last(); + var next = filtered.Take(count).ToEnumerable().Last(); + Assert.That(next.all.All(pred)); + Assert.That(next.added.All(pred)); + Assert.That(next.removed.All(pred)); + Assert.That(next.changed.All(pred)); + } + + // Do it again with TableUpdate + for (var count = 1; count < total; ++count) + { + var orig = raw.Take(count).reconstitute(id).Table().ToEnumerable().Last(); + var next = filtered.Take(count).ToEnumerable().Last(); + Assert.That(next.added.All(pred)); + Assert.That(next.removed.All(pred)); + Assert.That(next.changed.All(pred)); + } + } + + + void validate_filter_sub(IObservable> raw, IObservable> filtered, BehaviorSubject> pred) + { + raw.reconstitute(x => x.id).CombineLatest(filtered, (r, f) => Tuple.Create(r, f)).Subscribe(tup => + { + Assert.That(tup.Item2.all.All(pred.Value)); + Assert.That(tup.Item2.added.All(pred.Value)); + Assert.That(!tup.Item2.removed.Any(pred.Value)); + Assert.That(tup.Item2.changed.All(pred.Value)); + + Dictionary rawd = tup.Item1.added.ToDictionary(x => x.id); + Dictionary fd = tup.Item2.added.ToDictionary(x => x.id); + foreach (var kv in rawd) + { + if(pred.Value(kv.Value)) + Assert.That(fd.ContainsKey(kv.Key)); + else + Assert.That(!fd.ContainsKey(kv.Key)); + } + }); + } + + + [Test] + public void test_filter() + { + var raw = Observable.Create>(observer => + { + RawUpdate rup = new RawUpdate(); + for (int x = 0; x < 50; ++x) + { + rup.Clear(); + var item = _item_pool[x]; + rup.added_items.Add(item); + rup.added_keys.Add(item.id); + observer.OnNext(rup); + } + var rand = new Random(); + for (int r = 0; r < rand.Next(4, 30); r += rand.Next(1, 2)) + { + rup.Clear(); + rup.removed_keys.Add(_item_pool[r].id); + observer.OnNext(rup); + } + rup.Clear(); + for (int c = 31; c < rand.Next(32, 50); c += rand.Next(1, 2)) + { + var ch = new Item(_item_pool[c].id, _item_pool[c].name + "_change"); + rup.changed_items.Add(ch); + rup.changed_keys.Add(ch.id); + } + observer.OnNext(rup); + + observer.OnCompleted(); + return Disposable.empty; + }); + + Func evens = (x => x.id % 2 == 0); + Func odds = (x => x.id % 2 == 1); + var predeven = Observable.Create>(observer => + { + observer.OnNext(evens); + observer.OnCompleted(); + return Disposable.empty; + }); + + var filtered = raw.reconstitute(x=> x.id).Filter(predeven); + validate_filter(raw, filtered, evens); + + var predodd = Observable.Create>(observer => + { + observer.OnNext(odds); + observer.OnCompleted(); + return Disposable.empty; + }); + + filtered = raw.reconstitute(x => x.id).Filter(predodd); + validate_filter(raw, filtered, odds); + + + + var sub= new BehaviorSubject>(evens); + filtered = raw.reconstitute(x => x.id).Filter(sub); + validate_filter_sub(raw, filtered, sub); + + sub.OnNext(odds); + sub.OnCompleted(); + } + [Test] public async Task test_add() { // Add 1 item 10x diff --git a/src/client/sparkcmd/Program.cs b/src/client/sparkcmd/Program.cs index a4eeaaa20..1feb68ecb 100644 --- a/src/client/sparkcmd/Program.cs +++ b/src/client/sparkcmd/Program.cs @@ -13,8 +13,5 @@ namespace sparkcmd { ConsoleCommandDispatcher.DispatchCommand(commands, args, Console.Out); return; } - - - } } \ No newline at end of file diff --git a/src/client/sparkcmd/sparkcmd.csproj b/src/client/sparkcmd/sparkcmd.csproj index 2c2241ba3..31c2efcd9 100644 --- a/src/client/sparkcmd/sparkcmd.csproj +++ b/src/client/sparkcmd/sparkcmd.csproj @@ -8,7 +8,8 @@ Blue Trading Systems Blue Trading Systems Spark - x64 + x64 + x64 1.0.0 7.2 diff --git a/src/lib/CMakeDeps.cmake b/src/lib/CMakeDeps.cmake index a5efc88dc..9979952dc 100644 --- a/src/lib/CMakeDeps.cmake +++ b/src/lib/CMakeDeps.cmake @@ -1 +1 @@ -set(sources api/apicore.proto;api/apigateway.proto;api/apioptions.proto;api/core/ApiClientInterop.cpp;api/core/ApiServerInterop.cpp;api/public/ApiServer.cpp;api/public/ClientChannel.cpp;api/public/QuickbusClientChannel.cpp;api/public/ServerChannel.cpp;api/public/ServiceContext.cpp;api/service_helpers/AdminServiceUtils.cpp;api/services/AdminService.proto;api/services/ApiUnitTestService.proto;api/services/ErrorReportService.proto;api/services/HelloService.proto;api/services/MarketDataRelay.proto;api/services/OrderService.proto;api/services/OutOfProcessURLListener.proto;api/services/PersistenceService.proto;api/services/PositionService.proto;api/services/ShellDiagnosticService.proto;api/services/ShellLatencyService.proto;api/services/SyntheticSecurityService.proto;api/services/UserService.proto;api/test/ApiTests.cpp;api/test/HelloServiceServer.cpp;config/ConfigCheck.cpp;config/ConfigDownloader.cpp;config/ConfigErrorCollector.cpp;config/ConfigLog.cpp;config/ConfigPropertyEvaluator.cpp;config/ConfigValueQueue.cpp;config/ConfigurationException.cpp;config/ConfigurationState.cpp;config/PreprocessInputStream.cpp;config/config.cpp;config/defaults/Executor.conf;config/defaults/MarketData.conf;config/defaults/Ports.conf;config/defaults/Symbols.conf;config/defaults/cpp_config.conf;config/defaults/dynamic_orders.conf;config/defaults/edge.conf;config/defaults/expiration_display_offset.conf;config/defaults/jetfix.conf;config/defaults/logging.conf;config/defaults/orderman_cfg.conf;config/defaults/orders_fills_cleanup.conf;config/defaults/spreader.conf;config/defaults/testkeys.conf;config/defaults/threads.conf;config/defaults/toggles.conf;config/extensions/extensions.proto;config/invalid/invalid.proto;config/protos/ExchangeSimCfg.proto;config/protos/MarketData.proto;config/protos/Ports.proto;config/protos/Symbols.proto;config/protos/UI.proto;config/protos/cpp_config.proto;config/protos/dynamic_orders.proto;config/protos/expiration_display_offset.proto;config/protos/jetfix.proto;config/protos/logging.proto;config/protos/mercury_cfg.proto;config/protos/orderman_cfg.proto;config/protos/orders_fills_cleanup.proto;config/protos/quickbus_cfg.proto;config/protos/spreader.proto;config/protos/system.proto;config/protos/testkeys.proto;config/protos/threads.proto;config/protos/toggles.proto;config/tests/HTTPServer.cpp;config/tests/config_ref_tests.cpp;config/tests/config_tests.cpp;config/text_format/text_format.cpp;domain_model/Security.cpp;domain_model/Security.proto;domain_model/SecurityTests.cpp;domain_model/tests/domain_tests.cpp;expression/BooleanGrammar.cpp;expression/ExpressionEvaluator.cpp;expression/ParserExceptions.cpp;expression/Value.cpp;expression/tests/expression_parser_tests.cpp;jetfix/codec/FIXArray.cpp;jetfix/common/OutFIX.cpp;jetfix/connection/AsyncReader.cpp;jetfix/connection/IConnection.cpp;jetfix/connection/SocketConnection.cpp;jetfix/gateway/CoreGateway.cpp;jetfix/gateway/JetFIXGateway.cpp;jetfix/gateway/Session.cpp;jetfix/store/FIXFileStore.cpp;jetfix/store/session_name.cpp;jetfix/tests/async_reader_tests.cpp;jetfix/tests/decoding_tests.cpp;jetfix/tests/fix_encoding_stream_tests.cpp;jetfix/tests/fix_fragment_tests.cpp;jetfix/tests/jetfix_performance_tests.cpp;jetfix/tests/sample_fix.cpp;jetfix/tests/store_tests.cpp;logging/LoggerHub.cpp;logging/Sink.cpp;logging/logging.cpp;logging/tests/logging_tests.cpp;md/Diag/MDPriceLantencyTester.cpp;md/Diag/ShmDumper.cpp;md/MDConsumer.cpp;md/MDConsumerHandle.cpp;md/MDConsumerInterop.cpp;md/MDProducer.cpp;md/MDProducerHandle.cpp;md/MDRecoverableConsumer.cpp;md/MemorySegmentUtils.cpp;md/PlatformLoader.cpp;md/PriceSize.cpp;md/SecurityDB.cpp;md/SecurityIDRecorder.cpp;md/SettlementRecorder.cpp;md/Shm/FixedSecurity.cpp;md/SyntheticSecurityService.cpp;md/cme/MDP3Utils.cpp;md/common.cpp;md/protos/co2.proto;md/relay/QueryServer.cpp;md/relay/RelayClient.cpp;md/relay/RelayRecorder.cpp;md/test/Co2ManagedTestHelper.cpp;md/test/Co2TestInstrument.cpp;md/test/md2_tests.cpp;md/test/synthetic_book_merge_tests.cpp;mercury/actors/IPollableActor.cpp;mercury/broker/Broker.cpp;mercury/broker/BrokerProxy.cpp;mercury/broker/ILocalBroker.cpp;mercury/broker/IRemoteBroker.cpp;mercury/common/mercury_common.cpp;mercury/events/TimerEvent.cpp;mercury/executor/Executor.cpp;mercury/topics/Topic.cpp;metrics/metrics.cpp;metrics/system_metrics.cpp;orderman/actors/AsyncFactory.cpp;orderman/actors/ExecutionGateway.cpp;orderman/actors/FillStore.cpp;orderman/actors/OrderStore.cpp;orderman/actors/SODPosition.cpp;orderman/actors/SecuritySrc.cpp;orderman/actors/ServiceBridge.cpp;orderman/actors/order_manager/OrderBuilder.cpp;orderman/actors/order_manager/OrderManager.cpp;orderman/actors/order_manager/OrderServicesData.cpp;orderman/events/CancelRejectEvent.cpp;orderman/events/NewExecutionReportEvent.cpp;orderman/limiter/LimitStore.cpp;orderman/limiter/Limiter.cpp;orderman/limiter/ProductLimitNode.cpp;orderman/limiter/ProductLimitTree.cpp;orderman/limiter/SecurityLimitNode.cpp;orderman/limiter/SecurityLimitTree.cpp;orderman/limiter/tests/limit_tests.cpp;orderman/order_factory/OrderFactory.cpp;orderman/order_router/OrderRouter.cpp;orderman/orderman_utils/PerfTraceLog.cpp;orderman/orderman_utils/fix_order_codec.cpp;orderman/orders/DynamicOrder.cpp;orderman/orders/IcebergOrder.cpp;orderman/orders/NativeOrder.cpp;orderman/orders/Order.cpp;orderman/orders/TimeBoxOrder.cpp;orderman/orders/TriggerOrder.cpp;orderman/orders/book_algos/BookAlgo.cpp;orderman/orders/book_algos/CoverOrder.cpp;orderman/orders/book_algos/JoinOrder.cpp;orderman/orders/book_algos/LeanOrder.cpp;orderman/orders/book_algos/TickOrder.cpp;orderman/orders/book_algos/TopOrder.cpp;orderman/orders/spreader_orders/SpreaderOrder.cpp;orderman/orders/spreader_orders/spreader/LegPropertyData.cpp;orderman/orders/spreader_orders/spreader/QuoteLegPriceQuantityState.cpp;orderman/orders/spreader_orders/spreader/SingleFillHedger.cpp;orderman/orders/spreader_orders/spreader/SingleLegQuoter.cpp;orderman/orders/spreader_orders/spreader/SpreaderBookkeepingData.cpp;orderman/orders/spreader_orders/spreader/SpreaderChildOrderState.cpp;orderman/orders/spreader_orders/spreader/SpreaderLegData.cpp;orderman/orders/spreader_orders/spreader/SpreaderUtils.cpp;orderman/orders/spreader_orders/spreader/external_interfacing/SpreaderOrderBuilder.cpp;orderman/orders/spreader_orders/spreader/external_interfacing/SpreaderOrderServices.cpp;orderman/orders/spreader_orders/spreader/external_interfacing/SpreaderPreActivationOrderServices.cpp;orderman/orders/spreader_orders/spreader/hedger/UnhedgedDelta.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerEndStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerStartStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerWaitingForCancelAckStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerWaitingForFirstAckCancelledStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerWaitingForFirstAckStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerWorkingStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/QuoteDriverSet.cpp;orderman/orders/spreader_orders/spreader/quoter/QuoteDriverSetHistory.cpp;orderman/orders/spreader_orders/spreader/quoter/QuoteLegDriver.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterEndStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterStartStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForCancelAckStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForFirstAckCancelledWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForFirstAckWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForIOCWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForSizeStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWorkingStateWorker.cpp;orderman/orders/spreader_orders/spreader_states/SpreaderEndStateWorker.cpp;orderman/orders/spreader_orders/spreader_states/SpreaderStartStateWorker.cpp;orderman/orders/spreader_orders/spreader_states/SpreaderWaitingForCancelAllStateWorker.cpp;orderman/orders/spreader_orders/spreader_states/SpreaderWorkingStateWorker.cpp;orderman/orders/spreader_orders/tests/spreader_tests.cpp;orderman/server/OrderServiceServer.cpp;orderman/tests/fat_wrapper_tests.cpp;os/os.cpp;os/tests/os_tests.cpp;persistence/IStore.cpp;persistence/PersistenceServiceServer.cpp;persistence/SqliteStore.cpp;persistence/persistence_utils.cpp;persistence/tests/PersistenceServiceTests.cpp;quickbus/core/LocalNode.cpp;quickbus/core/Quickbus.cpp;quickbus/core/SubscriptionID.cpp;quickbus/core/TableID.cpp;quickbus/message_model/Update.cpp;quickbus/message_model/merge_updates.cpp;quickbus/network/QBConnection.cpp;quickbus/network/QBConnectionManager.cpp;quickbus/network/endpoint_description.cpp;quickbus/network/quickbus_msgs.proto;quickbus/pubsub/TablePublisher.cpp;quickbus/pubsub/TableSubscriber.cpp;quickbus/pubsub/tests/qb_pubsub_tests.cpp;quickbus/routing/INode.cpp;quickbus/routing/NodeID.cpp;quickbus/routing/Router.cpp;quickbus/rpc/RPCClient.cpp;quickbus/rpc/RPCServer.cpp;quickbus/rpc/RPCSessionID.cpp;quickbus/rpc/rpc_utils.cpp;quickbus/rpc/tests/qb_rpc_tests.cpp;quickbus/tests/quickbus_tests.cpp;sparkproto/SparkProtos.proto;testing/catch_test_runner.cpp;utils/Dispatcher.cpp;utils/GrowableSPSCQueue.cpp;utils/HTTPStream.cpp;utils/PBUtils.cpp;utils/Puid.cpp;utils/SecureLogon.cpp;utils/SharedMemory.cpp;utils/Sqlite_utils.cpp;utils/Uuid.cpp;utils/datetime.cpp;utils/download.cpp;utils/file_utils.cpp;utils/init_spark_lib.cpp;utils/is_unittest_environment.cpp;utils/stats.cpp;utils/thread_manager.cpp) \ No newline at end of file +set(sources api/apicore.proto;api/apigateway.proto;api/apioptions.proto;api/core/ApiClientInterop.cpp;api/core/ApiServerInterop.cpp;api/public/ApiServer.cpp;api/public/ClientChannel.cpp;api/public/QuickbusClientChannel.cpp;api/public/ServerChannel.cpp;api/public/ServiceContext.cpp;api/service_helpers/AdminServiceUtils.cpp;api/services/AdminService.proto;api/services/ApiUnitTestService.proto;api/services/ErrorReportService.proto;api/services/HelloService.proto;api/services/MarketDataAuxiliaryService.proto;api/services/MarketDataRelay.proto;api/services/OrderService.proto;api/services/OutOfProcessURLListener.proto;api/services/PersistenceService.proto;api/services/PositionService.proto;api/services/ShellDiagnosticService.proto;api/services/ShellLatencyService.proto;api/services/SyntheticSecurityService.proto;api/services/UserService.proto;api/test/ApiTests.cpp;api/test/HelloServiceServer.cpp;config/ConfigCheck.cpp;config/ConfigDownloader.cpp;config/ConfigErrorCollector.cpp;config/ConfigLog.cpp;config/ConfigPropertyEvaluator.cpp;config/ConfigValueQueue.cpp;config/ConfigurationException.cpp;config/ConfigurationState.cpp;config/PreprocessInputStream.cpp;config/config.cpp;config/defaults/Executor.conf;config/defaults/MarketData.conf;config/defaults/Ports.conf;config/defaults/Symbols.conf;config/defaults/cme_reference_data.conf;config/defaults/dynamic_orders.conf;config/defaults/edge.conf;config/defaults/jetfix.conf;config/defaults/logging.conf;config/defaults/orderman_cfg.conf;config/defaults/orders_fills_cleanup.conf;config/defaults/spreader.conf;config/defaults/testkeys.conf;config/defaults/threads.conf;config/defaults/toggles.conf;config/extensions/extensions.proto;config/invalid/invalid.proto;config/protos/ExchangeSimCfg.proto;config/protos/MarketData.proto;config/protos/Ports.proto;config/protos/Symbols.proto;config/protos/UI.proto;config/protos/cme_reference_data.proto;config/protos/dynamic_orders.proto;config/protos/jetfix.proto;config/protos/logging.proto;config/protos/mercury_cfg.proto;config/protos/orderman_cfg.proto;config/protos/orders_fills_cleanup.proto;config/protos/quickbus_cfg.proto;config/protos/spreader.proto;config/protos/system.proto;config/protos/testkeys.proto;config/protos/threads.proto;config/protos/toggles.proto;config/tests/HTTPServer.cpp;config/tests/config_ref_tests.cpp;config/tests/config_tests.cpp;config/text_format/text_format.cpp;domain_model/Security.cpp;domain_model/Security.proto;domain_model/SecurityTests.cpp;domain_model/tests/domain_tests.cpp;expression/BooleanGrammar.cpp;expression/ExpressionEvaluator.cpp;expression/ParserExceptions.cpp;expression/Value.cpp;expression/tests/expression_parser_tests.cpp;jetfix/codec/FIXArray.cpp;jetfix/common/OutFIX.cpp;jetfix/connection/AsyncReader.cpp;jetfix/connection/IConnection.cpp;jetfix/connection/SocketConnection.cpp;jetfix/gateway/CoreGateway.cpp;jetfix/gateway/JetFIXGateway.cpp;jetfix/gateway/Session.cpp;jetfix/store/FIXFileStore.cpp;jetfix/store/session_name.cpp;jetfix/tests/async_reader_tests.cpp;jetfix/tests/decoding_tests.cpp;jetfix/tests/fix_encoding_stream_tests.cpp;jetfix/tests/fix_fragment_tests.cpp;jetfix/tests/jetfix_performance_tests.cpp;jetfix/tests/sample_fix.cpp;jetfix/tests/store_tests.cpp;logging/LoggerHub.cpp;logging/Sink.cpp;logging/logging.cpp;logging/tests/logging_tests.cpp;md/Diag/MDPriceLantencyTester.cpp;md/Diag/ShmDumper.cpp;md/MDConsumer.cpp;md/MDConsumerHandle.cpp;md/MDConsumerInterop.cpp;md/MDProducer.cpp;md/MDProducerHandle.cpp;md/MDRecoverableConsumer.cpp;md/MemorySegmentUtils.cpp;md/PlatformLoader.cpp;md/PriceSize.cpp;md/SecurityDB.cpp;md/SecurityIDRecorder.cpp;md/SettlementRecorder.cpp;md/Shm/FixedSecurity.cpp;md/SyntheticSecurityService.cpp;md/cme/MDP3Utils.cpp;md/common.cpp;md/protos/co2.proto;md/relay/QueryServer.cpp;md/relay/RelayClient.cpp;md/relay/RelayRecorder.cpp;md/test/Co2ManagedTestHelper.cpp;md/test/Co2TestInstrument.cpp;md/test/md2_tests.cpp;md/test/synthetic_book_merge_tests.cpp;mercury/actors/IPollableActor.cpp;mercury/broker/Broker.cpp;mercury/broker/BrokerProxy.cpp;mercury/broker/ILocalBroker.cpp;mercury/broker/IRemoteBroker.cpp;mercury/common/mercury_common.cpp;mercury/events/TimerEvent.cpp;mercury/executor/Executor.cpp;mercury/topics/Topic.cpp;metrics/metrics.cpp;metrics/system_metrics.cpp;orderman/actors/AsyncFactory.cpp;orderman/actors/ExecutionGateway.cpp;orderman/actors/FillStore.cpp;orderman/actors/MDSupplementarySource.cpp;orderman/actors/OrderStore.cpp;orderman/actors/SODPosition.cpp;orderman/actors/SecuritySrc.cpp;orderman/actors/ServiceBridge.cpp;orderman/actors/order_manager/OrderBuilder.cpp;orderman/actors/order_manager/OrderManager.cpp;orderman/actors/order_manager/OrderServicesData.cpp;orderman/events/CancelRejectEvent.cpp;orderman/events/NewExecutionReportEvent.cpp;orderman/limiter/LimitStore.cpp;orderman/limiter/Limiter.cpp;orderman/limiter/ProductLimitNode.cpp;orderman/limiter/ProductLimitTree.cpp;orderman/limiter/SecurityLimitNode.cpp;orderman/limiter/SecurityLimitTree.cpp;orderman/limiter/tests/limit_tests.cpp;orderman/order_factory/OrderFactory.cpp;orderman/order_router/OrderRouter.cpp;orderman/orderman_utils/PerfTraceLog.cpp;orderman/orderman_utils/fix_order_codec.cpp;orderman/orders/DynamicOrder.cpp;orderman/orders/IcebergOrder.cpp;orderman/orders/NativeOrder.cpp;orderman/orders/Order.cpp;orderman/orders/TimeBoxOrder.cpp;orderman/orders/TriggerOrder.cpp;orderman/orders/book_algos/BookAlgo.cpp;orderman/orders/book_algos/CoverOrder.cpp;orderman/orders/book_algos/JoinOrder.cpp;orderman/orders/book_algos/LeanOrder.cpp;orderman/orders/book_algos/TickOrder.cpp;orderman/orders/book_algos/TopOrder.cpp;orderman/orders/spreader_orders/SpreaderOrder.cpp;orderman/orders/spreader_orders/spreader/LegPropertyData.cpp;orderman/orders/spreader_orders/spreader/QuoteLegPriceQuantityState.cpp;orderman/orders/spreader_orders/spreader/SingleFillHedger.cpp;orderman/orders/spreader_orders/spreader/SingleLegQuoter.cpp;orderman/orders/spreader_orders/spreader/SpreaderBookkeepingData.cpp;orderman/orders/spreader_orders/spreader/SpreaderChildOrderState.cpp;orderman/orders/spreader_orders/spreader/SpreaderLegData.cpp;orderman/orders/spreader_orders/spreader/SpreaderUtils.cpp;orderman/orders/spreader_orders/spreader/external_interfacing/SpreaderOrderBuilder.cpp;orderman/orders/spreader_orders/spreader/external_interfacing/SpreaderOrderServices.cpp;orderman/orders/spreader_orders/spreader/external_interfacing/SpreaderPreActivationOrderServices.cpp;orderman/orders/spreader_orders/spreader/hedger/UnhedgedDelta.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerEndStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerStartStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerWaitingForCancelAckStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerWaitingForFirstAckCancelledStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerWaitingForFirstAckStateWorker.cpp;orderman/orders/spreader_orders/spreader/hedger/hedger_states/HedgerWorkingStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/QuoteDriverSet.cpp;orderman/orders/spreader_orders/spreader/quoter/QuoteDriverSetHistory.cpp;orderman/orders/spreader_orders/spreader/quoter/QuoteLegDriver.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterEndStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterStartStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForCancelAckStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForFirstAckCancelledWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForFirstAckWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForIOCWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWaitingForSizeStateWorker.cpp;orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterWorkingStateWorker.cpp;orderman/orders/spreader_orders/spreader_states/SpreaderEndStateWorker.cpp;orderman/orders/spreader_orders/spreader_states/SpreaderStartStateWorker.cpp;orderman/orders/spreader_orders/spreader_states/SpreaderWaitingForCancelAllStateWorker.cpp;orderman/orders/spreader_orders/spreader_states/SpreaderWorkingStateWorker.cpp;orderman/orders/spreader_orders/tests/spreader_tests.cpp;orderman/server/OrderServiceServer.cpp;orderman/tests/fat_wrapper_tests.cpp;os/os.cpp;os/tests/os_tests.cpp;persistence/IStore.cpp;persistence/PersistenceServiceServer.cpp;persistence/SqliteStore.cpp;persistence/persistence_utils.cpp;persistence/tests/PersistenceServiceTests.cpp;quickbus/core/LocalNode.cpp;quickbus/core/Quickbus.cpp;quickbus/core/SubscriptionID.cpp;quickbus/core/TableID.cpp;quickbus/message_model/Update.cpp;quickbus/message_model/merge_updates.cpp;quickbus/network/QBConnection.cpp;quickbus/network/QBConnectionManager.cpp;quickbus/network/endpoint_description.cpp;quickbus/network/quickbus_msgs.proto;quickbus/pubsub/TablePublisher.cpp;quickbus/pubsub/TableSubscriber.cpp;quickbus/pubsub/tests/qb_pubsub_tests.cpp;quickbus/routing/INode.cpp;quickbus/routing/NodeID.cpp;quickbus/routing/Router.cpp;quickbus/rpc/RPCClient.cpp;quickbus/rpc/RPCServer.cpp;quickbus/rpc/RPCSessionID.cpp;quickbus/rpc/rpc_utils.cpp;quickbus/rpc/tests/qb_rpc_tests.cpp;quickbus/tests/quickbus_tests.cpp;sparkproto/SparkProtos.proto;testing/catch_test_runner.cpp;utils/Dispatcher.cpp;utils/GrowableSPSCQueue.cpp;utils/HTTPStream.cpp;utils/PBUtils.cpp;utils/Puid.cpp;utils/SecureLogon.cpp;utils/SharedMemory.cpp;utils/Sqlite_utils.cpp;utils/Uuid.cpp;utils/datetime.cpp;utils/download.cpp;utils/file_utils.cpp;utils/init_spark_lib.cpp;utils/is_unittest_environment.cpp;utils/stats.cpp;utils/thread_manager.cpp) \ No newline at end of file diff --git a/src/lib/api/service_helpers/AdminServiceUtils.cpp b/src/lib/api/service_helpers/AdminServiceUtils.cpp index f96811367..40b5bfdec 100644 --- a/src/lib/api/service_helpers/AdminServiceUtils.cpp +++ b/src/lib/api/service_helpers/AdminServiceUtils.cpp @@ -1,34 +1,40 @@ #include "lib/api/services/AdminService.server.pb.h" #include "lib/api/service_helpers/AdminServiceUtils.h" #include "lib/md/get_symbols.h" +#include "lib/config/protos/cme_reference_data.pb.h" +#include "lib/config/protos/jetfix.pb.h" +#include "lib/config/config.h" +#include namespace bts{ namespace admin{ //////////////////////////////////////////// // connection accessors //////////////////// //////////////////////////////////////////// -uint32_t AdminServiceUtils::find_max_connection_id (const bts::api::admin::AdminData& data){ +uint32_t AdminServiceUtils::find_max_connection_id (const AdminData& data){ uint32_t rv = 0; for(const auto &conn : data.connections()) if (conn.id() > rv) rv = conn.id(); return rv; } -const bts::api::admin::ExchangeConnection* AdminServiceUtils::find_connection(const bts::api::admin::AdminData& data, - const std::string& connection_name){ +const ExchangeConnection* AdminServiceUtils::find_connection( + const AdminData& data, + const std::string& connection_name){ for(const auto& connection : data.connections()) if(connection.name()==connection_name) return &connection; return nullptr; } -bool AdminServiceUtils::has_exchange(const bts::api::admin::ExchangeConnection& conn, - bts::api::Venue exchange){ +bool AdminServiceUtils::has_exchange(const ExchangeConnection& conn, + api::Venue exchange){ bool rv = false; switch(exchange) { case api::Venue::VENUE_GLBX: rv = conn.has_cme_connection() && - (conn.cme_connection().has_cgw_connection() || conn.cme_connection().has_msgw_connection()); + (conn.cme_connection().has_cgw_connection() || + conn.cme_connection().has_msgw_connection()); break; case api::Venue::VENUE_UNSET: case api::Venue::VENUE_SIM: @@ -870,6 +876,89 @@ void AdminServiceUtils::validate_user_exchange_data (const bts::api::admin::BTSU } } } +void AdminServiceUtils::get_md_entitlement_report(const AdminData& admin_data, + std::vector& entitlements){ + for (const auto& user : admin_data.users()){ + const CMEUserData* cme_data=AdminServiceUtils::get_user_cme_data(user, + admin_data); + if(cme_data!=nullptr){ + for (const auto& cme_mic_data : cme_data->md_permissions()){ + MDEntitlementReportUnit rv=get_cme_md_entitlement_unit(user.name(), + cme_mic_data); + entitlements.push_back(rv); + } + } + } +} +MDEntitlementReportUnit AdminServiceUtils::get_cme_md_entitlement_unit( + const std::string& user, + const CMEMicData& cme_mic_data){ + MDEntitlementReportUnit rv; + rv.set_user(user); + rv.set_exchange("GLBX"); + switch(cme_mic_data.mic()) + { + case api::CMEMics::CME: + rv.set_mic("CME"); + break; + case api::CMEMics::NYMEX: + rv.set_mic("NYMEX"); + break; + case api::CMEMics::CBT: + rv.set_mic("CBT"); + break; + case api::CMEMics::COMEX: + rv.set_mic("COMEX"); + break; + case api::CMEMics::MGE: + rv.set_mic("MGE"); + break; + case api::CMEMics::DME: + rv.set_mic("DME"); + break; + case api::CMEMics::NYMSW: + rv.set_mic("NYMSW"); + break; + case api::CMEMics::FXS: + rv.set_mic("FXS"); + break; + case api::CMEMics::UNSET: + case api::CMEMics::CMEMics_INT_MIN_SENTINEL_DO_NOT_USE_: + case api::CMEMics::CMEMics_INT_MAX_SENTINEL_DO_NOT_USE_: + rv.set_mic(""); + break; + } + rv.set_enabled(cme_mic_data.enabled()); + std::ostringstream key; + key << rv.user() << rv.exchange() << rv.mic(); + rv.set_record_id(key.str()); + return rv; +} +void AdminServiceUtils::get_user_symbol_permissions(const AdminData& data, + const BTSUser& user, + std::vector& rv){ + std::map mic_perm_map; + auto exchange = api::Venue::VENUE_GLBX; + const CMEUserData* cme_data=get_user_cme_data(user,data); + if(cme_data!=nullptr) + for(const auto& mic_data : cme_data->md_permissions()) + if(mic_data.enabled()) + mic_perm_map[mic_data.mic()]=true; + const auto& cme_symb_mics = config::get().cme_mic(); + for(auto it=cme_symb_mics.begin();it!=cme_symb_mics.end();it++){ + const std::string& symbol=it->first; + api::CMEMics mic=it->second; + UserSymbolMDPermissionUnit u; + std::ostringstream key; + key << exchange << user.name() << symbol; + u.set_record_id(key.str()); + u.set_user(user.name()); + u.set_exchange(exchange); + u.set_symbol(symbol); + u.set_enabled(mic_perm_map.find(mic)!=mic_perm_map.end()); + rv.push_back(u); + } +} //////////////////////////////////////////// // user mutators /////////////////////////// //////////////////////////////////////////// diff --git a/src/lib/api/service_helpers/AdminServiceUtils.h b/src/lib/api/service_helpers/AdminServiceUtils.h index de9ea6ad0..9836c65ee 100644 --- a/src/lib/api/service_helpers/AdminServiceUtils.h +++ b/src/lib/api/service_helpers/AdminServiceUtils.h @@ -4,12 +4,13 @@ #include "lib/api/services/AdminService.server.pb.h" #include "lib/api/services/OrderService.server.pb.h" #include "lib/domain_model/Security.h" -#include "lib/config/protos/jetfix.pb.h" -#include "lib/config/config.h" namespace bts{ namespace admin{ +using namespace bts::api::admin; +using namespace bts::api::orderman; + class AdminServiceUtils{ public: //////////////////////////////////////////// @@ -17,233 +18,286 @@ public: //////////////////////////////////////////// // finds the max id out of all the existing connections // (todo: deleted connections) - static uint32_t find_max_connection_id (const bts::api::admin::AdminData& data); + static uint32_t find_max_connection_id (const AdminData& data); // finds a connection by name and returns a mutable pointer to it - static const bts::api::admin::ExchangeConnection* find_connection(const bts::api::admin::AdminData& data, - const std::string& connection_name); + static const ExchangeConnection* find_connection(const AdminData& data, + const std::string& connection_name); // finds the presence of an exchange in a connection - static bool has_exchange(const bts::api::admin::ExchangeConnection& conn, + static bool has_exchange(const ExchangeConnection& conn, bts::api::Venue exchange); // finds the default connections for an exchange // in a given account - static const bts::api::admin::ExchangeConnection* find_default_account_connection(const bts::api::admin::AdminData& data, - const bts::api::admin::BTSAccount& account, - bts::api::Venue exchange); - static const bts::api::admin::ExchangeConnection* resolve_cme_connection(const bts::api::admin::AdminData& data, - const bts::api::admin::BTSAccount& account, - const bts::api::Security& sec); - static const bts::api::admin::ExchangeConnection* resolve_sim_connection(const bts::api::admin::AdminData& data, - const bts::api::admin::BTSAccount& account, - const bts::api::Security& sec); - static const bts::api::admin::ExchangeConnection* resolve_connection(const bts::api::admin::AdminData& data, - const bts::api::admin::BTSAccount& account, - const bts::api::Security& sec); + static const ExchangeConnection* find_default_account_connection( + const AdminData& data, + const BTSAccount& account, + bts::api::Venue exchange); + static const ExchangeConnection* resolve_cme_connection( + const AdminData& data, + const BTSAccount& account, + const api::Security& sec); + static const ExchangeConnection* resolve_sim_connection( + const AdminData& data, + const BTSAccount& account, + const api::Security& sec); + static const ExchangeConnection* resolve_connection( + const AdminData& data, + const BTSAccount& account, + const bts::api::Security& sec); //////////////////////////////////////////// // connection mutators ///////////////////// //////////////////////////////////////////// // adds a new connection (does not check for name uniqueness) - static bts::api::admin::ExchangeConnection* add_connection(bts::api::admin::AdminData& data, - const bts::api::admin::ExchangeConnection& new_connection); + static ExchangeConnection* add_connection(AdminData& data, + const ExchangeConnection& new_connection); // reads the connections from config and fills in the // admin data (with names only) - static void fill_connections_from_config(bts::api::admin::AdminData& admin_data); + static void fill_connections_from_config(AdminData& admin_data); //////////////////////////////////////////// // account accessors /////////////////////// //////////////////////////////////////////// // finds the account from the name and returns // a mutable pointer to the account - static bts::api::admin::BTSAccount* find_account (bts::api::admin::AdminData& data, const std::string& acct_name, bool include_deleted=false); + static BTSAccount* find_account (AdminData& data, + const std::string& acct_name, + bool include_deleted=false); // same as above for const pointer - static const bts::api::admin::BTSAccount* find_account (const bts::api::admin::AdminData& data, const std::string& acct_name, bool include_deleted=false); + static const BTSAccount* find_account (const AdminData& data, + const std::string& acct_name, + bool include_deleted=false); // finds the account from the id and returns // a mutable pointer to the account - static bts::api::admin::BTSAccount* find_account (bts::api::admin::AdminData& data, uint32_t id, bool include_deleted=false); + static BTSAccount* find_account (AdminData& data, + uint32_t id, + bool include_deleted=false); // like above but returns a const pointer - static const bts::api::admin::BTSAccount* find_account (const bts::api::admin::AdminData& data, uint32_t id, bool include_deleted=false); + static const BTSAccount* find_account (const AdminData& data, + uint32_t id, + bool include_deleted=false); // this function is an indirection layer for account access // so deletions are dealt in a more deliberate manner - static std::vector accounts (const bts::api::admin::AdminData& data, bool include_deleted); - static std::vector accounts_mutable (bts::api::admin::AdminData& data, bool include_deleted); + static std::vector accounts (const AdminData& data, + bool include_deleted); + static std::vector accounts_mutable (AdminData& data, + bool include_deleted); // finds the max id amongts all the existing accounts - static uint32_t find_max_acct_id (const bts::api::admin::AdminData& data); + static uint32_t find_max_acct_id (const AdminData& data); // gets the account data for cme (and goes up the hierarchy // if needed) - static const bts::api::admin::CMEAccountData* get_account_cme_data(const bts::api::admin::BTSAccount& account, - const bts::api::admin::AdminData& data); - static const bts::api::admin::SIMAccountData* get_account_sim_data(const bts::api::admin::BTSAccount& account, - const bts::api::admin::AdminData& data); + static const CMEAccountData* get_account_cme_data(const BTSAccount& account, + const AdminData& data); + static const SIMAccountData* get_account_sim_data(const BTSAccount& account, + const AdminData& data); // returns true if an account has a connection - bool static account_has_connection(const bts::api::admin::BTSAccount& account, const std::string& connection_name); + bool static account_has_connection(const BTSAccount& account, + const std::string& connection_name); // finds the external/clearing account associated // with an account - static std::string find_account_exchange_account (const bts::api::admin::AdminData& data, - const bts::api::admin::BTSAccount& account, - api::Venue exchange, - bts::api::admin::AdminServiceOperationResult& out_result); + static std::string find_account_exchange_account (const AdminData& data, + const BTSAccount& account, + api::Venue exchange, + AdminServiceOperationResult& out_result); // returns a map of the accounts vs their 'is_active' - static std::map accounts_map(const bts::api::admin::AdminData& admin_data); + static std::map accounts_map(const AdminData& admin_data); // validates that the update to the account is valid - static void validate_account_update (const bts::api::admin::AdminData& admin_data, - const bts::api::admin::BTSAccount& updated_account, - bts::api::admin::AdminServiceOperationResult& result); + static void validate_account_update (const AdminData& admin_data, + const BTSAccount& updated_account, + AdminServiceOperationResult& result); // validates that the account exchange changes are valid // (ex > 12 characters for accounts at CME) - static void validate_account_exchange_data (const bts::api::admin::BTSAccount& account, - bts::api::admin::AdminServiceOperationResult& out_result); - static bts::api::orderman::RoutingRecord get_routing_record(const bts::api::admin::AdminData& data, - const std::string& account_name, - const std::string& user_name, - const bts::api::Security& sec, - bts::api::admin::AdminServiceOperationResult& out_result); - static bts::api::orderman::RoutingRecord get_routing_record(const bts::api::admin::AdminData& data, - uint32_t account_id, - uint32_t user_id, - const bts::api::Security& sec, - bts::api::admin::AdminServiceOperationResult& out_result); + static void validate_account_exchange_data (const BTSAccount& account, + AdminServiceOperationResult& out_result); + static RoutingRecord get_routing_record( + const AdminData& data, + const std::string& account_name, + const std::string& user_name, + const bts::api::Security& sec, + AdminServiceOperationResult& out_result); + static RoutingRecord get_routing_record(const AdminData& data, + uint32_t account_id, + uint32_t user_id, + const bts::api::Security& sec, + AdminServiceOperationResult& out_result); //////////////////////////////////////////// // account mutators //////////////////////// //////////////////////////////////////////// - static bts::api::admin::BTSAccount* create_account(bts::api::admin::AdminData& data, - uint32_t parent_account_id, - const std::string& account_name, - const std::string& description, - bool is_active, - bts::api::admin::AdminServiceCreateAccountResult& result); + static BTSAccount* create_account(AdminData& data, + uint32_t parent_account_id, + const std::string& account_name, + const std::string& description, + bool is_active, + AdminServiceCreateAccountResult& result); // writes over the account data completely - static void update_account(bts::api::admin::AdminData& data, - const bts::api::admin::BTSAccount& updated_account, - bts::api::admin::AdminServiceOperationResult& result); - static void delete_account (bts::api::admin::AdminData& admin_data, - const bts::api::admin::IDAPIArgument& id, - bts::api::admin::AdminServiceOperationResult& result); - static void set_account_exchange_data(bts::api::admin::BTSAccount* account, - const bts::api::admin::ExchangeAccountData& exchange_data); + static void update_account(AdminData& data, + const BTSAccount& updated_account, + AdminServiceOperationResult& result); + static void delete_account (AdminData& admin_data, + const IDAPIArgument& id, + AdminServiceOperationResult& result); + static void set_account_exchange_data(BTSAccount* account, + const ExchangeAccountData& exchange_data); // this is not a generally useful function but since // connection are in config, this cleans up the account's // connections and syncs it with the list in AdminData - static void sync_account_connections(const bts::api::admin::AdminData& data, - bts::api::admin::BTSAccount& account); + static void sync_account_connections(const AdminData& data, + BTSAccount& account); //////////////////////////////////////////// // group accessors ///////////////////////// //////////////////////////////////////////// // given an id, finds the mutable pointer to a group - static bts::api::admin::BTSUserGroup* find_group (bts::api::admin::AdminData& data, uint32_t id); + static BTSUserGroup* find_group (AdminData& data, uint32_t id); // given an id, finds the const pointer to a group - static const bts::api::admin::BTSUserGroup* const find_group (const bts::api::admin::AdminData& data, uint32_t id); + static const BTSUserGroup* const find_group (const AdminData& data, + uint32_t id); // given a name, finds the mutable pointer to a group - static bts::api::admin::BTSUserGroup* find_group (bts::api::admin::AdminData& data, const std::string& group_name); + static BTSUserGroup* find_group (AdminData& data, + const std::string& group_name); // finds the max id out of all the groups - static uint32_t find_max_group_id (const bts::api::admin::AdminData& data); + static uint32_t find_max_group_id (const AdminData& data); // travels up the group tree until it finds a populated cme data - static const bts::api::admin::CMEUserData* get_group_cme_data(const bts::api::admin::BTSUserGroup& group, - const bts::api::admin::AdminData& data); - static const bts::api::admin::SIMUserData* get_group_sim_data(const bts::api::admin::BTSUserGroup& group, - const bts::api::admin::AdminData& data); - static const bts::api::admin::UserAccountPermissions* const get_group_user_permissions (const bts::api::admin::AdminData& admin_data, - uint32_t group_id); + static const CMEUserData* get_group_cme_data(const BTSUserGroup& group, + const AdminData& data); + static const SIMUserData* get_group_sim_data(const BTSUserGroup& group, + const AdminData& data); + static const UserAccountPermissions* const get_group_user_permissions ( + const bts::api::admin::AdminData& admin_data, + uint32_t group_id); //////////////////////////////////////////// // group mutators/ ///////////////////////// //////////////////////////////////////////// - static bts::api::admin::BTSUserGroup* create_group(bts::api::admin::AdminData& data, - uint32_t parent_group_id, - const std::string& group_name, - bts::api::admin::AdminServiceOperationResult& result); + static BTSUserGroup* create_group(AdminData& data, + uint32_t parent_group_id, + const std::string& group_name, + AdminServiceOperationResult& result); //////////////////////////////////////////// // user accessors ////////////////////////// //////////////////////////////////////////// - static bts::api::admin::BTSUser* find_user (bts::api::admin::AdminData& data, const std::string& user_name, bool include_deleted=false); - static const bts::api::admin::BTSUser* find_user (const bts::api::admin::AdminData& data, const std::string& user_name, bool include_deleted=false); - static bts::api::admin::BTSUser* find_user (bts::api::admin::AdminData& data, uint32_t id, bool include_deleted=false); - static const bts::api::admin::BTSUser* const find_user (const bts::api::admin::AdminData& data, uint32_t id, bool include_deleted=false); - static std::vector users (const bts::api::admin::AdminData& data, bool include_deleted); - static std::vector users_mutable (bts::api::admin::AdminData& data, bool include_deleted); - static uint32_t find_max_user_id (const bts::api::admin::AdminData& data); - static const bts::api::admin::CMEUserData* get_user_cme_data(const bts::api::admin::BTSUser& user, - const bts::api::admin::AdminData& data); - static const bts::api::admin::SIMUserData* get_user_sim_data(const bts::api::admin::BTSUser& user, - const bts::api::admin::AdminData& data); + static BTSUser* find_user (AdminData& data, + const std::string& user_name, + bool include_deleted=false); + static const BTSUser* find_user (const AdminData& data, + const std::string& user_name, + bool include_deleted=false); + static BTSUser* find_user (AdminData& data, + uint32_t id, + bool include_deleted=false); + static const BTSUser* const find_user (const AdminData& data, + uint32_t id, + bool include_deleted=false); + static std::vector users (const AdminData& data, + bool include_deleted); + static std::vector users_mutable (AdminData& data, + bool include_deleted); + static uint32_t find_max_user_id (const AdminData& data); + static const CMEUserData* get_user_cme_data(const BTSUser& user, + const AdminData& data); + static const SIMUserData* get_user_sim_data(const BTSUser& user, + const AdminData& data); // finds the "exchange user" (ex: "operator_id" at CME or "authorized_trader" // at ICE) for a user - static std::string find_user_exchange_id (const bts::api::admin::AdminData& data, - const bts::api::admin::BTSUser& user, - api::Venue exchange, - bts::api::admin::AdminServiceOperationResult& out_result); - static std::pair - find_user_account_matches (const bts::api::admin::AdminData& admin_data, - const std::string& connection, const std::string& exchange_account, + static std::string find_user_exchange_id ( + const AdminData& data, + const BTSUser& user, + api::Venue exchange, + AdminServiceOperationResult& out_result); + static std::pair + find_user_account_matches (const AdminData& admin_data, + const std::string& connection, + const std::string& exchange_account, const std::string& exchange_user, api::Venue exchange); // for a given user finds the account permissions (if permissions are // derived from the group, it traverses the group hierarcy to find the // correct permissions. - static const bts::api::admin::UserAccountPermissions* get_user_permissions (const bts::api::admin::AdminData& admin_data, uint32_t user_id); + static const UserAccountPermissions* get_user_permissions ( + const AdminData& admin_data, + uint32_t user_id); // for a given user, fills in the associated_accounts ref arg // with the accounts the user has permission to trade in - static void find_user_trading_accounts (const bts::api::admin::AdminData& admin_data, - uint32_t user_id, - std::vector& associated_accounts); + static void find_user_trading_accounts ( + const AdminData& admin_data, + uint32_t user_id, + std::vector& associated_accounts); // validates that changes in user data satisfy all the // constraints - static void validate_user_update (const bts::api::admin::AdminData& admin_data, - const bts::api::admin::BTSUser& updated_user, - bts::api::admin::AdminServiceOperationResult& result); + static void validate_user_update (const AdminData& admin_data, + const BTSUser& updated_user, + AdminServiceOperationResult& result); // vaidates the exchange part of user data updates - static void validate_user_exchange_data (const bts::api::admin::BTSUser& updated_user, - bts::api::admin::AdminServiceOperationResult& out_result); + static void validate_user_exchange_data ( + const BTSUser& updated_user, + AdminServiceOperationResult& out_result); + static void get_md_entitlement_report(const AdminData& admin_data, + std::vector& entitlements); + // Gets a reporting unit out of CMEMicData + static MDEntitlementReportUnit get_cme_md_entitlement_unit( + const std::string& user, + const CMEMicData& cme_mic_data); + static void get_user_symbol_permissions( + const AdminData& data, + const BTSUser& user, + std::vector& res); //////////////////////////////////////////// // user mutators /////////////////////////// //////////////////////////////////////////// - static bts::api::admin::BTSUser* create_user (bts::api::admin::AdminData& data, - uint32_t parent_group_id, - const std::string& name, - const std::string& password, - bool is_active, - const bts::api::admin::LicenseData& license_data, - const bts::api::admin::LicenseEntitlements& license_entitlements, - bts::api::admin::AdminServiceCreateEntityResult& result); - static bts::api::admin::BTSUser* update_user(bts::api::admin::AdminData& admin_data, - const bts::api::admin::BTSUser& updated_user, - bts::api::admin::AdminServiceOperationResult& result); - static void delete_user (bts::api::admin::AdminData& admin_data, - const bts::api::admin::IDAPIArgument& id, - bts::api::admin::AdminServiceOperationResult& result); - static void set_user_exchange_data(bts::api::admin::BTSUser* user, - const bts::api::admin::ExchangeUserData& exchange_user_data); - static void set_user_md_entitlement_data(bts::api::admin::BTSUser* user, - const bts::api::admin::ExchangeUserMarketDataEntitlementData& exchange_user_md_entitelemnt_data); - static void clear_user_accounts (bts::api::admin::BTSUser* user); - static void set_user_accounts (bts::api::admin::BTSUser* user, - const bts::api::admin::UserAccountPermissions& user_account_permissions); + static BTSUser* create_user (AdminData& data, + uint32_t parent_group_id, + const std::string& name, + const std::string& password, + bool is_active, + const LicenseData& license_data, + const LicenseEntitlements& license_entitlements, + AdminServiceCreateEntityResult& result); + static BTSUser* update_user(AdminData& admin_data, + const BTSUser& updated_user, + AdminServiceOperationResult& result); + static void delete_user (AdminData& admin_data, + const IDAPIArgument& id, + AdminServiceOperationResult& result); + static void set_user_exchange_data( + BTSUser* user, + const ExchangeUserData& exchange_user_data); + static void set_user_md_entitlement_data(BTSUser* user, + const ExchangeUserMarketDataEntitlementData&); + static void clear_user_accounts (BTSUser* user); + static void set_user_accounts (BTSUser* user, + const UserAccountPermissions& user_account_permissions); //////////////////////////////////////////// //risk limits ////////////////////////////// //////////////////////////////////////////// // sorting function used by sort_limits to // sort SecurityScope's - static bool _sort_security_scopes (const bts::api::admin::SecurityScope& a, const bts::api::admin::SecurityScope& b); + static bool _sort_security_scopes (const SecurityScope& a, + const SecurityScope& b); // sorting function used by sort_limits to // sort ProductScope's - static bool _sort_product_scopes (const bts::api::admin::ProductScope& a, const bts::api::admin::ProductScope&); + static bool _sort_product_scopes (const ProductScope& a, + const ProductScope&); // this sorts the rules such that the most // specific ones are at the top. this makes it // easier to match rules with securities/products - static void sort_limits (bts::api::admin::BTSAccount& account); + static void sort_limits (BTSAccount& account); // creates a copy of AdminData with sorted rules - static bts::api::admin::AdminData sort_limits (const bts::api::admin::AdminData& admin_data); + static AdminData sort_limits (const AdminData& admin_data); // find the clip size for a security, account - static const uint32_t find_account_security_clip_size(const bts::api::admin::BTSAccount& account, - const api::Security& security); + static const uint32_t find_account_security_clip_size( + const BTSAccount& account, + const api::Security& security); // find the position limit for an account, security - static const bts::api::admin::AccountSecurityPositionLimitUnit* find_account_security_position_limit(const bts::api::admin::BTSAccount& account, + static const AccountSecurityPositionLimitUnit* + find_account_security_position_limit(const BTSAccount& account, const api::Security& security); // find the position limit for an account, product - static const bts::api::admin::AccountProductPositionLimitUnit* find_account_product_position_limit(const bts::api::admin::BTSAccount& account, + static const AccountProductPositionLimitUnit* + find_account_product_position_limit(const BTSAccount& account, const api::Product& product); - static bool match_security_scope (const bts::api::admin::SecurityScope& scope, - const bts::api::Security& security); - static bool match_product_scope (const bts::api::admin::ProductScope& scope, - const bts::api::Product& product); - static bool match_product(const api::Security& security, const api::Product& product); - static bool _match_limit_rule_symbol (const std::string& symbol, const std::string& limit_symbol); + static bool match_security_scope (const SecurityScope& scope, + const api::Security& security); + static bool match_product_scope (const ProductScope& scope, + const api::Product& product); + static bool match_product(const api::Security& security, + const api::Product& product); + static bool _match_limit_rule_symbol (const std::string& symbol, + const std::string& limit_symbol); static api::SecurityType security_type(const api::Security& security); //////////////////////////////////////////// // misc//////// //////////////////////////// diff --git a/src/lib/api/services/AdminService.proto b/src/lib/api/services/AdminService.proto index 0f699a548..3e13152f1 100644 --- a/src/lib/api/services/AdminService.proto +++ b/src/lib/api/services/AdminService.proto @@ -182,6 +182,17 @@ message AccountExchangeConnections{ /***************************Exchange Data********************************/ /************************************************************************/ +message CMEMicData{ + api.CMEMics mic=10; + bool enabled=20; + // there are other things here +} + +message ICEMicData{ + api.CMEMics mic=10; + bool enabled=20; +} + message ExchangeUserMarketDataEntitlementData{ CMEUserMarketDataEntitlementData cme_data = 10; ICEUserMarketDataEntitlementData ice_data = 20; @@ -276,6 +287,8 @@ message CMEUserData{ string smp_instruction = 70; string max_order_rate_per_connection = 80; string location = 90; + // add suff here + repeated CMEMicData md_permissions = 1000; } message SIMUserData { @@ -331,6 +344,48 @@ message ICEUserData{ message ICEUserMarketDataEntitlementData{ } +/////////////////////////////////////////////////////////// +// Order Auxiliary Service //////////////////////////////// + +message OrdersSummaryUnit{ + string account = 10; + string user = 20; + uint32 total_orders = 30; + uint32 total_contracts = 40; +} +message OrdersSummary{ + repeated OrdersSummaryUnit units = 10; +} +message OrderSummaryRequest{ +} +// top level orders that have account +// and user that are present here, will +// be cancelled. wild cards are not +// supported. +message OrderCancelRequest{ + repeated string accounts = 10; + repeated string users = 20; +} +/////////////////////////////////////////////////////////// +// Reporting ////////////////////////////////////////////// +message MDEntitlementReportUnit{ + string record_id = 1 [(bts.api.key)=true]; + string user = 10; + string exchange = 20; + string mic = 30; + bool enabled = 40; +} + +//////////////////////////////////////////////////////////// +// Misc /////////////////////////////////////////////////// +message UserSymbolMDPermissionUnit{ + string record_id = 1 [(bts.api.key)=true]; + string user=10; + api.Venue exchange=20; + string symbol=30; + bool enabled=40; +} + /************************************************************************/ /************************************************************************/ /************************************************************************/ @@ -385,4 +440,11 @@ service AdminService { // subscription rpc admin_data(bts.api.VoidMessage) returns (stream AdminData); rpc get_admin_data(bts.api.VoidMessage) returns (AdminData); + // orders + rpc get_order_summary(bts.api.VoidMessage) returns (OrdersSummary); + rpc cancel_orders(OrderCancelRequest) returns (bts.api.VoidMessage); + // reports + rpc md_entitlement_data(bts.api.VoidMessage) returns (stream MDEntitlementReportUnit); + // misc + rpc md_permissions_data(bts.api.VoidMessage) returns (stream UserSymbolMDPermissionUnit); } \ No newline at end of file diff --git a/src/lib/api/services/ApiUnitTestService.proto b/src/lib/api/services/ApiUnitTestService.proto index a711ba570..a5e56e044 100644 --- a/src/lib/api/services/ApiUnitTestService.proto +++ b/src/lib/api/services/ApiUnitTestService.proto @@ -48,7 +48,8 @@ message TestResult { TEARDOWN = 2; PASS = 3; TIMEOUT = 4; - IGNORED = 5; + CRASH = 5; + IGNORED = 6; } string test_name = 1; string message = 3; diff --git a/src/lib/api/services/MarketDataAuxiliaryService.proto b/src/lib/api/services/MarketDataAuxiliaryService.proto new file mode 100644 index 000000000..559f4f3f4 --- /dev/null +++ b/src/lib/api/services/MarketDataAuxiliaryService.proto @@ -0,0 +1,20 @@ +syntax="proto3"; +package bts.api.md.auxiliary; +option csharp_namespace="bts.api.md.auxiliary"; +import "lib/api/apioptions.proto"; +import "lib/api/apicore.proto"; + +message VolumeAtPriceLevel{ + int32 record_id = 1 [(bts.api.key)=true]; + uint32 security_id = 20; + double price = 30; + uint32 volume = 40; + uint32 buy_volume = 50; + uint32 sell_volume = 60; + uint32 passive_volume = 70; +} + +service MarketDataAuxiliaryService{ + option (bts.api.service_description)="Provides non-core supplementary market data"; + rpc volume_at_price(bts.api.VoidMessage) returns (stream VolumeAtPriceLevel); +} diff --git a/src/lib/config/defaults/cpp_config.conf b/src/lib/config/defaults/cme_reference_data.conf similarity index 50% rename from src/lib/config/defaults/cpp_config.conf rename to src/lib/config/defaults/cme_reference_data.conf index 0f4fb61f8..45372ff86 100644 --- a/src/lib/config/defaults/cpp_config.conf +++ b/src/lib/config/defaults/cme_reference_data.conf @@ -1,5 +1,8 @@ -# Generated by automated script. DO NOT EDIT! +# +# Entire file generated by automated script. DO NOT EDIT! # source: tools/cpp_helper/generate-cpps.ps1 +# + CppConfig { value @@ -179,7 +182,7 @@ CppConfig cpp_entry { key: "AE5" value: 1000 } cpp_entry { key: "AE8" value: 5 } cpp_entry { key: "AE9" value: 80 } - cpp_entry { key: "AEJ" value: 1020 } + cpp_entry { key: "AEJ" value: 940 } cpp_entry { key: "AEP" value: 25 } cpp_entry { key: "AET" value: 1000 } cpp_entry { key: "AEZ" value: 42000 } @@ -218,7 +221,7 @@ CppConfig cpp_entry { key: "AJB" value: 1000 } cpp_entry { key: "AJJ" value: 5 } cpp_entry { key: "AJL" value: 42000 } - cpp_entry { key: "AJP" value: 1020 } + cpp_entry { key: "AJP" value: 940 } cpp_entry { key: "AJR" value: 1000 } cpp_entry { key: "AJS" value: 42000 } cpp_entry { key: "AJU" value: 42000 } @@ -723,7 +726,7 @@ CppConfig cpp_entry { key: "JFB" value: 1000 } cpp_entry { key: "JFC" value: 1000 } cpp_entry { key: "JKM" value: 10000 } - cpp_entry { key: "JML" value: 440 } + cpp_entry { key: "JML" value: 40 } cpp_entry { key: "JNC" value: 1000 } cpp_entry { key: "JNG" value: 10000 } cpp_entry { key: "JNL" value: 80 } @@ -813,7 +816,7 @@ CppConfig cpp_entry { key: "MEB" value: 100 } cpp_entry { key: "MEE" value: 100 } cpp_entry { key: "MEF" value: 10 } - cpp_entry { key: "MEL" value: 840 } + cpp_entry { key: "MEL" value: 920 } cpp_entry { key: "MEO" value: 100 } cpp_entry { key: "MEW" value: 100 } cpp_entry { key: "MFB" value: 1000 } @@ -1124,20 +1127,19 @@ CppConfig cpp_entry { key: "STY" value: 1000 } cpp_entry { key: "SU" value: 250 } cpp_entry { key: "SUT" value: 250 } - cpp_entry { key: "T08" value: 800 } - cpp_entry { key: "T09" value: 800 } + cpp_entry { key: "T04" value: 800 } + cpp_entry { key: "T05" value: 800 } + cpp_entry { key: "T06" value: 800 } + cpp_entry { key: "T07" value: 800 } cpp_entry { key: "T10" value: 800 } + cpp_entry { key: "T11" value: 800 } + cpp_entry { key: "T12" value: 800 } cpp_entry { key: "T13" value: 800 } cpp_entry { key: "T14" value: 800 } - cpp_entry { key: "T15" value: 800 } - cpp_entry { key: "T16" value: 800 } cpp_entry { key: "T17" value: 800 } + cpp_entry { key: "T18" value: 800 } + cpp_entry { key: "T19" value: 800 } cpp_entry { key: "T1U" value: 1000 } - cpp_entry { key: "T20" value: 800 } - cpp_entry { key: "T21" value: 800 } - cpp_entry { key: "T22" value: 800 } - cpp_entry { key: "T23" value: 800 } - cpp_entry { key: "T24" value: 800 } cpp_entry { key: "T27" value: 800 } cpp_entry { key: "T28" value: 800 } cpp_entry { key: "T29" value: 800 } @@ -1145,6 +1147,7 @@ CppConfig cpp_entry { key: "T2D" value: 1000 } cpp_entry { key: "T2M" value: 100 } cpp_entry { key: "T30" value: 800 } + cpp_entry { key: "T31" value: 800 } cpp_entry { key: "T3B" value: 1000 } cpp_entry { key: "T3L" value: 80 } cpp_entry { key: "T4B" value: 1000 } @@ -1180,8 +1183,8 @@ CppConfig cpp_entry { key: "TPB" value: 5000 } cpp_entry { key: "TPY" value: 5000 } cpp_entry { key: "TRB" value: 25 } - cpp_entry { key: "TRE" value: 125000 } cpp_entry { key: "TRI" value: 25 } + cpp_entry { key: "TRL" value: 1000000 } cpp_entry { key: "TRM" value: 25 } cpp_entry { key: "TRY" value: 200000 } cpp_entry { key: "TRZ" value: 2500 } @@ -1189,7 +1192,7 @@ CppConfig cpp_entry { key: "TT" value: 50000 } cpp_entry { key: "TTD" value: 24 } cpp_entry { key: "TTE" value: 10000 } - cpp_entry { key: "TTF" value: 744 } + cpp_entry { key: "TTF" value: 720 } cpp_entry { key: "TW" value: 1000 } cpp_entry { key: "TXN" value: 2500 } cpp_entry { key: "TZ6" value: 2500 } @@ -1204,7 +1207,7 @@ CppConfig cpp_entry { key: "UFU" value: 100 } cpp_entry { key: "UFZ" value: 100 } cpp_entry { key: "UGG" value: 42000 } - cpp_entry { key: "UKG" value: 310 } + cpp_entry { key: "UKG" value: 300 } cpp_entry { key: "ULB" value: 42000 } cpp_entry { key: "UMB" value: 1000 } cpp_entry { key: "UMD" value: 1000 } @@ -1291,6 +1294,7 @@ CppConfig cpp_entry { key: "XAU" value: 100 } cpp_entry { key: "XAV" value: 100 } cpp_entry { key: "XAY" value: 100 } + cpp_entry { key: "XAZ" value: 250 } cpp_entry { key: "XBT" value: 100 } cpp_entry { key: "XC" value: 10 } cpp_entry { key: "XER" value: 1000 } @@ -1310,6 +1314,8 @@ CppConfig cpp_entry { key: "XW" value: 10 } cpp_entry { key: "XX" value: 2500 } cpp_entry { key: "XYT" value: 100 } + cpp_entry { key: "XZI" value: 250 } + cpp_entry { key: "XZT" value: 250 } cpp_entry { key: "YF" value: 42000 } cpp_entry { key: "YHE" value: 80 } cpp_entry { key: "YHF" value: 80 } @@ -1391,7 +1397,7 @@ CppConfig cpp_entry { key: "ZD9909" value: 1000 } cpp_entry { key: "ZD9912" value: 1000 } cpp_entry { key: "ZF" value: 1000 } - cpp_entry { key: "ZGD" value: 100 } + cpp_entry { key: "ZGD" value: 1000 } cpp_entry { key: "ZGL" value: 5 } cpp_entry { key: "ZJL" value: 5 } cpp_entry { key: "ZKU" value: 1000 } @@ -1409,4 +1415,1418 @@ CppConfig cpp_entry { key: "ZW" value: 50 } cpp_entry { key: "ZWT" value: 50 } } +} + +CmeSymbolMics +{ + value + { + cme_mic { key: "0A" value: 2 } + cme_mic { key: "0B" value: 2 } + cme_mic { key: "0C" value: 2 } + cme_mic { key: "0E" value: 2 } + cme_mic { key: "16E" value: 1 } + cme_mic { key: "1BT" value: 1 } + cme_mic { key: "1E" value: 2 } + cme_mic { key: "1ET" value: 1 } + cme_mic { key: "1FT" value: 1 } + cme_mic { key: "1H" value: 2 } + cme_mic { key: "1IT" value: 1 } + cme_mic { key: "1KT" value: 1 } + cme_mic { key: "1NA" value: 2 } + cme_mic { key: "1PT" value: 1 } + cme_mic { key: "1S" value: 2 } + cme_mic { key: "1T" value: 2 } + cme_mic { key: "1UT" value: 1 } + cme_mic { key: "1VT" value: 1 } + cme_mic { key: "1Y" value: 2 } + cme_mic { key: "1YT" value: 1 } + cme_mic { key: "1ZA" value: 2 } + cme_mic { key: "22" value: 2 } + cme_mic { key: "23" value: 2 } + cme_mic { key: "26" value: 2 } + cme_mic { key: "27" value: 2 } + cme_mic { key: "29" value: 2 } + cme_mic { key: "2C" value: 2 } + cme_mic { key: "2D" value: 2 } + cme_mic { key: "2FW" value: 2 } + cme_mic { key: "2GT" value: 1 } + cme_mic { key: "2JW" value: 2 } + cme_mic { key: "2RB" value: 2 } + cme_mic { key: "2VT" value: 1 } + cme_mic { key: "2ZW" value: 2 } + cme_mic { key: "36" value: 2 } + cme_mic { key: "36E" value: 1 } + cme_mic { key: "37" value: 2 } + cme_mic { key: "38" value: 2 } + cme_mic { key: "39" value: 2 } + cme_mic { key: "3L" value: 2 } + cme_mic { key: "3NA" value: 2 } + cme_mic { key: "3NB" value: 2 } + cme_mic { key: "3P" value: 2 } + cme_mic { key: "3V" value: 2 } + cme_mic { key: "3XW" value: 2 } + cme_mic { key: "3ZW" value: 2 } + cme_mic { key: "4C" value: 2 } + cme_mic { key: "4G" value: 2 } + cme_mic { key: "4RB" value: 2 } + cme_mic { key: "4U" value: 2 } + cme_mic { key: "4V" value: 2 } + cme_mic { key: "4XW" value: 2 } + cme_mic { key: "4Y" value: 2 } + cme_mic { key: "51" value: 2 } + cme_mic { key: "5L" value: 2 } + cme_mic { key: "5Y" value: 2 } + cme_mic { key: "5ZN" value: 2 } + cme_mic { key: "63" value: 2 } + cme_mic { key: "6A" value: 1 } + cme_mic { key: "6B" value: 1 } + cme_mic { key: "6C" value: 1 } + cme_mic { key: "6E" value: 1 } + cme_mic { key: "6J" value: 1 } + cme_mic { key: "6L" value: 1 } + cme_mic { key: "6M" value: 1 } + cme_mic { key: "6N" value: 1 } + cme_mic { key: "6R" value: 1 } + cme_mic { key: "6S" value: 1 } + cme_mic { key: "6Z" value: 1 } + cme_mic { key: "7D" value: 2 } + cme_mic { key: "7FT" value: 2 } + cme_mic { key: "7N" value: 2 } + cme_mic { key: "7P" value: 2 } + cme_mic { key: "7S" value: 2 } + cme_mic { key: "7U" value: 2 } + cme_mic { key: "7V" value: 2 } + cme_mic { key: "7X" value: 2 } + cme_mic { key: "88" value: 2 } + cme_mic { key: "8D" value: 2 } + cme_mic { key: "8W" value: 2 } + cme_mic { key: "8XN" value: 2 } + cme_mic { key: "8ZN" value: 2 } + cme_mic { key: "9FN" value: 2 } + cme_mic { key: "9Q" value: 2 } + cme_mic { key: "A0D" value: 2 } + cme_mic { key: "A0F" value: 2 } + cme_mic { key: "A1D" value: 2 } + cme_mic { key: "A1G" value: 2 } + cme_mic { key: "A1L" value: 2 } + cme_mic { key: "A1M" value: 2 } + cme_mic { key: "A1P" value: 2 } + cme_mic { key: "A1Q" value: 2 } + cme_mic { key: "A1R" value: 2 } + cme_mic { key: "A1U" value: 2 } + cme_mic { key: "A1V" value: 2 } + cme_mic { key: "A1W" value: 2 } + cme_mic { key: "A1X" value: 2 } + cme_mic { key: "A32" value: 2 } + cme_mic { key: "A33" value: 2 } + cme_mic { key: "A38" value: 4 } + cme_mic { key: "A3C" value: 2 } + cme_mic { key: "A3G" value: 2 } + cme_mic { key: "A3M" value: 2 } + cme_mic { key: "A3N" value: 2 } + cme_mic { key: "A3Q" value: 2 } + cme_mic { key: "A3R" value: 2 } + cme_mic { key: "A42" value: 2 } + cme_mic { key: "A43" value: 2 } + cme_mic { key: "A46" value: 2 } + cme_mic { key: "A47" value: 2 } + cme_mic { key: "A49" value: 2 } + cme_mic { key: "A4L" value: 2 } + cme_mic { key: "A4M" value: 2 } + cme_mic { key: "A4N" value: 2 } + cme_mic { key: "A4P" value: 2 } + cme_mic { key: "A4Q" value: 2 } + cme_mic { key: "A4R" value: 2 } + cme_mic { key: "A50" value: 2 } + cme_mic { key: "A55" value: 2 } + cme_mic { key: "A58" value: 2 } + cme_mic { key: "A59" value: 2 } + cme_mic { key: "A5C" value: 2 } + cme_mic { key: "A5U" value: 2 } + cme_mic { key: "A6L" value: 2 } + cme_mic { key: "A6V" value: 2 } + cme_mic { key: "A6W" value: 2 } + cme_mic { key: "A6X" value: 2 } + cme_mic { key: "A7E" value: 2 } + cme_mic { key: "A7G" value: 2 } + cme_mic { key: "A7I" value: 2 } + cme_mic { key: "A7L" value: 2 } + cme_mic { key: "A7Q" value: 2 } + cme_mic { key: "A7Y" value: 2 } + cme_mic { key: "A81" value: 2 } + cme_mic { key: "A8C" value: 2 } + cme_mic { key: "A8G" value: 2 } + cme_mic { key: "A8I" value: 2 } + cme_mic { key: "A8J" value: 2 } + cme_mic { key: "A8K" value: 2 } + cme_mic { key: "A8L" value: 2 } + cme_mic { key: "A8M" value: 2 } + cme_mic { key: "A8N" value: 2 } + cme_mic { key: "A8O" value: 2 } + cme_mic { key: "A91" value: 2 } + cme_mic { key: "A9N" value: 2 } + cme_mic { key: "AA3" value: 2 } + cme_mic { key: "AA4" value: 2 } + cme_mic { key: "AA5" value: 2 } + cme_mic { key: "AA6" value: 2 } + cme_mic { key: "AA7" value: 2 } + cme_mic { key: "AA8" value: 2 } + cme_mic { key: "AA9" value: 2 } + cme_mic { key: "AAW" value: 2 } + cme_mic { key: "AB3" value: 2 } + cme_mic { key: "AB7" value: 2 } + cme_mic { key: "ABH" value: 2 } + cme_mic { key: "ABI" value: 2 } + cme_mic { key: "ABS" value: 2 } + cme_mic { key: "ABT" value: 2 } + cme_mic { key: "ABX" value: 2 } + cme_mic { key: "ABY" value: 2 } + cme_mic { key: "AC0" value: 2 } + cme_mic { key: "ACD" value: 1 } + cme_mic { key: "ACM" value: 2 } + cme_mic { key: "ACT" value: 2 } + cme_mic { key: "AD0" value: 2 } + cme_mic { key: "AD5" value: 2 } + cme_mic { key: "AD6" value: 2 } + cme_mic { key: "AD8" value: 2 } + cme_mic { key: "AD9" value: 2 } + cme_mic { key: "ADB" value: 2 } + cme_mic { key: "AE3" value: 2 } + cme_mic { key: "AE5" value: 2 } + cme_mic { key: "AE8" value: 2 } + cme_mic { key: "AE9" value: 2 } + cme_mic { key: "AEJ" value: 2 } + cme_mic { key: "AEP" value: 4 } + cme_mic { key: "AET" value: 2 } + cme_mic { key: "AEZ" value: 2 } + cme_mic { key: "AF" value: 2 } + cme_mic { key: "AF2" value: 2 } + cme_mic { key: "AF4" value: 2 } + cme_mic { key: "AF5" value: 2 } + cme_mic { key: "AFE" value: 2 } + cme_mic { key: "AFF" value: 2 } + cme_mic { key: "AFH" value: 2 } + cme_mic { key: "AFI" value: 2 } + cme_mic { key: "AFK" value: 2 } + cme_mic { key: "AFY" value: 2 } + cme_mic { key: "AGA" value: 2 } + cme_mic { key: "AGE" value: 2 } + cme_mic { key: "AGP" value: 2 } + cme_mic { key: "AGT" value: 2 } + cme_mic { key: "AGX" value: 2 } + cme_mic { key: "AH1" value: 2 } + cme_mic { key: "AH3" value: 2 } + cme_mic { key: "AHJ" value: 2 } + cme_mic { key: "AHL" value: 2 } + cme_mic { key: "AHM" value: 2 } + cme_mic { key: "AI1" value: 2 } + cme_mic { key: "AI2" value: 2 } + cme_mic { key: "AI3" value: 2 } + cme_mic { key: "AI4" value: 2 } + cme_mic { key: "AI5" value: 2 } + cme_mic { key: "AI6" value: 2 } + cme_mic { key: "AI7" value: 2 } + cme_mic { key: "AI9" value: 2 } + cme_mic { key: "AJ" value: 2 } + cme_mic { key: "AJ1" value: 2 } + cme_mic { key: "AJ2" value: 2 } + cme_mic { key: "AJ9" value: 2 } + cme_mic { key: "AJB" value: 2 } + cme_mic { key: "AJJ" value: 2 } + cme_mic { key: "AJL" value: 2 } + cme_mic { key: "AJP" value: 2 } + cme_mic { key: "AJR" value: 2 } + cme_mic { key: "AJS" value: 2 } + cme_mic { key: "AJU" value: 2 } + cme_mic { key: "AJY" value: 1 } + cme_mic { key: "AK1" value: 2 } + cme_mic { key: "AKL" value: 2 } + cme_mic { key: "AKR" value: 2 } + cme_mic { key: "AKS" value: 2 } + cme_mic { key: "AKX" value: 2 } + cme_mic { key: "AKZ" value: 2 } + cme_mic { key: "AL1" value: 2 } + cme_mic { key: "AL5" value: 2 } + cme_mic { key: "AL6" value: 2 } + cme_mic { key: "AL8" value: 2 } + cme_mic { key: "AL9" value: 2 } + cme_mic { key: "ALA" value: 4 } + cme_mic { key: "ALB" value: 4 } + cme_mic { key: "ALI" value: 4 } + cme_mic { key: "ALM" value: 2 } + cme_mic { key: "ALW" value: 2 } + cme_mic { key: "ALX" value: 2 } + cme_mic { key: "ALY" value: 2 } + cme_mic { key: "AM1" value: 2 } + cme_mic { key: "AML" value: 2 } + cme_mic { key: "AN1" value: 2 } + cme_mic { key: "ANC" value: 2 } + cme_mic { key: "ANE" value: 1 } + cme_mic { key: "ANL" value: 2 } + cme_mic { key: "ANT" value: 2 } + cme_mic { key: "AO1" value: 2 } + cme_mic { key: "AOB" value: 2 } + cme_mic { key: "AOH" value: 2 } + cme_mic { key: "AOJ" value: 2 } + cme_mic { key: "AOL" value: 2 } + cme_mic { key: "AP1" value: 2 } + cme_mic { key: "AP2" value: 2 } + cme_mic { key: "AP3" value: 2 } + cme_mic { key: "AP4" value: 2 } + cme_mic { key: "AP5" value: 2 } + cme_mic { key: "AP7" value: 2 } + cme_mic { key: "AP8" value: 2 } + cme_mic { key: "AP9" value: 2 } + cme_mic { key: "APA" value: 2 } + cme_mic { key: "APS" value: 2 } + cme_mic { key: "AQ5" value: 2 } + cme_mic { key: "AQ8" value: 2 } + cme_mic { key: "AQA" value: 2 } + cme_mic { key: "AQK" value: 2 } + cme_mic { key: "AR0" value: 2 } + cme_mic { key: "AR1" value: 2 } + cme_mic { key: "AR3" value: 2 } + cme_mic { key: "AR4" value: 2 } + cme_mic { key: "AR6" value: 2 } + cme_mic { key: "AR8" value: 2 } + cme_mic { key: "ARE" value: 2 } + cme_mic { key: "ARI" value: 2 } + cme_mic { key: "ARY" value: 2 } + cme_mic { key: "AS4" value: 2 } + cme_mic { key: "ASD" value: 2 } + cme_mic { key: "ASP" value: 2 } + cme_mic { key: "AT0" value: 2 } + cme_mic { key: "ATP" value: 2 } + cme_mic { key: "ATU" value: 2 } + cme_mic { key: "ATY" value: 2 } + cme_mic { key: "AU2" value: 2 } + cme_mic { key: "AU3" value: 2 } + cme_mic { key: "AU4" value: 2 } + cme_mic { key: "AU5" value: 2 } + cme_mic { key: "AU6" value: 2 } + cme_mic { key: "AUB" value: 2 } + cme_mic { key: "AUDJYC" value: 1 } + cme_mic { key: "AUDUSC" value: 1 } + cme_mic { key: "AUDUSD" value: 8 } + cme_mic { key: "AUDUSN" value: 1 } + cme_mic { key: "AUF" value: 2 } + cme_mic { key: "AUH" value: 2 } + cme_mic { key: "AUI" value: 2 } + cme_mic { key: "AUJ" value: 2 } + cme_mic { key: "AUP" value: 4 } + cme_mic { key: "AUS" value: 2 } + cme_mic { key: "AUT" value: 2 } + cme_mic { key: "AUW" value: 3 } + cme_mic { key: "AUY" value: 2 } + cme_mic { key: "AUZ" value: 2 } + cme_mic { key: "AV0" value: 2 } + cme_mic { key: "AVK" value: 2 } + cme_mic { key: "AVL" value: 2 } + cme_mic { key: "AVU" value: 2 } + cme_mic { key: "AVZ" value: 2 } + cme_mic { key: "AW" value: 3 } + cme_mic { key: "AW2" value: 2 } + cme_mic { key: "AW4" value: 2 } + cme_mic { key: "AW6" value: 2 } + cme_mic { key: "AWI" value: 3 } + cme_mic { key: "AWJ" value: 2 } + cme_mic { key: "AWQ" value: 2 } + cme_mic { key: "AWT" value: 3 } + cme_mic { key: "AX1" value: 2 } + cme_mic { key: "AXA" value: 2 } + cme_mic { key: "AXB" value: 2 } + cme_mic { key: "AY" value: 2 } + cme_mic { key: "AY1" value: 2 } + cme_mic { key: "AY3" value: 2 } + cme_mic { key: "AY4" value: 2 } + cme_mic { key: "AYH" value: 2 } + cme_mic { key: "AYV" value: 2 } + cme_mic { key: "AYX" value: 2 } + cme_mic { key: "AZ0" value: 2 } + cme_mic { key: "AZ1" value: 2 } + cme_mic { key: "AZ5" value: 2 } + cme_mic { key: "AZ7" value: 2 } + cme_mic { key: "AZ9" value: 2 } + cme_mic { key: "B0" value: 2 } + cme_mic { key: "B1" value: 2 } + cme_mic { key: "B1U" value: 3 } + cme_mic { key: "B2K" value: 2 } + cme_mic { key: "B6L" value: 2 } + cme_mic { key: "B7H" value: 2 } + cme_mic { key: "B8" value: 2 } + cme_mic { key: "BB" value: 2 } + cme_mic { key: "BBT" value: 2 } + cme_mic { key: "BBV" value: 1 } + cme_mic { key: "BCF" value: 3 } + cme_mic { key: "BCH" value: 3 } + cme_mic { key: "BDB" value: 2 } + cme_mic { key: "BDI" value: 1 } + cme_mic { key: "BDS" value: 6 } + cme_mic { key: "BFR" value: 2 } + cme_mic { key: "BG" value: 2 } + cme_mic { key: "BIO" value: 1 } + cme_mic { key: "BIT" value: 1 } + cme_mic { key: "BK" value: 2 } + cme_mic { key: "BOO" value: 2 } + cme_mic { key: "BOS" value: 1 } + cme_mic { key: "BR7" value: 2 } + cme_mic { key: "BSW" value: 3 } + cme_mic { key: "BTC" value: 1 } + cme_mic { key: "BTT" value: 1 } + cme_mic { key: "BUC" value: 2 } + cme_mic { key: "BUS" value: 2 } + cme_mic { key: "BUW" value: 7 } + cme_mic { key: "BWF" value: 3 } + cme_mic { key: "BWH" value: 3 } + cme_mic { key: "BZ" value: 2 } + cme_mic { key: "BZ1" value: 2 } + cme_mic { key: "BZ2" value: 2 } + cme_mic { key: "BZL" value: 2 } + cme_mic { key: "BZS" value: 2 } + cme_mic { key: "BZT" value: 2 } + cme_mic { key: "C2E" value: 2 } + cme_mic { key: "C3D" value: 2 } + cme_mic { key: "C4D" value: 2 } + cme_mic { key: "C4Z" value: 2 } + cme_mic { key: "C5D" value: 2 } + cme_mic { key: "C6C" value: 2 } + cme_mic { key: "C7C" value: 2 } + cme_mic { key: "C8C" value: 2 } + cme_mic { key: "C9C" value: 2 } + cme_mic { key: "CADJYC" value: 1 } + cme_mic { key: "CB" value: 1 } + cme_mic { key: "CBA" value: 2 } + cme_mic { key: "CBB" value: 2 } + cme_mic { key: "CBC" value: 2 } + cme_mic { key: "CBO" value: 2 } + cme_mic { key: "CC0" value: 2 } + cme_mic { key: "CC5" value: 2 } + cme_mic { key: "CCM" value: 2 } + cme_mic { key: "CF" value: 2 } + cme_mic { key: "CFB" value: 2 } + cme_mic { key: "CFC" value: 2 } + cme_mic { key: "CGB" value: 2 } + cme_mic { key: "CHB" value: 2 } + cme_mic { key: "CHI" value: 1 } + cme_mic { key: "CHL" value: 1 } + cme_mic { key: "CHP" value: 1 } + cme_mic { key: "CIL" value: 2 } + cme_mic { key: "CIN" value: 2 } + cme_mic { key: "CJ" value: 2 } + cme_mic { key: "CJY" value: 1 } + cme_mic { key: "CK" value: 2 } + cme_mic { key: "CKW" value: 3 } + cme_mic { key: "CL" value: 2 } + cme_mic { key: "CL1" value: 2 } + cme_mic { key: "CL2" value: 2 } + cme_mic { key: "CLL" value: 2 } + cme_mic { key: "CLS" value: 2 } + cme_mic { key: "CLT" value: 2 } + cme_mic { key: "CMF" value: 2 } + cme_mic { key: "CNH" value: 1 } + cme_mic { key: "COL" value: 2 } + cme_mic { key: "CPB" value: 2 } + cme_mic { key: "CPC" value: 1 } + cme_mic { key: "CPD" value: 2 } + cme_mic { key: "CPO" value: 1 } + cme_mic { key: "CPP" value: 2 } + cme_mic { key: "CRB" value: 2 } + cme_mic { key: "CRG" value: 2 } + cme_mic { key: "CSC" value: 1 } + cme_mic { key: "CSN" value: 2 } + cme_mic { key: "CSW" value: 2 } + cme_mic { key: "CSX" value: 2 } + cme_mic { key: "CTB" value: 1 } + cme_mic { key: "CTM" value: 1 } + cme_mic { key: "CTR" value: 1 } + cme_mic { key: "CU" value: 2 } + cme_mic { key: "CUP" value: 4 } + cme_mic { key: "CUS" value: 1 } + cme_mic { key: "CY" value: 2 } + cme_mic { key: "CZK" value: 1 } + cme_mic { key: "CZN" value: 2 } + cme_mic { key: "D0" value: 1 } + cme_mic { key: "D0X" value: 1 } + cme_mic { key: "D0Z" value: 1 } + cme_mic { key: "D1N" value: 2 } + cme_mic { key: "D2" value: 1 } + cme_mic { key: "D2L" value: 2 } + cme_mic { key: "D2X" value: 1 } + cme_mic { key: "D2Z" value: 1 } + cme_mic { key: "D3L" value: 2 } + cme_mic { key: "D4L" value: 2 } + cme_mic { key: "D7L" value: 2 } + cme_mic { key: "DAX" value: 2 } + cme_mic { key: "DAZ" value: 2 } + cme_mic { key: "DBB" value: 2 } + cme_mic { key: "DBI" value: 6 } + cme_mic { key: "DBL" value: 2 } + cme_mic { key: "DBR" value: 6 } + cme_mic { key: "DBT" value: 2 } + cme_mic { key: "DC" value: 1 } + cme_mic { key: "DCB" value: 2 } + cme_mic { key: "DCD" value: 6 } + cme_mic { key: "DCL" value: 2 } + cme_mic { key: "DCW" value: 2 } + cme_mic { key: "DDA" value: 6 } + cme_mic { key: "DDB" value: 6 } + cme_mic { key: "DDE" value: 6 } + cme_mic { key: "DDI" value: 6 } + cme_mic { key: "DDO" value: 6 } + cme_mic { key: "DEB" value: 2 } + cme_mic { key: "DEN" value: 1 } + cme_mic { key: "DEP" value: 2 } + cme_mic { key: "DES" value: 6 } + cme_mic { key: "DFL" value: 3 } + cme_mic { key: "DFT" value: 3 } + cme_mic { key: "DG2" value: 3 } + cme_mic { key: "DG3" value: 3 } + cme_mic { key: "DGH" value: 6 } + cme_mic { key: "DGI" value: 3 } + cme_mic { key: "DGS" value: 3 } + cme_mic { key: "DGT" value: 3 } + cme_mic { key: "DHA" value: 2 } + cme_mic { key: "DHE" value: 6 } + cme_mic { key: "DHI" value: 6 } + cme_mic { key: "DIH" value: 2 } + cme_mic { key: "DJR" value: 3 } + cme_mic { key: "DKR" value: 6 } + cme_mic { key: "DMB" value: 6 } + cme_mic { key: "DOB" value: 6 } + cme_mic { key: "DOP" value: 6 } + cme_mic { key: "DRB" value: 6 } + cme_mic { key: "DRS" value: 3 } + cme_mic { key: "DSD" value: 6 } + cme_mic { key: "DSE" value: 6 } + cme_mic { key: "DSG" value: 6 } + cme_mic { key: "DSI" value: 6 } + cme_mic { key: "DSM" value: 6 } + cme_mic { key: "DUA" value: 6 } + cme_mic { key: "DUP" value: 6 } + cme_mic { key: "DVE" value: 1 } + cme_mic { key: "DVT" value: 1 } + cme_mic { key: "DY" value: 1 } + cme_mic { key: "E1U" value: 3 } + cme_mic { key: "E4L" value: 2 } + cme_mic { key: "E6" value: 2 } + cme_mic { key: "E6M" value: 2 } + cme_mic { key: "E7" value: 1 } + cme_mic { key: "E9X" value: 2 } + cme_mic { key: "EAA" value: 2 } + cme_mic { key: "EAB" value: 2 } + cme_mic { key: "EAC" value: 2 } + cme_mic { key: "EAD" value: 1 } + cme_mic { key: "EAE" value: 2 } + cme_mic { key: "EAF" value: 2 } + cme_mic { key: "EAW" value: 2 } + cme_mic { key: "EBE" value: 2 } + cme_mic { key: "ECD" value: 1 } + cme_mic { key: "ECK" value: 1 } + cme_mic { key: "EDP" value: 4 } + cme_mic { key: "EFF" value: 2 } + cme_mic { key: "EFM" value: 2 } + cme_mic { key: "EGB" value: 2 } + cme_mic { key: "EGN" value: 2 } + cme_mic { key: "EH" value: 3 } + cme_mic { key: "EHF" value: 1 } + cme_mic { key: "EI" value: 1 } + cme_mic { key: "EIT" value: 1 } + cme_mic { key: "EJL" value: 2 } + cme_mic { key: "EL1" value: 2 } + cme_mic { key: "EMC" value: 2 } + cme_mic { key: "EMD" value: 1 } + cme_mic { key: "EMT" value: 1 } + cme_mic { key: "EMW" value: 2 } + cme_mic { key: "EN" value: 2 } + cme_mic { key: "ENK" value: 1 } + cme_mic { key: "ENP" value: 2 } + cme_mic { key: "ENS" value: 2 } + cme_mic { key: "ENY" value: 1 } + cme_mic { key: "EO1" value: 2 } + cme_mic { key: "EOB" value: 2 } + cme_mic { key: "EP1" value: 2 } + cme_mic { key: "EPN" value: 2 } + cme_mic { key: "EPZ" value: 1 } + cme_mic { key: "ERL" value: 2 } + cme_mic { key: "ES" value: 1 } + cme_mic { key: "ESB" value: 2 } + cme_mic { key: "ESK" value: 1 } + cme_mic { key: "ESQ" value: 1 } + cme_mic { key: "ESS" value: 2 } + cme_mic { key: "EST" value: 1 } + cme_mic { key: "ETT" value: 1 } + cme_mic { key: "EUB" value: 2 } + cme_mic { key: "EUL" value: 2 } + cme_mic { key: "EURADC" value: 1 } + cme_mic { key: "EURBPC" value: 1 } + cme_mic { key: "EURBPN" value: 1 } + cme_mic { key: "EURJYC" value: 1 } + cme_mic { key: "EURSFC" value: 1 } + cme_mic { key: "EURUSC" value: 1 } + cme_mic { key: "EURUSD" value: 8 } + cme_mic { key: "EURUSN" value: 1 } + cme_mic { key: "EVC" value: 2 } + cme_mic { key: "EWB" value: 2 } + cme_mic { key: "EWG" value: 2 } + cme_mic { key: "EWN" value: 2 } + cme_mic { key: "EXR" value: 2 } + cme_mic { key: "F1U" value: 3 } + cme_mic { key: "F3" value: 2 } + cme_mic { key: "FAL" value: 2 } + cme_mic { key: "FBD" value: 2 } + cme_mic { key: "FBT" value: 2 } + cme_mic { key: "FCB" value: 2 } + cme_mic { key: "FCN" value: 2 } + cme_mic { key: "FEF" value: 2 } + cme_mic { key: "FEW" value: 2 } + cme_mic { key: "FL" value: 2 } + cme_mic { key: "FLB" value: 2 } + cme_mic { key: "FLP" value: 2 } + cme_mic { key: "FO" value: 2 } + cme_mic { key: "FOA" value: 2 } + cme_mic { key: "FOC" value: 2 } + cme_mic { key: "FOM" value: 2 } + cme_mic { key: "FOR" value: 2 } + cme_mic { key: "FRC" value: 2 } + cme_mic { key: "FRS" value: 2 } + cme_mic { key: "FSF" value: 2 } + cme_mic { key: "FSS" value: 2 } + cme_mic { key: "FT1" value: 1 } + cme_mic { key: "FT5" value: 1 } + cme_mic { key: "FTB" value: 1 } + cme_mic { key: "FTC" value: 1 } + cme_mic { key: "FTL" value: 2 } + cme_mic { key: "FTT" value: 1 } + cme_mic { key: "FTU" value: 1 } + cme_mic { key: "FVB" value: 2 } + cme_mic { key: "FXBP" value: 8 } + cme_mic { key: "FZE" value: 3 } + cme_mic { key: "G0" value: 1 } + cme_mic { key: "G0K" value: 1 } + cme_mic { key: "G0N" value: 1 } + cme_mic { key: "G2" value: 1 } + cme_mic { key: "G2K" value: 1 } + cme_mic { key: "G2N" value: 1 } + cme_mic { key: "GB" value: 4 } + cme_mic { key: "GBA" value: 2 } + cme_mic { key: "GBB" value: 2 } + cme_mic { key: "GBC" value: 4 } + cme_mic { key: "GBPUSC" value: 1 } + cme_mic { key: "GBPUSD" value: 8 } + cme_mic { key: "GBPUSN" value: 1 } + cme_mic { key: "GBR" value: 2 } + cme_mic { key: "GC" value: 4 } + cme_mic { key: "GC7" value: 4 } + cme_mic { key: "GCB" value: 2 } + cme_mic { key: "GCC" value: 2 } + cme_mic { key: "GCD" value: 4 } + cme_mic { key: "GCG" value: 2 } + cme_mic { key: "GCI" value: 2 } + cme_mic { key: "GCK" value: 4 } + cme_mic { key: "GCM" value: 2 } + cme_mic { key: "GCT" value: 4 } + cme_mic { key: "GCU" value: 2 } + cme_mic { key: "GD" value: 1 } + cme_mic { key: "GDH" value: 2 } + cme_mic { key: "GDK" value: 1 } + cme_mic { key: "GDL" value: 2 } + cme_mic { key: "GE" value: 1 } + cme_mic { key: "GES" value: 2 } + cme_mic { key: "GF" value: 1 } + cme_mic { key: "GFC" value: 2 } + cme_mic { key: "GFT" value: 1 } + cme_mic { key: "GHB" value: 2 } + cme_mic { key: "GHS" value: 2 } + cme_mic { key: "GIE" value: 1 } + cme_mic { key: "GKS" value: 2 } + cme_mic { key: "GLB" value: 1 } + cme_mic { key: "GLI" value: 2 } + cme_mic { key: "GNB" value: 2 } + cme_mic { key: "GNF" value: 1 } + cme_mic { key: "GNL" value: 2 } + cme_mic { key: "GNO" value: 2 } + cme_mic { key: "GNS" value: 2 } + cme_mic { key: "GOC" value: 2 } + cme_mic { key: "GPS" value: 4 } + cme_mic { key: "GSI" value: 2 } + cme_mic { key: "GSP" value: 4 } + cme_mic { key: "GSR" value: 4 } + cme_mic { key: "GSW" value: 2 } + cme_mic { key: "GUD" value: 2 } + cme_mic { key: "GY" value: 2 } + cme_mic { key: "GZ" value: 2 } + cme_mic { key: "H0" value: 1 } + cme_mic { key: "H0X" value: 1 } + cme_mic { key: "H0Z" value: 1 } + cme_mic { key: "H1" value: 1 } + cme_mic { key: "H1X" value: 1 } + cme_mic { key: "H1Z" value: 1 } + cme_mic { key: "H2" value: 1 } + cme_mic { key: "H2L" value: 2 } + cme_mic { key: "H2X" value: 1 } + cme_mic { key: "H2Z" value: 1 } + cme_mic { key: "H3" value: 1 } + cme_mic { key: "H3X" value: 1 } + cme_mic { key: "H3Z" value: 1 } + cme_mic { key: "H4" value: 1 } + cme_mic { key: "H4X" value: 1 } + cme_mic { key: "H4Z" value: 1 } + cme_mic { key: "H5" value: 1 } + cme_mic { key: "H5L" value: 2 } + cme_mic { key: "H5X" value: 1 } + cme_mic { key: "H5Z" value: 1 } + cme_mic { key: "HB" value: 2 } + cme_mic { key: "HE" value: 1 } + cme_mic { key: "HET" value: 1 } + cme_mic { key: "HG" value: 4 } + cme_mic { key: "HG0" value: 4 } + cme_mic { key: "HGS" value: 4 } + cme_mic { key: "HGT" value: 4 } + cme_mic { key: "HH" value: 2 } + cme_mic { key: "HHE" value: 2 } + cme_mic { key: "HHT" value: 2 } + cme_mic { key: "HIA" value: 2 } + cme_mic { key: "HIB" value: 2 } + cme_mic { key: "HIL" value: 2 } + cme_mic { key: "HJC" value: 2 } + cme_mic { key: "HO" value: 2 } + cme_mic { key: "HO2" value: 2 } + cme_mic { key: "HO4" value: 2 } + cme_mic { key: "HOA" value: 2 } + cme_mic { key: "HOB" value: 2 } + cme_mic { key: "HOL" value: 2 } + cme_mic { key: "HOS" value: 2 } + cme_mic { key: "HOT" value: 2 } + cme_mic { key: "HP" value: 2 } + cme_mic { key: "HPD" value: 2 } + cme_mic { key: "HPE" value: 2 } + cme_mic { key: "HQ" value: 1 } + cme_mic { key: "HQX" value: 1 } + cme_mic { key: "HQZ" value: 1 } + cme_mic { key: "HRC" value: 2 } + cme_mic { key: "HS" value: 1 } + cme_mic { key: "HSX" value: 1 } + cme_mic { key: "HSZ" value: 1 } + cme_mic { key: "HTA" value: 2 } + cme_mic { key: "HTB" value: 2 } + cme_mic { key: "HTT" value: 2 } + cme_mic { key: "HUF" value: 1 } + cme_mic { key: "HWA" value: 2 } + cme_mic { key: "IBB" value: 1 } + cme_mic { key: "IBE" value: 2 } + cme_mic { key: "IBS" value: 2 } + cme_mic { key: "IBV" value: 1 } + cme_mic { key: "IC" value: 5 } + cme_mic { key: "ICI" value: 2 } + cme_mic { key: "IDL" value: 2 } + cme_mic { key: "IH" value: 5 } + cme_mic { key: "ILB" value: 2 } + cme_mic { key: "ILS" value: 1 } + cme_mic { key: "IN" value: 2 } + cme_mic { key: "IP" value: 5 } + cme_mic { key: "IPO" value: 1 } + cme_mic { key: "IPT" value: 1 } + cme_mic { key: "IS" value: 5 } + cme_mic { key: "ITB" value: 2 } + cme_mic { key: "ITP" value: 2 } + cme_mic { key: "IW" value: 5 } + cme_mic { key: "J4L" value: 2 } + cme_mic { key: "J7" value: 1 } + cme_mic { key: "JA" value: 2 } + cme_mic { key: "JDL" value: 2 } + cme_mic { key: "JE" value: 2 } + cme_mic { key: "JET" value: 2 } + cme_mic { key: "JFB" value: 2 } + cme_mic { key: "JFC" value: 2 } + cme_mic { key: "JKM" value: 2 } + cme_mic { key: "JML" value: 2 } + cme_mic { key: "JNC" value: 2 } + cme_mic { key: "JNG" value: 2 } + cme_mic { key: "JNL" value: 2 } + cme_mic { key: "JPP" value: 2 } + cme_mic { key: "JQ" value: 1 } + cme_mic { key: "JTB" value: 2 } + cme_mic { key: "K0" value: 1 } + cme_mic { key: "K0K" value: 1 } + cme_mic { key: "K0N" value: 1 } + cme_mic { key: "K1" value: 1 } + cme_mic { key: "K1K" value: 1 } + cme_mic { key: "K1N" value: 1 } + cme_mic { key: "K2" value: 1 } + cme_mic { key: "K2K" value: 1 } + cme_mic { key: "K2L" value: 2 } + cme_mic { key: "K2N" value: 1 } + cme_mic { key: "K3" value: 1 } + cme_mic { key: "K3K" value: 1 } + cme_mic { key: "K3L" value: 2 } + cme_mic { key: "K3N" value: 1 } + cme_mic { key: "K4" value: 1 } + cme_mic { key: "K4K" value: 1 } + cme_mic { key: "K4L" value: 2 } + cme_mic { key: "K4N" value: 1 } + cme_mic { key: "K5" value: 1 } + cme_mic { key: "K5K" value: 1 } + cme_mic { key: "K5N" value: 1 } + cme_mic { key: "KE" value: 3 } + cme_mic { key: "KET" value: 3 } + cme_mic { key: "KQ" value: 1 } + cme_mic { key: "KQK" value: 1 } + cme_mic { key: "KQN" value: 1 } + cme_mic { key: "KRW" value: 1 } + cme_mic { key: "KS" value: 1 } + cme_mic { key: "KSK" value: 1 } + cme_mic { key: "KSN" value: 1 } + cme_mic { key: "KT" value: 2 } + cme_mic { key: "L3L" value: 2 } + cme_mic { key: "LAF" value: 2 } + cme_mic { key: "LAP" value: 2 } + cme_mic { key: "LAV" value: 1 } + cme_mic { key: "LAX" value: 1 } + cme_mic { key: "LBB" value: 2 } + cme_mic { key: "LBS" value: 1 } + cme_mic { key: "LCF" value: 2 } + cme_mic { key: "LE" value: 1 } + cme_mic { key: "LED" value: 4 } + cme_mic { key: "LEL" value: 2 } + cme_mic { key: "LET" value: 1 } + cme_mic { key: "LHV" value: 2 } + cme_mic { key: "LL" value: 2 } + cme_mic { key: "LLR" value: 2 } + cme_mic { key: "LPE" value: 2 } + cme_mic { key: "LPS" value: 2 } + cme_mic { key: "LSF" value: 4 } + cme_mic { key: "LSW" value: 2 } + cme_mic { key: "LT" value: 2 } + cme_mic { key: "LWB" value: 2 } + cme_mic { key: "M5F" value: 2 } + cme_mic { key: "M6A" value: 1 } + cme_mic { key: "M6B" value: 1 } + cme_mic { key: "M6E" value: 1 } + cme_mic { key: "MAA" value: 2 } + cme_mic { key: "MAB" value: 2 } + cme_mic { key: "MAC" value: 2 } + cme_mic { key: "MAE" value: 2 } + cme_mic { key: "MAF" value: 2 } + cme_mic { key: "MAS" value: 2 } + cme_mic { key: "MB" value: 2 } + cme_mic { key: "MBB" value: 2 } + cme_mic { key: "MBC" value: 2 } + cme_mic { key: "MBE" value: 2 } + cme_mic { key: "MBL" value: 2 } + cme_mic { key: "MBM" value: 2 } + cme_mic { key: "MBR" value: 2 } + cme_mic { key: "MBS" value: 2 } + cme_mic { key: "MCC" value: 2 } + cme_mic { key: "MCD" value: 1 } + cme_mic { key: "MCT" value: 2 } + cme_mic { key: "MCX" value: 3 } + cme_mic { key: "MDB" value: 2 } + cme_mic { key: "MDD" value: 2 } + cme_mic { key: "MDL" value: 2 } + cme_mic { key: "MDM" value: 2 } + cme_mic { key: "MDR" value: 2 } + cme_mic { key: "ME" value: 2 } + cme_mic { key: "MEB" value: 2 } + cme_mic { key: "MEE" value: 2 } + cme_mic { key: "MEF" value: 2 } + cme_mic { key: "MEL" value: 2 } + cme_mic { key: "MEO" value: 2 } + cme_mic { key: "MEW" value: 2 } + cme_mic { key: "MFB" value: 2 } + cme_mic { key: "MFD" value: 2 } + cme_mic { key: "MFF" value: 2 } + cme_mic { key: "MFP" value: 2 } + cme_mic { key: "MFR" value: 2 } + cme_mic { key: "MGB" value: 2 } + cme_mic { key: "MGC" value: 4 } + cme_mic { key: "MGF" value: 2 } + cme_mic { key: "MGH" value: 2 } + cme_mic { key: "MGN" value: 2 } + cme_mic { key: "MGO" value: 2 } + cme_mic { key: "MGS" value: 2 } + cme_mic { key: "MH" value: 2 } + cme_mic { key: "MHE" value: 2 } + cme_mic { key: "MHS" value: 2 } + cme_mic { key: "MIA" value: 1 } + cme_mic { key: "MIP" value: 2 } + cme_mic { key: "MIR" value: 1 } + cme_mic { key: "MJB" value: 2 } + cme_mic { key: "MJC" value: 2 } + cme_mic { key: "MJN" value: 2 } + cme_mic { key: "MJP" value: 4 } + cme_mic { key: "MJY" value: 1 } + cme_mic { key: "MKC" value: 3 } + cme_mic { key: "MLD" value: 2 } + cme_mic { key: "MM" value: 2 } + cme_mic { key: "MME" value: 2 } + cme_mic { key: "MMF" value: 2 } + cme_mic { key: "MMO" value: 2 } + cme_mic { key: "MMP" value: 2 } + cme_mic { key: "MMR" value: 2 } + cme_mic { key: "MNB" value: 2 } + cme_mic { key: "MNC" value: 2 } + cme_mic { key: "MNE" value: 2 } + cme_mic { key: "MNG" value: 2 } + cme_mic { key: "MNH" value: 1 } + cme_mic { key: "MNS" value: 2 } + cme_mic { key: "MNT" value: 2 } + cme_mic { key: "MO" value: 2 } + cme_mic { key: "MOA" value: 2 } + cme_mic { key: "MOI" value: 2 } + cme_mic { key: "MPA" value: 2 } + cme_mic { key: "MPE" value: 2 } + cme_mic { key: "MPP" value: 2 } + cme_mic { key: "MPS" value: 2 } + cme_mic { key: "MPX" value: 2 } + cme_mic { key: "MQ" value: 2 } + cme_mic { key: "MRI" value: 2 } + cme_mic { key: "MRT" value: 2 } + cme_mic { key: "MSB" value: 2 } + cme_mic { key: "MSD" value: 2 } + cme_mic { key: "MSE" value: 2 } + cme_mic { key: "MSF" value: 1 } + cme_mic { key: "MSG" value: 2 } + cme_mic { key: "MT2" value: 2 } + cme_mic { key: "MTB" value: 2 } + cme_mic { key: "MTF" value: 2 } + cme_mic { key: "MTI" value: 2 } + cme_mic { key: "MTS" value: 2 } + cme_mic { key: "MUD" value: 2 } + cme_mic { key: "MWE" value: 5 } + cme_mic { key: "MX" value: 2 } + cme_mic { key: "MXB" value: 2 } + cme_mic { key: "MXL" value: 2 } + cme_mic { key: "MXR" value: 2 } + cme_mic { key: "N1B" value: 2 } + cme_mic { key: "N1U" value: 3 } + cme_mic { key: "N3L" value: 2 } + cme_mic { key: "N3P" value: 2 } + cme_mic { key: "N9L" value: 2 } + cme_mic { key: "NBB" value: 2 } + cme_mic { key: "NBD" value: 2 } + cme_mic { key: "NBP" value: 2 } + cme_mic { key: "NCC" value: 2 } + cme_mic { key: "NCD" value: 2 } + cme_mic { key: "NCF" value: 2 } + cme_mic { key: "NCO" value: 2 } + cme_mic { key: "NCP" value: 2 } + cme_mic { key: "NEN" value: 2 } + cme_mic { key: "NEO" value: 2 } + cme_mic { key: "NEP" value: 2 } + cme_mic { key: "NFC" value: 2 } + cme_mic { key: "NFD" value: 2 } + cme_mic { key: "NFG" value: 2 } + cme_mic { key: "NFN" value: 2 } + cme_mic { key: "NFO" value: 2 } + cme_mic { key: "NG" value: 2 } + cme_mic { key: "NGT" value: 2 } + cme_mic { key: "NHH" value: 2 } + cme_mic { key: "NHN" value: 2 } + cme_mic { key: "NHO" value: 2 } + cme_mic { key: "NHP" value: 2 } + cme_mic { key: "NIY" value: 1 } + cme_mic { key: "NJ" value: 2 } + cme_mic { key: "NKD" value: 1 } + cme_mic { key: "NKN" value: 2 } + cme_mic { key: "NL" value: 2 } + cme_mic { key: "NLS" value: 2 } + cme_mic { key: "NMO" value: 2 } + cme_mic { key: "NMP" value: 2 } + cme_mic { key: "NN" value: 2 } + cme_mic { key: "NNE" value: 2 } + cme_mic { key: "NNP" value: 2 } + cme_mic { key: "NNT" value: 2 } + cme_mic { key: "NOD" value: 2 } + cme_mic { key: "NOI" value: 2 } + cme_mic { key: "NOK" value: 1 } + cme_mic { key: "NOO" value: 2 } + cme_mic { key: "NPG" value: 2 } + cme_mic { key: "NQ" value: 1 } + cme_mic { key: "NQT" value: 1 } + cme_mic { key: "NR" value: 2 } + cme_mic { key: "NRO" value: 2 } + cme_mic { key: "NRP" value: 2 } + cme_mic { key: "NRR" value: 2 } + cme_mic { key: "NSO" value: 2 } + cme_mic { key: "NSP" value: 2 } + cme_mic { key: "NTP" value: 2 } + cme_mic { key: "NW" value: 2 } + cme_mic { key: "NWO" value: 2 } + cme_mic { key: "NWP" value: 2 } + cme_mic { key: "NX" value: 2 } + cme_mic { key: "NY" value: 2 } + cme_mic { key: "NYA" value: 2 } + cme_mic { key: "NYB" value: 2 } + cme_mic { key: "NYF" value: 2 } + cme_mic { key: "NYG" value: 2 } + cme_mic { key: "NYH" value: 2 } + cme_mic { key: "NYM" value: 1 } + cme_mic { key: "NYP" value: 2 } + cme_mic { key: "NYR" value: 2 } + cme_mic { key: "NYS" value: 2 } + cme_mic { key: "NYT" value: 2 } + cme_mic { key: "NZDUSC" value: 1 } + cme_mic { key: "NZN" value: 2 } + cme_mic { key: "OAD" value: 2 } + cme_mic { key: "ODO" value: 6 } + cme_mic { key: "OFF" value: 2 } + cme_mic { key: "OML" value: 2 } + cme_mic { key: "OMM" value: 2 } + cme_mic { key: "OOD" value: 2 } + cme_mic { key: "OPF" value: 1 } + cme_mic { key: "OPO" value: 2 } + cme_mic { key: "OPS" value: 1 } + cme_mic { key: "OQ1" value: 6 } + cme_mic { key: "OQB" value: 6 } + cme_mic { key: "OQD" value: 6 } + cme_mic { key: "OVL" value: 2 } + cme_mic { key: "PA" value: 2 } + cme_mic { key: "PAL" value: 2 } + cme_mic { key: "PBO" value: 2 } + cme_mic { key: "PBP" value: 2 } + cme_mic { key: "PCO" value: 2 } + cme_mic { key: "PCP" value: 2 } + cme_mic { key: "PD" value: 2 } + cme_mic { key: "PDJ" value: 2 } + cme_mic { key: "PDL" value: 2 } + cme_mic { key: "PE" value: 2 } + cme_mic { key: "PEL" value: 2 } + cme_mic { key: "PEX" value: 2 } + cme_mic { key: "PF" value: 2 } + cme_mic { key: "PFO" value: 2 } + cme_mic { key: "PFP" value: 2 } + cme_mic { key: "PGG" value: 2 } + cme_mic { key: "PGN" value: 2 } + cme_mic { key: "PH" value: 2 } + cme_mic { key: "PHF" value: 2 } + cme_mic { key: "PHO" value: 2 } + cme_mic { key: "PHP" value: 2 } + cme_mic { key: "PIO" value: 2 } + cme_mic { key: "PJY" value: 1 } + cme_mic { key: "PKP" value: 2 } + cme_mic { key: "PL" value: 2 } + cme_mic { key: "PLN" value: 1 } + cme_mic { key: "PLO" value: 2 } + cme_mic { key: "PLP" value: 2 } + cme_mic { key: "PLT" value: 2 } + cme_mic { key: "PM" value: 2 } + cme_mic { key: "PMF" value: 2 } + cme_mic { key: "PMO" value: 2 } + cme_mic { key: "PMP" value: 2 } + cme_mic { key: "PNF" value: 2 } + cme_mic { key: "PNL" value: 2 } + cme_mic { key: "POC" value: 2 } + cme_mic { key: "POG" value: 1 } + cme_mic { key: "POL" value: 2 } + cme_mic { key: "PPL" value: 2 } + cme_mic { key: "PPP" value: 2 } + cme_mic { key: "PPS" value: 2 } + cme_mic { key: "PPW" value: 2 } + cme_mic { key: "PQO" value: 2 } + cme_mic { key: "PQP" value: 2 } + cme_mic { key: "PRO" value: 2 } + cme_mic { key: "PRP" value: 2 } + cme_mic { key: "PSF" value: 1 } + cme_mic { key: "PSO" value: 2 } + cme_mic { key: "PSP" value: 2 } + cme_mic { key: "PTL" value: 2 } + cme_mic { key: "PTO" value: 2 } + cme_mic { key: "PUO" value: 2 } + cme_mic { key: "PUP" value: 2 } + cme_mic { key: "PVL" value: 2 } + cme_mic { key: "PVO" value: 2 } + cme_mic { key: "PVP" value: 2 } + cme_mic { key: "PW" value: 2 } + cme_mic { key: "PWL" value: 2 } + cme_mic { key: "PXO" value: 2 } + cme_mic { key: "PXP" value: 2 } + cme_mic { key: "PYO" value: 2 } + cme_mic { key: "PYP" value: 2 } + cme_mic { key: "PZO" value: 2 } + cme_mic { key: "PZP" value: 2 } + cme_mic { key: "QC" value: 4 } + cme_mic { key: "QCN" value: 1 } + cme_mic { key: "QG" value: 2 } + cme_mic { key: "QH" value: 2 } + cme_mic { key: "QI" value: 4 } + cme_mic { key: "QM" value: 2 } + cme_mic { key: "QO" value: 4 } + cme_mic { key: "QP" value: 2 } + cme_mic { key: "QU" value: 2 } + cme_mic { key: "QVL" value: 2 } + cme_mic { key: "QXB" value: 2 } + cme_mic { key: "R1T" value: 1 } + cme_mic { key: "R2G" value: 1 } + cme_mic { key: "R2V" value: 1 } + cme_mic { key: "R7L" value: 2 } + cme_mic { key: "RB" value: 2 } + cme_mic { key: "RBB" value: 2 } + cme_mic { key: "RBF" value: 2 } + cme_mic { key: "RBG" value: 2 } + cme_mic { key: "RBL" value: 2 } + cme_mic { key: "RBS" value: 2 } + cme_mic { key: "RBT" value: 2 } + cme_mic { key: "RE" value: 2 } + cme_mic { key: "REB" value: 2 } + cme_mic { key: "REX" value: 3 } + cme_mic { key: "RF" value: 1 } + cme_mic { key: "RGF" value: 2 } + cme_mic { key: "RGI" value: 2 } + cme_mic { key: "RGT" value: 1 } + cme_mic { key: "RHB" value: 2 } + cme_mic { key: "RHS" value: 2 } + cme_mic { key: "RKA" value: 2 } + cme_mic { key: "RLT" value: 1 } + cme_mic { key: "RLX" value: 2 } + cme_mic { key: "RMB" value: 1 } + cme_mic { key: "RME" value: 1 } + cme_mic { key: "RP" value: 1 } + cme_mic { key: "RRE" value: 1 } + cme_mic { key: "RS1" value: 1 } + cme_mic { key: "RSG" value: 1 } + cme_mic { key: "RSV" value: 1 } + cme_mic { key: "RT" value: 2 } + cme_mic { key: "RTY" value: 1 } + cme_mic { key: "RVR" value: 2 } + cme_mic { key: "RVT" value: 1 } + cme_mic { key: "RX" value: 3 } + cme_mic { key: "RY" value: 1 } + cme_mic { key: "RYA" value: 2 } + cme_mic { key: "S1U" value: 3 } + cme_mic { key: "SBT" value: 3 } + cme_mic { key: "SD" value: 2 } + cme_mic { key: "SDA" value: 1 } + cme_mic { key: "SDD" value: 2 } + cme_mic { key: "SDG" value: 1 } + cme_mic { key: "SDI" value: 1 } + cme_mic { key: "SDM" value: 2 } + cme_mic { key: "SE" value: 2 } + cme_mic { key: "SE2" value: 1 } + cme_mic { key: "SE3" value: 1 } + cme_mic { key: "SEK" value: 1 } + cme_mic { key: "SES" value: 1 } + cme_mic { key: "SF1" value: 2 } + cme_mic { key: "SF3" value: 2 } + cme_mic { key: "SFB" value: 2 } + cme_mic { key: "SFR" value: 1 } + cme_mic { key: "SG" value: 1 } + cme_mic { key: "SGB" value: 2 } + cme_mic { key: "SGE" value: 2 } + cme_mic { key: "SGF" value: 2 } + cme_mic { key: "SGG" value: 2 } + cme_mic { key: "SGT" value: 1 } + cme_mic { key: "SI" value: 4 } + cme_mic { key: "SIL" value: 4 } + cme_mic { key: "SIR" value: 1 } + cme_mic { key: "SIT" value: 4 } + cme_mic { key: "SJY" value: 1 } + cme_mic { key: "SLP" value: 1 } + cme_mic { key: "SLT" value: 1 } + cme_mic { key: "SMC" value: 1 } + cme_mic { key: "SMT" value: 1 } + cme_mic { key: "SMU" value: 2 } + cme_mic { key: "SMW" value: 2 } + cme_mic { key: "SP" value: 1 } + cme_mic { key: "SR1" value: 1 } + cme_mic { key: "SR3" value: 1 } + cme_mic { key: "SRT" value: 2 } + cme_mic { key: "SSI" value: 2 } + cme_mic { key: "SSP" value: 4 } + cme_mic { key: "SSW" value: 2 } + cme_mic { key: "STI" value: 2 } + cme_mic { key: "STR" value: 2 } + cme_mic { key: "STS" value: 2 } + cme_mic { key: "STT" value: 2 } + cme_mic { key: "STY" value: 2 } + cme_mic { key: "SU" value: 1 } + cme_mic { key: "SUT" value: 1 } + cme_mic { key: "T04" value: 2 } + cme_mic { key: "T05" value: 2 } + cme_mic { key: "T06" value: 2 } + cme_mic { key: "T07" value: 2 } + cme_mic { key: "T10" value: 2 } + cme_mic { key: "T11" value: 2 } + cme_mic { key: "T12" value: 2 } + cme_mic { key: "T13" value: 2 } + cme_mic { key: "T14" value: 2 } + cme_mic { key: "T17" value: 2 } + cme_mic { key: "T18" value: 2 } + cme_mic { key: "T19" value: 2 } + cme_mic { key: "T1U" value: 3 } + cme_mic { key: "T27" value: 2 } + cme_mic { key: "T28" value: 2 } + cme_mic { key: "T29" value: 2 } + cme_mic { key: "T2B" value: 2 } + cme_mic { key: "T2D" value: 2 } + cme_mic { key: "T2M" value: 2 } + cme_mic { key: "T30" value: 2 } + cme_mic { key: "T31" value: 2 } + cme_mic { key: "T3B" value: 2 } + cme_mic { key: "T3L" value: 2 } + cme_mic { key: "T4B" value: 2 } + cme_mic { key: "T4D" value: 2 } + cme_mic { key: "T5B" value: 2 } + cme_mic { key: "T5C" value: 2 } + cme_mic { key: "T6B" value: 2 } + cme_mic { key: "T7K" value: 2 } + cme_mic { key: "T8B" value: 2 } + cme_mic { key: "T9B" value: 2 } + cme_mic { key: "TB2" value: 2 } + cme_mic { key: "TC" value: 2 } + cme_mic { key: "TC6" value: 2 } + cme_mic { key: "TC9" value: 2 } + cme_mic { key: "TD3" value: 2 } + cme_mic { key: "TD8" value: 2 } + cme_mic { key: "TGT" value: 1 } + cme_mic { key: "TH" value: 2 } + cme_mic { key: "THB" value: 2 } + cme_mic { key: "THD" value: 2 } + cme_mic { key: "TIC" value: 2 } + cme_mic { key: "TIO" value: 2 } + cme_mic { key: "TK" value: 2 } + cme_mic { key: "TKB" value: 2 } + cme_mic { key: "TL" value: 2 } + cme_mic { key: "TLB" value: 2 } + cme_mic { key: "TLD" value: 2 } + cme_mic { key: "TM" value: 2 } + cme_mic { key: "TMB" value: 2 } + cme_mic { key: "TMD" value: 2 } + cme_mic { key: "TMT" value: 1 } + cme_mic { key: "TN" value: 3 } + cme_mic { key: "TPB" value: 1 } + cme_mic { key: "TPY" value: 1 } + cme_mic { key: "TRB" value: 1 } + cme_mic { key: "TRI" value: 1 } + cme_mic { key: "TRL" value: 1 } + cme_mic { key: "TRM" value: 1 } + cme_mic { key: "TRY" value: 1 } + cme_mic { key: "TRZ" value: 2 } + cme_mic { key: "TST" value: 1 } + cme_mic { key: "TT" value: 2 } + cme_mic { key: "TTD" value: 2 } + cme_mic { key: "TTE" value: 2 } + cme_mic { key: "TTF" value: 2 } + cme_mic { key: "TW" value: 2 } + cme_mic { key: "TXN" value: 2 } + cme_mic { key: "TZ6" value: 2 } + cme_mic { key: "U7" value: 2 } + cme_mic { key: "U9" value: 2 } + cme_mic { key: "UA" value: 2 } + cme_mic { key: "UB" value: 3 } + cme_mic { key: "UCM" value: 2 } + cme_mic { key: "UDL" value: 2 } + cme_mic { key: "UFG" value: 3 } + cme_mic { key: "UFN" value: 3 } + cme_mic { key: "UFU" value: 3 } + cme_mic { key: "UFZ" value: 3 } + cme_mic { key: "UGG" value: 2 } + cme_mic { key: "UKG" value: 2 } + cme_mic { key: "ULB" value: 2 } + cme_mic { key: "UMB" value: 2 } + cme_mic { key: "UMD" value: 2 } + cme_mic { key: "UME" value: 3 } + cme_mic { key: "UN" value: 2 } + cme_mic { key: "UNB" value: 2 } + cme_mic { key: "UNS" value: 2 } + cme_mic { key: "UR" value: 2 } + cme_mic { key: "USDBRL" value: 1 } + cme_mic { key: "USDCAC" value: 1 } + cme_mic { key: "USDCAD" value: 8 } + cme_mic { key: "USDCAN" value: 1 } + cme_mic { key: "USDCKC" value: 1 } + cme_mic { key: "USDCLP" value: 1 } + cme_mic { key: "USDCNY" value: 1 } + cme_mic { key: "USDCOP" value: 1 } + cme_mic { key: "USDDKC" value: 1 } + cme_mic { key: "USDHFC" value: 1 } + cme_mic { key: "USDHKC" value: 1 } + cme_mic { key: "USDIDR" value: 1 } + cme_mic { key: "USDINR" value: 1 } + cme_mic { key: "USDISC" value: 1 } + cme_mic { key: "USDJPY" value: 8 } + cme_mic { key: "USDJYC" value: 1 } + cme_mic { key: "USDJYN" value: 1 } + cme_mic { key: "USDKRW" value: 1 } + cme_mic { key: "USDMPC" value: 1 } + cme_mic { key: "USDMXN" value: 8 } + cme_mic { key: "USDNKC" value: 1 } + cme_mic { key: "USDPEN" value: 1 } + cme_mic { key: "USDPHP" value: 1 } + cme_mic { key: "USDPZC" value: 1 } + cme_mic { key: "USDRUB" value: 1 } + cme_mic { key: "USDSDC" value: 1 } + cme_mic { key: "USDSFC" value: 1 } + cme_mic { key: "USDSFN" value: 1 } + cme_mic { key: "USDSKC" value: 1 } + cme_mic { key: "USDTBC" value: 1 } + cme_mic { key: "USDTLC" value: 1 } + cme_mic { key: "USDTWD" value: 1 } + cme_mic { key: "USDZRC" value: 1 } + cme_mic { key: "UV" value: 2 } + cme_mic { key: "UX" value: 2 } + cme_mic { key: "V3L" value: 2 } + cme_mic { key: "V7" value: 2 } + cme_mic { key: "VDL" value: 2 } + cme_mic { key: "VLD" value: 2 } + cme_mic { key: "VR" value: 2 } + cme_mic { key: "VV" value: 2 } + cme_mic { key: "W0" value: 2 } + cme_mic { key: "WBR" value: 2 } + cme_mic { key: "WCC" value: 2 } + cme_mic { key: "WCE" value: 2 } + cme_mic { key: "WCW" value: 2 } + cme_mic { key: "WDB" value: 2 } + cme_mic { key: "WDC" value: 1 } + cme_mic { key: "WHB" value: 2 } + cme_mic { key: "WHD" value: 2 } + cme_mic { key: "WMB" value: 2 } + cme_mic { key: "WMD" value: 2 } + cme_mic { key: "WMK" value: 3 } + cme_mic { key: "WMR" value: 2 } + cme_mic { key: "WNC" value: 2 } + cme_mic { key: "WNT" value: 2 } + cme_mic { key: "WOL" value: 2 } + cme_mic { key: "WPL" value: 2 } + cme_mic { key: "WS" value: 2 } + cme_mic { key: "WTA" value: 2 } + cme_mic { key: "WTD" value: 2 } + cme_mic { key: "WTI" value: 2 } + cme_mic { key: "WTS" value: 2 } + cme_mic { key: "WTT" value: 2 } + cme_mic { key: "X0" value: 2 } + cme_mic { key: "X6" value: 2 } + cme_mic { key: "X7" value: 2 } + cme_mic { key: "X9" value: 2 } + cme_mic { key: "XAB" value: 1 } + cme_mic { key: "XAE" value: 1 } + cme_mic { key: "XAF" value: 1 } + cme_mic { key: "XAI" value: 1 } + cme_mic { key: "XAK" value: 1 } + cme_mic { key: "XAP" value: 1 } + cme_mic { key: "XAR" value: 1 } + cme_mic { key: "XAU" value: 1 } + cme_mic { key: "XAV" value: 1 } + cme_mic { key: "XAY" value: 1 } + cme_mic { key: "XAZ" value: 1 } + cme_mic { key: "XBT" value: 1 } + cme_mic { key: "XC" value: 3 } + cme_mic { key: "XER" value: 2 } + cme_mic { key: "XET" value: 1 } + cme_mic { key: "XFT" value: 1 } + cme_mic { key: "XH" value: 2 } + cme_mic { key: "XIT" value: 1 } + cme_mic { key: "XK" value: 3 } + cme_mic { key: "XKT" value: 1 } + cme_mic { key: "XPP" value: 2 } + cme_mic { key: "XPT" value: 1 } + cme_mic { key: "XR" value: 2 } + cme_mic { key: "XRI" value: 1 } + cme_mic { key: "XRT" value: 1 } + cme_mic { key: "XUT" value: 1 } + cme_mic { key: "XVT" value: 1 } + cme_mic { key: "XW" value: 3 } + cme_mic { key: "XX" value: 2 } + cme_mic { key: "XYT" value: 1 } + cme_mic { key: "XZI" value: 1 } + cme_mic { key: "XZT" value: 1 } + cme_mic { key: "YF" value: 2 } + cme_mic { key: "YHE" value: 2 } + cme_mic { key: "YHF" value: 2 } + cme_mic { key: "YM" value: 3 } + cme_mic { key: "YMT" value: 3 } + cme_mic { key: "YNO" value: 2 } + cme_mic { key: "YO" value: 2 } + cme_mic { key: "YRP" value: 2 } + cme_mic { key: "YRW" value: 2 } + cme_mic { key: "YS" value: 2 } + cme_mic { key: "YUE" value: 2 } + cme_mic { key: "YWE" value: 2 } + cme_mic { key: "YWF" value: 2 } + cme_mic { key: "YWK" value: 2 } + cme_mic { key: "Z00000" value: 1 } + cme_mic { key: "Z00001" value: 1 } + cme_mic { key: "Z00002" value: 1 } + cme_mic { key: "Z1B" value: 2 } + cme_mic { key: "Z3N" value: 3 } + cme_mic { key: "Z4" value: 2 } + cme_mic { key: "Z6" value: 2 } + cme_mic { key: "ZA9102" value: 1 } + cme_mic { key: "ZA9202" value: 1 } + cme_mic { key: "ZA9903" value: 1 } + cme_mic { key: "ZA9906" value: 1 } + cme_mic { key: "ZA9909" value: 1 } + cme_mic { key: "ZA9912" value: 1 } + cme_mic { key: "ZAL" value: 2 } + cme_mic { key: "ZAR" value: 1 } + cme_mic { key: "ZB" value: 3 } + cme_mic { key: "ZB0001" value: 1 } + cme_mic { key: "ZB0002" value: 1 } + cme_mic { key: "ZB0003" value: 1 } + cme_mic { key: "ZB0038" value: 1 } + cme_mic { key: "ZB0039" value: 1 } + cme_mic { key: "ZB0040" value: 1 } + cme_mic { key: "ZB9103" value: 1 } + cme_mic { key: "ZB9104" value: 1 } + cme_mic { key: "ZB9105" value: 1 } + cme_mic { key: "ZB9203" value: 1 } + cme_mic { key: "ZB9204" value: 1 } + cme_mic { key: "ZB9205" value: 1 } + cme_mic { key: "ZB9903" value: 1 } + cme_mic { key: "ZB9906" value: 1 } + cme_mic { key: "ZB9909" value: 1 } + cme_mic { key: "ZB9912" value: 1 } + cme_mic { key: "ZC" value: 3 } + cme_mic { key: "ZC0001" value: 1 } + cme_mic { key: "ZC0002" value: 1 } + cme_mic { key: "ZC0003" value: 1 } + cme_mic { key: "ZC0004" value: 1 } + cme_mic { key: "ZC0005" value: 1 } + cme_mic { key: "ZC0029" value: 1 } + cme_mic { key: "ZC0030" value: 1 } + cme_mic { key: "ZC9107" value: 1 } + cme_mic { key: "ZC9110" value: 1 } + cme_mic { key: "ZC9207" value: 1 } + cme_mic { key: "ZC9210" value: 1 } + cme_mic { key: "ZC9903" value: 1 } + cme_mic { key: "ZC9906" value: 1 } + cme_mic { key: "ZC9909" value: 1 } + cme_mic { key: "ZC9912" value: 1 } + cme_mic { key: "ZCT" value: 3 } + cme_mic { key: "ZD0030" value: 1 } + cme_mic { key: "ZD9112" value: 1 } + cme_mic { key: "ZD9115" value: 1 } + cme_mic { key: "ZD9120" value: 1 } + cme_mic { key: "ZD9130" value: 1 } + cme_mic { key: "ZD9212" value: 1 } + cme_mic { key: "ZD9215" value: 1 } + cme_mic { key: "ZD9220" value: 1 } + cme_mic { key: "ZD9230" value: 1 } + cme_mic { key: "ZD9803" value: 1 } + cme_mic { key: "ZD9806" value: 1 } + cme_mic { key: "ZD9809" value: 1 } + cme_mic { key: "ZD9812" value: 1 } + cme_mic { key: "ZD9903" value: 1 } + cme_mic { key: "ZD9906" value: 1 } + cme_mic { key: "ZD9909" value: 1 } + cme_mic { key: "ZD9912" value: 1 } + cme_mic { key: "ZF" value: 3 } + cme_mic { key: "ZGD" value: 6 } + cme_mic { key: "ZGL" value: 2 } + cme_mic { key: "ZJL" value: 2 } + cme_mic { key: "ZKU" value: 2 } + cme_mic { key: "ZL" value: 3 } + cme_mic { key: "ZLT" value: 3 } + cme_mic { key: "ZM" value: 3 } + cme_mic { key: "ZMT" value: 3 } + cme_mic { key: "ZN" value: 3 } + cme_mic { key: "ZNC" value: 4 } + cme_mic { key: "ZO" value: 3 } + cme_mic { key: "ZQ" value: 3 } + cme_mic { key: "ZR" value: 3 } + cme_mic { key: "ZS" value: 3 } + cme_mic { key: "ZT" value: 3 } + cme_mic { key: "ZW" value: 3 } + cme_mic { key: "ZWT" value: 3 } + } } diff --git a/src/lib/config/defaults/dynamic_orders.conf b/src/lib/config/defaults/dynamic_orders.conf index c5d41b59d..527c6a4cb 100644 --- a/src/lib/config/defaults/dynamic_orders.conf +++ b/src/lib/config/defaults/dynamic_orders.conf @@ -2,17 +2,17 @@ DynamicOrderList { value { -# dynamic_orders -# { -# type_name: 'DynamicOrder1' -# parameter_keys: 'param1' -# parameter_keys: 'param2' -# } -# dynamic_orders -# { -# type_name: 'DynamicOrder2' -# parameter_keys: 'param3' -# parameter_keys: 'param4' -# } + dynamic_orders + { + type_name: 'DynamicOrder1' + parameter_keys: 'param1' + parameter_keys: 'param2' + } + dynamic_orders + { + type_name: 'DynamicOrder2' + parameter_keys: 'param3' + parameter_keys: 'param4' + } } } \ No newline at end of file diff --git a/src/lib/config/defaults/expiration_display_offset.conf b/src/lib/config/defaults/expiration_display_offset.conf deleted file mode 100644 index 959d3f291..000000000 --- a/src/lib/config/defaults/expiration_display_offset.conf +++ /dev/null @@ -1,194 +0,0 @@ -ExpirationDisplayOffset -{ - value - { - entries - { - security_type: "OPTION" - security_name: "lo" - addition_count: 1 - addition_type: MonthMoveToStart - } - - entries - { - security_type: "FUTURE" - security_name: "CL" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "ob" - addition_count: 1 - addition_type: MonthMoveToStart - } - - entries - { - security_type: "FUTURE" - security_name: "RB" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "oh" - addition_count: 1 - addition_type: MonthMoveToStart - } - - entries - { - security_type: "FUTURE" - security_name: "HO" - addition_count: 1 - addition_type: MonthMoveToStart - } - - entries - { - security_type: "OPTION" - security_name: "wbs" - addition_count: 1 - addition_type: MonthMoveToStart - } - - entries - { - security_type: "FUTURE" - security_name: "WBS" - addition_count: 1 - addition_type: MonthMoveToStart - } - - entries - { - security_type: "OPTION" - security_name: "so" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "FUTURE" - security_name: "SI" - addition_count: 0 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "og" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "FUTURE" - security_name: "GC" - addition_count: 0 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "hxe" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "FUTURE" - security_name: "HG" - addition_count: 0 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "ozs" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "ozw" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "osd" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "FUTURE" - security_name: "ZS" - addition_count: 0 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "ozm" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "FUTURE" - security_name: "ZM" - addition_count: 0 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "ozl" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "FUTURE" - security_name: "ZL" - addition_count: 0 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "ozc" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "FUTURE" - security_name: "ZC" - addition_count: 0 - addition_type: MonthMoveToStart - } - entries - { - security_type: "OPTION" - security_name: "ozn" - addition_count: 1 - addition_type: MonthMoveToStart - } - entries - { - security_type: "FUTURE" - security_name: "ZN" - addition_count: 0 - addition_type: MonthMoveToStart - } - } -} \ No newline at end of file diff --git a/src/lib/config/protos/ExchangeSimCfg.proto b/src/lib/config/protos/ExchangeSimCfg.proto index 790ef5779..4702536b5 100644 --- a/src/lib/config/protos/ExchangeSimCfg.proto +++ b/src/lib/config/protos/ExchangeSimCfg.proto @@ -3,5 +3,6 @@ package bts.config; message ExchangeSim { optional bool merge_with_md = 1 [default=true]; + optional bool forward_implied = 2 [default = false]; } diff --git a/src/lib/config/protos/MarketData.proto b/src/lib/config/protos/MarketData.proto index df6c5f924..b08e0e7ca 100755 --- a/src/lib/config/protos/MarketData.proto +++ b/src/lib/config/protos/MarketData.proto @@ -78,10 +78,6 @@ message CMEOptions optional string thread_group = 10 [default=""]; // if present this will be used to retrieve affinity settings from Threads config key optional uint32 udp_socket_buffer_size = 11 [default = 33554432]; //32mb } - enum BookType { - BOOKTYPE_CONSOLIDATED = 0; - BOOKTYPE_DIRECT = 1; - } message CMEChannel { repeated string id = 2; optional string feed_engine = 3 [default = "default"]; @@ -104,7 +100,8 @@ message CMEOptions optional bool join_snapshot_on_gap = 32 [default = false]; optional bool load_security_db = 33 [default = true]; - optional BookType book_type = 34 [default = BOOKTYPE_CONSOLIDATED]; + reserved 34; + // optional BookType book_type = 34 [default = BOOKTYPE_CONSOLIDATED]; } optional string onixs_license = 1 [default = "{default}"]; diff --git a/src/lib/config/protos/cme_reference_data.proto b/src/lib/config/protos/cme_reference_data.proto new file mode 100644 index 000000000..4aba79684 --- /dev/null +++ b/src/lib/config/protos/cme_reference_data.proto @@ -0,0 +1,11 @@ +syntax='proto2'; +package bts.config; +import "lib/domain_model/Security.proto"; + +message CppConfig { + map cpp_entry = 1; +} + +message CmeSymbolMics { + map cme_mic = 1; +} \ No newline at end of file diff --git a/src/lib/config/protos/cpp_config.proto b/src/lib/config/protos/cpp_config.proto deleted file mode 100644 index 7070adbe9..000000000 --- a/src/lib/config/protos/cpp_config.proto +++ /dev/null @@ -1,6 +0,0 @@ -syntax='proto2'; -package bts.config; - -message CppConfig { - map cpp_entry = 1; -} \ No newline at end of file diff --git a/src/lib/config/protos/expiration_display_offset.proto b/src/lib/config/protos/expiration_display_offset.proto deleted file mode 100644 index 3c8ded3c7..000000000 --- a/src/lib/config/protos/expiration_display_offset.proto +++ /dev/null @@ -1,24 +0,0 @@ -syntax='proto2'; -package bts.config; - -message ExpirationOffsetType -{ - enum type - { - Month=0; - MonthMoveToStart=1; - } -} - -message ExpirationDisplayOffsetEntry -{ - required string security_type = 1; - required string security_name = 2; - required int32 addition_count = 3; - required ExpirationOffsetType.type addition_type = 4; -} - -message ExpirationDisplayOffset -{ - repeated ExpirationDisplayOffsetEntry entries = 1; -} diff --git a/src/lib/config/protos/toggles.proto b/src/lib/config/protos/toggles.proto index f7277ca33..d940fa416 100644 --- a/src/lib/config/protos/toggles.proto +++ b/src/lib/config/protos/toggles.proto @@ -26,4 +26,8 @@ message OrdersSoftCleanup{ message ToastNotifications { optional bool enabled = 1 [default = false]; +} + +message VolumeAtPrice{ + optional bool enabled = 1 [default = true]; } \ No newline at end of file diff --git a/src/lib/domain_model/Security.proto b/src/lib/domain_model/Security.proto index 9e6424230..3d2b5f738 100644 --- a/src/lib/domain_model/Security.proto +++ b/src/lib/domain_model/Security.proto @@ -40,6 +40,22 @@ enum Currency { CURRENCY_RMB = 14; } +enum CMEMics { + UNSET = 0; + CME = 1; + NYMEX = 2; + CBT = 3; + COMEX = 4; + MGE = 5; + DME = 6; + NYMSW = 7; + FXS = 8; +} + +enum ICEMics{ + ICE_FUTURES_CANADA=0; +} + enum Side { SIDE_UNDEFINED = 0; SIDE_BUY = 49; diff --git a/src/lib/logging/logging.cpp b/src/lib/logging/logging.cpp index 489bf3d7d..c8e05eb55 100644 --- a/src/lib/logging/logging.cpp +++ b/src/lib/logging/logging.cpp @@ -212,7 +212,9 @@ const char* intern(const char* str) { extern "C" BTS_EXPORT void Logger_log (int level, int subsystem, const char *message, const char *file, int line) { - bts::log::_thread_logger.logger->enqueue(log::log_ts(),log::Subsystem{(uint8_t)subsystem}, log::Level{(uint8_t)level}, intern(file), line, "{}", std::string(message)); + if (bts::log::_thread_logger.logger != nullptr) { + bts::log::_thread_logger.logger->enqueue(log::log_ts(), log::Subsystem{ (uint8_t)subsystem }, log::Level{ (uint8_t)level }, intern(file), line, "{}", std::string(message)); + } } } #endif diff --git a/src/lib/md/Diag/MDPriceLantencyTester.cpp b/src/lib/md/Diag/MDPriceLantencyTester.cpp index 0067cfebe..c15319110 100644 --- a/src/lib/md/Diag/MDPriceLantencyTester.cpp +++ b/src/lib/md/Diag/MDPriceLantencyTester.cpp @@ -39,7 +39,7 @@ void run_md_price_latency_test(UserSymbol symbol) { while (true) { for(auto& entry : handles) { PriceSize bid, ask; - if (entry.handle.get_bbo(entry.time, bid, ask) == HandleReadResult::SUCCESS) { + if (entry.handle.get_direct_book(entry.time, 1, &bid, &ask) == HandleReadResult::SUCCESS) { auto bid_price = bid.price(); auto ask_price = ask.price(); if (bid_price != entry.bid || ask_price != entry.ask) { diff --git a/src/lib/md/Diag/ShmDumper.cpp b/src/lib/md/Diag/ShmDumper.cpp index 1caa319d0..914f58897 100644 --- a/src/lib/md/Diag/ShmDumper.cpp +++ b/src/lib/md/Diag/ShmDumper.cpp @@ -25,6 +25,11 @@ double dump(TimeStamp time) { return (TimeStamp::now() - time).seconds(); } +string dump(IntPrice price) { + stringstream res; + res << std::setprecision(11) << price.price(); + return res.str(); +} string dump(PriceSize ps) { stringstream res; res << "{"; @@ -144,9 +149,8 @@ string dump_entry(const ColdEntry& entry) { if (has_value(entry.last_trade)) res << "'last_trade': " << dump(entry.last_trade) << ", "; if (entry.total_volume > 0) res << "'total_volume': " << entry.total_volume << ", "; if (entry.last_trade_time > 0) res << "'last_trade_time_ll': " << entry.last_trade_time << ", "; - if (!os::is_nan(entry.misc.settlement_price)) res << "'settlement_price': " << entry.misc.settlement_price << ", "; if (entry.misc.settlement_date.year > 0) res << "'settlement_date': " << dump(entry.misc.settlement_date) << ", "; - if (!os::is_nan(entry.misc.previous_settlement_price)) res << "'previous_settlement_price': " << entry.misc.previous_settlement_price << ", "; + if (!(entry.misc.previous_settlement_price.is_nan())) res << "'previous_settlement_price': " << entry.misc.previous_settlement_price.price() << ", "; if (entry.misc.previous_settlement_date.year > 0) res << "'previous_settlement_date': " << dump(entry.misc.previous_settlement_date) << ", "; res << "'deleted': " << std::boolalpha << entry.deleted << " "; res << "}"; @@ -159,18 +163,31 @@ string dump_entry(const HotDepthEntry& entry) { res << "'timestamp': " << dump(entry.time()) << ", "; res << "'key': " << entry.key << ", "; - res << "'bids': ["; - for (uint32_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { - res << dump(entry.at(x)->bid()) << ", "; + res << "\n'bid': ["; + for (const auto& level : entry.direct) { + res << dump(level.bid()) << ", "; } res << "], "; - res << "'asks': ["; - for (uint32_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { - res << dump(entry.at(x)->ask()) << ", "; + res << "\n'ask': ["; + for (const auto& level : entry.direct) { + res << dump(level.ask()) << ", "; + } + res << "], "; + + res << "'\nimplied_bid': ["; + for (const auto& level : entry.implied) { + res << dump(level.bid()) << ", "; } res << "], "; + res << "'\nimplied_ask': ["; + for (const auto& level : entry.implied) { + res << dump(level.ask()) << ", "; + } + res << "], "; + + res << "\n'settlement': '" << dump(entry.settlement) << "', "; res << "'status': '" << CoconutSecurityStatus_Name((CoconutSecurityStatus)entry.status) << "', "; res << "'sequence': '" << entry.sequence << "', "; res << " }"; diff --git a/src/lib/md/IntPrice.h b/src/lib/md/IntPrice.h new file mode 100644 index 000000000..c3c6c3573 --- /dev/null +++ b/src/lib/md/IntPrice.h @@ -0,0 +1,107 @@ +#pragma once +#include "lib/utils/bts_assert.h" +#include "lib/utils/bts_array_size.h" + +namespace bts { +namespace md { + +static const double powers_10[] = { + 1E-20, 1E-19, 1E-18, 1E-17, 1E-16, 1E-15, 1E-14, 1E-13, 1E-12, 1E-11, 1E-10, 1E-9, 1E-8, 1E-7, 1E-6, 1E-5, 1E-4, 1E-3, 1E-2, 1E-1, + 1E0, + 1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8, 1E9, 1E10, 1E11, 1E12, 1E13, 1E14, 1E15, 1E16, 1E17, 1E18, 1E19, 1E20 +}; +static const int max_abs_power_10 = BTS_ARRAYSIZE(powers_10) / 2; +inline double pow10(int power) { + const int offset = power + max_abs_power_10; + const int powers_10_sz = BTS_ARRAYSIZE(powers_10); + bts_assert_d(offset >= 0 && offset < powers_10_sz); + if (offset >= 0 && offset < powers_10_sz) { + return powers_10[offset]; + } + return std::numeric_limits().quiet_NaN(); +} + +class IntPrice { +#ifdef _WIN32 + int64_t _mantissa : 56; + int64_t _exponent : 8; +#else + int64_t _mantissa : 56; + int8_t _exponent : 8; +#endif + public: + IntPrice() : _mantissa(0), _exponent(127) {/*nan*/} + IntPrice(int64_t mantissa, int8_t exponent) : _mantissa(mantissa), _exponent(exponent) {} + IntPrice(const IntPrice& other) : _mantissa(other._mantissa), _exponent(other._exponent) {} + + bool operator==(const IntPrice& other) const { + // this requires both mantissa and exponent to be equal + // logically 100*pow10(1) == 10*pow10(2) + // HOWEVER, once converted to double there are subtelties that can make them not equal so + // this function is conservative + if(_exponent == other._exponent) + return _mantissa == other._mantissa; + else + return price()==other.price(); + } + bool operator!=(const IntPrice& other) const { + return !(*this == other); + } + bool operator<(const IntPrice& other) const { + if (_exponent == other._exponent) { + return _mantissa < other._mantissa; + } + else { + return price() < other.price(); + } + } + IntPrice operator - (const IntPrice& o) const { + if(_exponent==o._exponent) { + return IntPrice(_mantissa-o.mantissa(), _exponent); + } + else { + // not sure there's ever a cause for a positive exponent unless we get into scientific computing + if (is_nan() || o.is_nan()) return IntPrice(); + bts_assert_d(o.exponent() <= 0 && _exponent <= 0); + if (o.exponent() < _exponent) { + int8_t delta = _exponent - o.exponent(); + auto m = _mantissa * pow10(delta); + return IntPrice(m - o.mantissa(), o.exponent()); + } + else { + int8_t delta = o.exponent() - _exponent; + auto m = o.mantissa() * pow10(delta); + return IntPrice(_mantissa - m, _exponent); + } + } + } + int64_t mantissa() const { return _mantissa; } + int8_t exponent() const { return _exponent; } + double price() const { + if (is_nan()) return NO_PRICE; + return mantissa() * pow10(exponent()); + } + bool is_nan() const { return _exponent == 127; } +}; +static_assert(sizeof(IntPrice) == 8, "IntPrice should be this big |--------|"); +extern IntPrice INT_NAN; + +template +IntPrice IntPrice_from_double(double price) { + if (os::is_nan(price)) return INT_NAN; + double temp; + if (fabs(modf(price, &temp)) != 0.0) { + const double rounding = 0.5 * pow10(-PRECISION); + price += (price > 0) ? rounding : -rounding; + price *= pow10(PRECISION); + return {(int64_t)price, -PRECISION}; + } + return {(int64_t)price, 0}; +} + +inline std::ostream& operator<<(std::ostream& os, const IntPrice& p) { + os << p.price(); + return os; +} + +}} \ No newline at end of file diff --git a/src/lib/md/MDConsumer.cpp b/src/lib/md/MDConsumer.cpp index b534058ec..dba9eda76 100644 --- a/src/lib/md/MDConsumer.cpp +++ b/src/lib/md/MDConsumer.cpp @@ -419,6 +419,7 @@ private: auto iter = _data.find(m.security_key()); if (iter != _data.end()) { iter->second->strategy.CopyFrom(m.strategy()); + iter->second->updated = TimeStamp::now(); } } @@ -476,6 +477,7 @@ private: s->security.CopyFrom(*strategy.security); s->strategy.CopyFrom(strategy.strategy); s->legs = legs; + s->updated = TimeStamp::now(); result.insert( make_pair(strategy.security->id(),s)); } diff --git a/src/lib/md/MDConsumerHandle.cpp b/src/lib/md/MDConsumerHandle.cpp index 0b82746bf..a078dbef1 100644 --- a/src/lib/md/MDConsumerHandle.cpp +++ b/src/lib/md/MDConsumerHandle.cpp @@ -1,10 +1,13 @@ #ifndef NO_CPP0X #include "MDConsumerHandle.h" -#include "lib/utils/sane_algorithm.h" #include "lib/domain_model/Security.h" +#include #define never bts_assert(false) +template +using small_vector = boost::container::small_vector; + namespace bts { namespace md { @@ -122,8 +125,7 @@ struct DepthTraversal { } }; - -void copy_depth(size_t count, api::Side side, const api::ContractSpecification& spec, std::vector& leg_travs, Level* out_price) { +void copy_depth(size_t count, api::Side side, const api::ContractSpecification& spec, small_vector& leg_travs, Level* out_price) { size_t out_index = 0; bool first = true; while(out_index < count) { @@ -165,16 +167,40 @@ void copy_depth(size_t count, api::Side side, const api::ContractSpecification& } TimeStamp copy_depth(const SyntheticStrategy& synth, size_t count, PriceSize* bid, PriceSize* ask) { - std::vector bid_leg_travs; - std::vector ask_leg_travs; + small_vector bid_leg_travs; + small_vector ask_leg_travs; bid_leg_travs.reserve(synth.legs.size()); ask_leg_travs.reserve(synth.legs.size()); TimeStamp rv = std::numeric_limits::min(); for (size_t x = 0; x < synth.legs.size(); ++x) { std::array bids, asks; TimeStamp t; - HandleReadResult r = synth.legs[x].get_depth(t, count, &bids[0], &asks[0]); + auto key = synth.legs[x].key(); + auto iter = synth.strategy.leg_params_map().find(key); + const spark::LegSpreaderParams& leg_params = + iter == synth.strategy.leg_params_map().end() ? + spark::LegSpreaderParams::default_instance() : + iter->second; + + auto mdtype = leg_params.market_data_type(); + HandleReadResult r; + if (mdtype == spark::LegSpreaderParams::DIRECT) { + r = synth.legs[x].get_direct_book(t, count, &bids[0], &asks[0]); + } + else { + r = synth.legs[x].get_book(t, count, &bids[0], &asks[0]); + } if (r == HandleReadResult::BUSY || t == TimeStamp::invalid()) return TimeStamp::invalid(); + + // NET Change + if (leg_params.leg_md_calc() == spark::LegSpreaderParams::NET_CHANGE) { + auto settle = synth.legs[x].settlement_price(); + for (size_t d = 0; d < COCONUT_MAX_DEPTH; ++d) { + bids[d] = bids[d] - settle; + asks[d] = asks[d] - settle; + } + } + rv = std::max(rv, t); const auto& leg = synth.security.synthetic().legs(x); const auto& eff_bid = leg.side() == api::SIDE_BUY ? bids : asks; @@ -186,12 +212,12 @@ TimeStamp copy_depth(const SyntheticStrategy& synth, size_t count, PriceSize* bi const auto& spec = synth.security.contract_spec(); copy_depth(count, api::SIDE_BUY, spec, bid_leg_travs, scratch.data()); for (size_t x = 0; x < count; ++x) { - bid[x] = PriceSize::from_double<7>(scratch[x].price, scratch[x].size); + bid[x] = PriceSize_from_double<7>(scratch[x].price, scratch[x].size); } memset(scratch.data(), 0, sizeof(scratch)); copy_depth(count, api::SIDE_SELL, spec, ask_leg_travs, scratch.data()); for (size_t x = 0; x < count; ++x) { - ask[x] = PriceSize::from_double<7>(scratch[x].price, scratch[x].size); + ask[x] = PriceSize_from_double<7>(scratch[x].price, scratch[x].size); } return rv; } @@ -200,20 +226,6 @@ TimeStamp copy_depth(const SyntheticStrategy& synth, size_t count, PriceSize* bi // END SYNTHETIC DEPTH ///////////////////////////// -TimeStamp MDConsumerHandle::last_market_update() const { - switch(_type) { - case HANDLETYPE_VOID: - return TimeStamp::invalid(); - case HANDLETYPE_DEPTH: - return book.hot->time(); - case HANDLETYPE_BBO: - return bbo.hot->time(); - case HANDLETYPE_SYNTHETIC: - return sane::max(synthetic.def->legs, [](const MDConsumerHandle& h) { return h.last_market_update(); }); - } - never; -} - bool MDConsumerHandle::get_last_trade(TimeStamp& time, PriceSize& trade, datetime::LLDateTime& trade_timestamp, size_t* total_volume) const { switch(_type) { case HANDLETYPE_VOID: return false; diff --git a/src/lib/md/MDConsumerHandle.h b/src/lib/md/MDConsumerHandle.h index 1f8904f22..7f1097d73 100644 --- a/src/lib/md/MDConsumerHandle.h +++ b/src/lib/md/MDConsumerHandle.h @@ -5,6 +5,7 @@ #include "lib/md/Shm/MDStructure.h" #include "lib/sparkproto/SparkProtos.pb.h" #include "UserSymbol.h" +#include "lib/utils/sane_algorithm.h" #include #include @@ -16,6 +17,7 @@ struct SyntheticStrategy { spark::ExecutionStrategy strategy; api::Security security; std::vector legs; + TimeStamp updated; }; TimeStamp copy_bbo(const HotEntry& src, PriceSize& out_bid, PriceSize& out_ask); @@ -28,6 +30,117 @@ enum class HandleReadResult { SUCCESS }; +inline void read_direct( + const HotDepthEntry& hot, + size_t count, + PriceSize* bid, PriceSize* ask) { + + bts_assert_d(count <= COCONUT_MAX_DEPTH); + count = std::min(count, (size_t)COCONUT_MAX_DEPTH); + for (size_t x = 0; x < count; ++x) { + hot.read_direct(x, &bid[x], &ask[x]); + } +} +inline void read_implied( + const HotDepthEntry& hot, + size_t count, + PriceSize* bid, PriceSize* ask) { + + bts_assert_d(count <= COCONUT_MAX_IMPLIED_DEPTH); + count = std::min(count, (size_t)COCONUT_MAX_IMPLIED_DEPTH); + for (size_t x = 0; x < count; ++x) { + hot.read_implied(x, &bid[x], &ask[x]); + } +} +inline void read_combined( + const HotDepthEntry& hot, + size_t count, + PriceSize* bid, PriceSize* ask) { + bts_assert_d(count <= COCONUT_MAX_DEPTH); + size_t xda=0, xdb=0, xia=0, xib=0; + if (hot.implied[xib].bid().is_nan()) xib = COCONUT_MAX_IMPLIED_DEPTH; + if (hot.implied[xia].ask().is_nan()) xia = COCONUT_MAX_IMPLIED_DEPTH; + + for (size_t x = 0; x < count; ++x) { + if (xib < COCONUT_MAX_IMPLIED_DEPTH) { + auto direct = hot.direct[xdb].bid(); + auto implied = hot.implied[xib].bid(); + + // Compare bids + if (direct.int_price() == implied.int_price()) { + bid[x] = PriceSize( + direct.int_price(), + direct.size(), + ImpliedSize(implied.size())); + xdb += 1; + xib += 1; + } + // Bids grow down, take the higher price + else if (implied.int_price() < direct.int_price()) { + bid[x] = direct; + xdb += 1; + } + else { + bid[x] = implied; + xib += 1; + } + } + else { + bid[x] = hot.direct[xdb].bid(); + xdb += 1; + } + + if (xia < COCONUT_MAX_IMPLIED_DEPTH) { + auto direct = hot.direct[xda].ask(); + auto implied = hot.implied[xia].ask(); + + // Compare asks + if (direct.int_price() == implied.int_price()) { + ask[x] = PriceSize( + direct.int_price(), + direct.size(), + ImpliedSize(implied.size())); + xda += 1; + xia += 1; + } + // asks grow up, take the lower price + else if (direct.int_price() < implied.int_price()) { + ask[x] = direct; + xda += 1; + } + else { + ask[x] = implied; + xia += 1; + } + } + else { + ask[x] = hot.direct[xda].ask(); + xda += 1; + } + } +} + +template +TimeStamp optimistic_copy( + Read read, + const HotDepthEntry& hot, + size_t count, + PriceSize* bid, PriceSize* ask) +{ + TimeStamp t; + int attempts = 0; + const int max_attempts = 1619200; + do { + t = hot.time(); + read(hot, count, bid, ask); + ++attempts; + } while((t != hot.time() || t == TimeStamp::invalid()) && attempts < max_attempts); + if (attempts >= max_attempts) { + return TimeStamp::invalid(); + } + return t; +} + // Can be an invalid handle if you request a security which has not been discovered yet. class BTS_EXPORT MDConsumerHandle @@ -75,69 +188,44 @@ public: bts_assert(false); } - TimeStamp last_market_update() const; - - HandleReadResult get_bbo(TimeStamp& time, PriceSize& bid, PriceSize& ask) const - { - if (!is_initialized()) return HandleReadResult::NO_CHANGE; - + TimeStamp last_market_update() const { switch(_type) { - case HANDLETYPE_DEPTH: - if (book.hot->time() > time || book.hot->time() == TimeStamp::invalid()) { - TimeStamp temp = copy_depth(*book.hot, 1, &bid, &ask); - if (temp == TimeStamp::invalid()) { return HandleReadResult::BUSY; } - time = temp; - return HandleReadResult::SUCCESS; - } - return HandleReadResult::NO_CHANGE; - case HANDLETYPE_BBO: - if (bbo.hot->time() > time || bbo.hot->time() == TimeStamp::invalid()) { - TimeStamp temp = copy_bbo(*bbo.hot, bid, ask); - if (temp == TimeStamp::invalid()) { return HandleReadResult::BUSY; } - time = temp; - return HandleReadResult::SUCCESS; - } - return HandleReadResult::NO_CHANGE; - case HANDLETYPE_SYNTHETIC: - return get_depth(time, 1, &bid, &ask); case HANDLETYPE_VOID: - return HandleReadResult::NO_CHANGE; - } - bts_assert_d(false); - return HandleReadResult::NO_CHANGE; - } - - HandleReadResult get_depth(TimeStamp& time, uint32_t copydepth, PriceSize* bids, PriceSize* asks) const { - bts_assert_d(copydepth <= COCONUT_MAX_DEPTH); - if (!is_initialized() || copydepth == 0) return HandleReadResult::NO_CHANGE; - - switch(_type) { - case HANDLETYPE_VOID: return HandleReadResult::NO_CHANGE; + return TimeStamp::invalid(); case HANDLETYPE_DEPTH: - if (book.hot->time() > time || book.hot->time() == TimeStamp::invalid()) { - TimeStamp temp = copy_depth(*book.hot, std::min(copydepth, COCONUT_MAX_DEPTH), bids, asks); - if (temp == TimeStamp::invalid()) { return HandleReadResult::BUSY; } - time = temp; - return HandleReadResult::SUCCESS; - } - return HandleReadResult::NO_CHANGE; + return book.hot->time(); case HANDLETYPE_BBO: - return get_bbo(time, *bids, *asks); - case HANDLETYPE_SYNTHETIC: - auto t = last_market_update(); - if (t > time || t == TimeStamp::invalid()) { - TimeStamp temp = copy_depth(*synthetic.def, std::min(copydepth, COCONUT_MAX_DEPTH), bids, asks); - if (temp == TimeStamp::invalid()) { return HandleReadResult::BUSY; } - time = temp; - return HandleReadResult::SUCCESS; - } - return HandleReadResult::NO_CHANGE; + return bbo.hot->time(); + case HANDLETYPE_SYNTHETIC: + return std::max( + synthetic.def->updated, + sane::max(synthetic.def->legs, [](const MDConsumerHandle& h) { return h.last_market_update(); })); } bts_assert(false); } + + HandleReadResult get_direct_book(TimeStamp& time, uint32_t copydepth, PriceSize* bids, PriceSize* asks) const { + return get_book(read_direct, time, copydepth, bids, asks); + } + HandleReadResult get_implied_book(TimeStamp& time, uint32_t copydepth, PriceSize* bids, PriceSize* asks) const { + return get_book(read_implied, time, copydepth, bids, asks); + } + HandleReadResult get_book(TimeStamp& time, uint32_t copydepth, PriceSize* bids, PriceSize* asks) const { + return get_book(read_combined, time, copydepth, bids, asks); + } bool get_last_trade(TimeStamp& time, PriceSize& out_trade, datetime::LLDateTime& out_trade_timestamp, size_t* total_volume=nullptr) const; + IntPrice settlement_price() const { + switch(_type) { + case HANDLETYPE_DEPTH: return book.hot->settlement; + case HANDLETYPE_BBO: return bbo.hot->settlement; + default: + return INT_NAN; + } + } + datetime::Date settlement_date() const { return this->misc().settlement_date; } + TimeStamp misc_timestamp() const; MiscData misc() const; @@ -162,6 +250,37 @@ public: private: friend class MDConsumerInst; + + template + HandleReadResult get_book(Read read, TimeStamp& time, uint32_t copydepth, PriceSize* bids, PriceSize* asks) const { + bts_assert_d(copydepth <= COCONUT_MAX_DEPTH); + if (!is_initialized() || copydepth == 0) return HandleReadResult::NO_CHANGE; + switch(_type) { + case HANDLETYPE_VOID: return HandleReadResult::NO_CHANGE; + case HANDLETYPE_DEPTH: + if (book.hot->time() > time || book.hot->time() == TimeStamp::invalid()) { + TimeStamp temp = optimistic_copy(read, *book.hot, copydepth, bids, asks); + if (temp == TimeStamp::invalid()) { return HandleReadResult::BUSY; } + time = temp; + return HandleReadResult::SUCCESS; + } + return HandleReadResult::NO_CHANGE; + case HANDLETYPE_BBO: + bts_assert_msg(false,"Not impelmented"); + return HandleReadResult::NO_CHANGE; + case HANDLETYPE_SYNTHETIC: + auto t = last_market_update(); + if (t > time || t == TimeStamp::invalid()) { + TimeStamp temp = copy_depth(*synthetic.def, std::min(copydepth, COCONUT_MAX_DEPTH), bids, asks); + if (temp == TimeStamp::invalid()) { return HandleReadResult::BUSY; } + time = temp; + return HandleReadResult::SUCCESS; + } + return HandleReadResult::NO_CHANGE; + } + bts_assert(false); + } + void set_hot(const void* hot) { switch(_type) { case HANDLETYPE_DEPTH: diff --git a/src/lib/md/MDConsumerInterop.cpp b/src/lib/md/MDConsumerInterop.cpp index ad46d10a0..7f4848bea 100644 --- a/src/lib/md/MDConsumerInterop.cpp +++ b/src/lib/md/MDConsumerInterop.cpp @@ -102,16 +102,6 @@ CAPI void md_consumer_set_on_security(MDConsumer* consumer, security_cb cb) { } #endif -typedef void(*just_security_cb)(security_key, const char*, size_t); -CAPI void md_consumer_get_single_security(const ColdEntry* entry, just_security_cb cb) { - bts_assert(entry); - bts::api::Security security; - convert_security(entry->security, &security); - std::string buffer; - security.SerializePartialToString(&buffer); - cb(security.id(), buffer.c_str(), buffer.size()); -} - bool is_heart_beat_ok(const ColdHeader &header); CAPI bool md_consumer_is_heart_beating(const ColdHeader* header) { if (header == nullptr) return false; diff --git a/src/lib/md/MDProducerHandle.cpp b/src/lib/md/MDProducerHandle.cpp index 3af9741da..de5819197 100644 --- a/src/lib/md/MDProducerHandle.cpp +++ b/src/lib/md/MDProducerHandle.cpp @@ -148,16 +148,21 @@ void MDProducerHandle::update_exchange_status(TimeStamp, TradingStatus status) { cheader().exchange_status_count++; } -void MDProducerHandle::update_settlement(TimeStamp time, Price price, Date date) { +void MDProducerHandle::update_settlement(TimeStamp time, IntPrice price, Date date) { if (date > cdata().misc.settlement_date) { - cdata().misc.previous_settlement_price = cdata().misc.settlement_price; + cdata().misc.previous_settlement_price = has_depth() ? hdepth().settlement : hdata().settlement; cdata().misc.previous_settlement_date = cdata().misc.settlement_date; } - cdata().misc.settlement_price = price; + if (has_depth()) { + hdepth().settlement = price; + } + else { + hdata().settlement = price; + } cdata().misc.settlement_date = date; cdata().misc.misc_timestamp = time; } -void MDProducerHandle::update_previous_settlement(TimeStamp time, Price price, Date date) { +void MDProducerHandle::update_previous_settlement(TimeStamp time, IntPrice price, Date date) { cdata().misc.previous_settlement_price = price; cdata().misc.previous_settlement_date = date; cdata().misc.misc_timestamp = time; @@ -209,10 +214,15 @@ void MDProducerHandle::mark_deleted() { TimeStamp ts = TimeStamp::now(); cdata().deleted = true; std::array blank; + std::array iblank; for (size_t x = 0; x < blank.size(); ++x) { - blank[x] = PriceSize::NO_MARKET; + blank[x] = NO_MARKET; + if (x < COCONUT_MAX_IMPLIED_DEPTH) { + iblank[x] = NO_MARKET; + } } - update_book(ts, blank, blank, 0); + update_direct(ts, blank, blank, 0); + update_implied(ts, iblank, iblank, 0); } }}//namespace diff --git a/src/lib/md/MDProducerHandle.h b/src/lib/md/MDProducerHandle.h index 4203c1fe4..37600a69e 100644 --- a/src/lib/md/MDProducerHandle.h +++ b/src/lib/md/MDProducerHandle.h @@ -1,5 +1,5 @@ #pragma once - + #include "lib/utils/bts_no_copy.h" #include "lib/utils/bts_export.h" #include "Shm/MDStructure.h" @@ -33,9 +33,9 @@ public: return key; } void set_legs(const std::vector& legs); - - void update_bbo(TimeStamp time, const PriceSize bid_price, const PriceSize ask_price, uint16_t seq=0); // hot - void update_book(TimeStamp time, std::array& bids, std::array& asks, uint32_t seq=0) { + + void update_bbo(TimeStamp time, const PriceSize bid_price, const PriceSize ask_price, uint16_t seq=0); + void update_direct(TimeStamp time, const std::array& bids, const std::array& asks, uint32_t seq=0) { if (!has_depth()) { update_bbo(time, bids[0], asks[0], seq); } @@ -51,11 +51,12 @@ public: hheader().direct_count += 1; } } - void update_implied(TimeStamp time, std::array& bids, std::array& asks, uint32_t seq=0) { + + void update_implied(TimeStamp time, const std::array& bids, const std::array& asks, uint32_t seq=0) { if (has_depth()) { auto& book = hdepth(); book.set_time(TimeStamp::invalid()); - for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { book.write_implied(x, bids[x], asks[x]); } book.status = co2::COCONUTSECURITYSTATUS_OK; @@ -64,7 +65,31 @@ public: hheader().implied_count += 1; } } - + + void update_books(TimeStamp time, + const std::array& bid, + const std::array& ask, + const std::array& implied_bid, + const std::array& implied_ask, + uint32_t seq=0) { + + if (has_depth()) { + auto& book = hdepth(); + book.set_time(TimeStamp::invalid()); + for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { + if (x < COCONUT_MAX_IMPLIED_DEPTH) { + book.write_implied(x, implied_bid[x], implied_ask[x]); + } + book.write_direct(x, bid[x], ask[x]); + } + book.status = co2::COCONUTSECURITYSTATUS_OK; + book.sequence = seq; + book.set_time(time); + hheader().direct_count += 1; + hheader().implied_count += 1; + } + } + void set_status(co2::CoconutSecurityStatus status); // hot // Updates trade log and last trade @@ -72,12 +97,12 @@ public: // Update last_trade without updating trade log void set_last_trade(TimeStamp time, const PriceSize ps, datetime::LLDateTime date, uint32_t total_volume); void add_rfq(TimeStamp time, const co2::QuoteRequestMessage& rfq); // cold - + void update_exchange_status(TimeStamp time, TradingStatus status); // cold // Misc fields - void update_settlement(TimeStamp time, Price price, datetime::Date date); // cold - void update_previous_settlement(TimeStamp time, Price price, datetime::Date date); // cold + void update_settlement(TimeStamp time, IntPrice price, datetime::Date date); // cold + void update_previous_settlement(TimeStamp time, IntPrice price, datetime::Date date); // cold void update_opening(TimeStamp time, Price price, datetime::Date date); // cold void update_closing(TimeStamp time, Price price, datetime::Date date); // cold void update_session_high(TimeStamp time, Price price); // cold @@ -96,7 +121,7 @@ private: friend class tbb::concurrent_queue < MDProducerHandle > ; friend class SecurityIDRecorder; friend struct bts::co2::Work; - + HotEntry& hdata() const { bts_assert_d(_hdata && !has_depth()); return *static_cast(_hdata); @@ -111,7 +136,7 @@ private: bts_assert_d(_hheader); return *_hheader; } - + ColdEntry& cdata() const { bts_assert_d(_cdata); return *_cdata; diff --git a/src/lib/md/MemorySegmentUtils.cpp b/src/lib/md/MemorySegmentUtils.cpp index 1f216ce33..60b92b71e 100644 --- a/src/lib/md/MemorySegmentUtils.cpp +++ b/src/lib/md/MemorySegmentUtils.cpp @@ -8,8 +8,9 @@ using namespace std; namespace bts { namespace md { -bool enable_depth(const string&, SecurityType type) { - return type == api::SECURITYTYPE_FUTURE || type == api::SECURITYTYPE_SPREAD || type == api::SECURITYTYPE_SYNTHETIC; +bool enable_depth(const string&, SecurityType) { + return true; + // return type == api::SECURITYTYPE_FUTURE || type == api::SECURITYTYPE_SPREAD || type == api::SECURITYTYPE_SYNTHETIC; } string segment_name(const string& symbol, SecurityType type, Venue exchange, Platform platform) { diff --git a/src/lib/md/PriceSize.cpp b/src/lib/md/PriceSize.cpp index 490e132b6..46ea81030 100644 --- a/src/lib/md/PriceSize.cpp +++ b/src/lib/md/PriceSize.cpp @@ -4,7 +4,8 @@ namespace bts { namespace md { -BTS_EXPORT PriceSize PriceSize::NO_MARKET = PriceSize(); +IntPrice INT_NAN = IntPrice(); +PriceSize NO_MARKET = PriceSize(); int to_exponent(double value) { if (value == 0 && os::is_nan(value)) return 0; diff --git a/src/lib/md/PriceSize.h b/src/lib/md/PriceSize.h index 2b33551e3..c8b647d39 100644 --- a/src/lib/md/PriceSize.h +++ b/src/lib/md/PriceSize.h @@ -1,6 +1,9 @@ #pragma once #include "common.h" +#include "IntPrice.h" +#include "SizeTypes.h" + #include "lib/os/os.h" #include "lib/utils/warn_ctrl.h" #include "lib/utils/bts_array_size.h" @@ -8,6 +11,7 @@ #include "lib/utils/bts_no_copy.h" #include "lib/utils/Optimistic.h" + #include #include #include @@ -18,107 +22,62 @@ namespace md { typedef double Price; typedef uint32_t Size; -// #define LOAD(x) x.load(std::memory_order_acquire) -// #define STORE(x,value) x.store(value, std::memory_order_release) - -static const double powers_10[] = { - 1E-20, 1E-19, 1E-18, 1E-17, 1E-16, 1E-15, 1E-14, 1E-13, 1E-12, 1E-11, 1E-10, 1E-9, 1E-8, 1E-7, 1E-6, 1E-5, 1E-4, 1E-3, 1E-2, 1E-1, - 1E0, - 1E1, 1E2, 1E3, 1E4, 1E5, 1E6, 1E7, 1E8, 1E9, 1E10, 1E11, 1E12, 1E13, 1E14, 1E15, 1E16, 1E17, 1E18, 1E19, 1E20 -}; -static const int max_abs_power_10 = BTS_ARRAYSIZE(powers_10) / 2; -inline double pow10(int power) { - const int offset = power + max_abs_power_10; - const int powers_10_sz = BTS_ARRAYSIZE(powers_10); - bts_assert_d(offset >= 0 && offset < powers_10_sz); - if (offset >= 0 && offset < powers_10_sz) { - return powers_10[offset]; - } - return std::numeric_limits().quiet_NaN(); -} int to_exponent(double value); -template -inline bool compact_mantissa(Mantissa& mantissa, Exponent& exponent) { - bool result = false; - // If the mantissa is larger than 39 bits will allow attempt to shrink it - while (mantissa > 0x8000000000 || mantissa < -0x8000000000) { - mantissa /= 10; - exponent += 1; - result = true; - } - return result; -} class PriceSize { - int64_t _mantissa; - int8_t _exponent; + IntPrice _price; + // merged size uint32_t _size; + uint32_t _direct_size; public: - PriceSize(int64_t mantissa, int8_t exponent, Size size) : - _mantissa(mantissa), - _exponent(exponent), - _size(size) - { - // bts_assert_d(exponent != 0); - } - PriceSize() : _mantissa(0), _exponent(), _size(0) { } + PriceSize(IntPrice price, Size size) : + _price(size == 0 ? INT_NAN : price), + _size(size), + _direct_size(size) + { } + PriceSize(IntPrice price, Size direct_size, ImpliedSize implied_size) : + _price(price), + _size(direct_size + implied_size.value()), + _direct_size(direct_size) + { } + PriceSize() : _price(INT_NAN), _size(0), _direct_size(0) {} PriceSize(const PriceSize& other) = default; PriceSize& operator=(const PriceSize& other) = default; bool operator==(const PriceSize& other) const { - return (_size == other._size && _mantissa == other._mantissa && _exponent == other._exponent); + return (_size == other._size && _direct_size == other._direct_size && int_price() == other.int_price()); } bool operator!=(const PriceSize& other) const { return !(*this == other); } - - BTS_EXPORT static PriceSize NO_MARKET; - - template - static PriceSize from_double(double price, Size size) { - if (size == 0 || os::is_nan(price)) return NO_MARKET; - double temp; - if (fabs(modf(price, &temp)) != 0.0) { - const double rounding = 0.5 * pow10(-PRECISION); - price += (price > 0) ? rounding : -rounding; - price *= pow10(PRECISION); - return PriceSize((int64_t)price, -PRECISION, size); - } - return PriceSize((int64_t)price, 0, size); + PriceSize operator-(IntPrice price) const{ + PriceSize res=*this; + res._price=res._price-price; + return res; } - static PriceSize from_double(double price, int precision, Size size) { - if (size == 0 || os::is_nan(price)) return NO_MARKET; - double temp; - if (fabs(modf(price, &temp)) != 0.0) { - const double rounding = 0.5 * pow10(-precision); - price += (price > 0) ? rounding : -rounding; - price *= pow10(precision); - return PriceSize((int64_t)price, -precision, size); - } - return PriceSize((int64_t)price, 0, size); - } - // // Accessors - int64_t mantissa() const { - return _mantissa; - } - int8_t exponent() const { - return _exponent; - } - // This call is relatively slow so don't call it often - double price() const { - if (*this == NO_MARKET) return NO_PRICE; - return mantissa() * pow10(exponent()); - } - Size size() const { - return _size; - } + int64_t mantissa() const { return _price.mantissa(); } + int8_t exponent() const { return _price.exponent(); } + Size size() const { return _size; } + Size direct_size() const { return _direct_size; } + Size implied_size() const { return _size - _direct_size; } + double price() const { return _price.price(); } + IntPrice int_price() const { return _price; } + bool is_nan() const { return _price.is_nan(); } }; +extern PriceSize NO_MARKET; static_assert(sizeof(PriceSize) == 16, "PriceSize should be small"); + +template +PriceSize PriceSize_from_double(double price, Size size) { + if (size == 0 || os::is_nan(price)) return NO_MARKET; + return { IntPrice_from_double(price), size }; +} + typedef utils::Optimistic AtomicPriceSize; @@ -127,63 +86,34 @@ typedef utils::Optimistic AtomicPriceSize; // This structure can store the same data in 24 bytes so it's more efficient for shared memory struct PriceLevel { PriceLevel() : - _mantissa_b(0), _exponent_b(0), - _mantissa_a(0), _exponent_a(0), - _size_b(0), _size_a(0) {} - PriceLevel( - int64_t bid_mantissa, - int8_t bid_exponent, - uint32_t bid_size, - int64_t ask_mantissa, - int8_t ask_exponent, - uint32_t ask_size) : - _mantissa_b(bid_mantissa), _exponent_b(bid_exponent), - _mantissa_a(ask_mantissa), _exponent_a(ask_exponent), - _size_b(bid_size), - _size_a(ask_size) - {} + _bid(), _ask(), + _bid_size(0), _ask_size(0) {} PriceLevel(const PriceSize& bid, const PriceSize& ask): - _mantissa_b(bid.mantissa()), _exponent_b(bid.exponent()), - _mantissa_a(ask.mantissa()), _exponent_a(ask.exponent()), - _size_b(bid.size()), _size_a(ask.size()) + _bid(bid.int_price()), _ask(ask.int_price()), + _bid_size(bid.size()), _ask_size(ask.size()) {} -#ifdef _WIN32 - int64_t _mantissa_b : 56; - int64_t _exponent_b : 8; - int64_t _mantissa_a : 56; - int64_t _exponent_a : 8; -#else - int64_t _mantissa_b : 56; - int8_t _exponent_b : 8; - int64_t _mantissa_a : 56; - int8_t _exponent_a : 8; -#endif - uint32_t _size_b; - uint32_t _size_a; + IntPrice _bid; + IntPrice _ask; + uint32_t _bid_size; + uint32_t _ask_size; - uint64_t bid_mantissa() const { return _mantissa_b; } - uint64_t ask_mantissa() const { return _mantissa_a; } - int8_t bid_exponent() const { return _exponent_b; } - int8_t ask_exponent() const { return _exponent_a; } - uint32_t bid_size() const { return _size_b; } - uint32_t ask_size() const { return _size_a; } + const IntPrice& bid_price() const { return _bid; } + const IntPrice& ask_price() const { return _ask; } + uint32_t bid_size() const { return _bid_size; } + uint32_t ask_size() const { return _ask_size; } bool operator==(const PriceLevel& other) const { - return _size_a == other._size_a && - _size_b == other._size_b && - _mantissa_a == other._mantissa_a && - _mantissa_b == other._mantissa_b && - _exponent_a == other._exponent_a && - _exponent_b == other._exponent_b; + return _ask_size == other._ask_size && + _bid_size == other._bid_size && + _bid == other._bid && + _ask == other._ask; } bool operator!=(const PriceLevel& other) const { - return _size_a != other._size_a || - _size_b != other._size_b || - _mantissa_a != other._mantissa_a || - _mantissa_b != other._mantissa_b || - _exponent_a != other._exponent_a || - _exponent_b != other._exponent_b; + return _ask_size != other._ask_size || + _bid_size != other._bid_size || + _bid != other._bid || + _ask != other._ask; } bool update(const PriceLevel& temp) { @@ -195,14 +125,33 @@ struct PriceLevel { } PriceSize bid() const { - return PriceSize(_mantissa_b, _exponent_b, _size_b); + return PriceSize(_bid, _bid_size); } PriceSize ask() const { - return PriceSize(_mantissa_a, _exponent_a, _size_a); + return PriceSize(_ask, _ask_size); } }; static_assert(sizeof(PriceLevel) == 24, "PriceLevel should be small"); +struct ImpliedPriceLevel : private PriceLevel { + ImpliedPriceLevel() : PriceLevel() {} + ImpliedPriceLevel(const PriceSize& bid, const PriceSize& ask): + PriceLevel(bid,ask) {} + using PriceLevel::bid_price; + using PriceLevel::ask_price; + using PriceLevel::bid_size; + using PriceLevel::ask_size; + using PriceLevel::operator==; + using PriceLevel::operator!=; + bool operator=(const PriceLevel& other) { return update(other); } + PriceSize bid() const { + return PriceSize(_bid, 0, ImpliedSize(_bid_size)); + } + PriceSize ask() const { + return PriceSize(_ask, 0, ImpliedSize(_ask_size)); + } +}; +static_assert(sizeof(ImpliedPriceLevel) == sizeof(PriceLevel), "ImpliedPriceLevel should not be bigger than PriceLevel"); }}//namespace diff --git a/src/lib/md/SettlementRecorder.cpp b/src/lib/md/SettlementRecorder.cpp index cef22e6f7..dfa1eb549 100644 --- a/src/lib/md/SettlementRecorder.cpp +++ b/src/lib/md/SettlementRecorder.cpp @@ -26,7 +26,7 @@ enum WorkType { struct Work { WorkType type; MDProducerHandle handle; - Price price; + IntPrice price; Date date; }; @@ -43,10 +43,11 @@ struct SettlementRecorderImpl { sqlite3_stmt_ptr insert_stmt; }; -bool record_settle(sqlite3_stmt* insert, security_key key, Price price, Date date) { +bool record_settle(sqlite3_stmt* insert, security_key key, IntPrice price, Date date) { sqlite3_bind_int64(insert, 1, key); sqlite3_bind_int(insert, 2, date.to_short()); - sqlite3_bind_double(insert, 3, price); + sqlite3_bind_int64(insert, 3, price.mantissa()); + sqlite3_bind_int(insert, 4, price.exponent()); auto result = sqlite3_step(insert); sqlite3_reset(insert); return (result == SQLITE_DONE || result == SQLITE_ROW); @@ -56,14 +57,16 @@ void publish_settles(sqlite3_stmt* select, MDProducerHandle handle) { sqlite3_bind_int(select, 1, handle.key()); auto result = sqlite3_step(select); if (result == SQLITE_ROW) { - Price price = sqlite3_column_double(select, 0); - Date date = Date(sqlite3_column_int(select, 1)); - handle.update_settlement(TimeStamp::now(), price, date); + int64_t mantissa = sqlite3_column_int(select, 0); + int exponent = sqlite3_column_int(select, 1); + Date date = Date(sqlite3_column_int(select, 2)); + handle.update_settlement(TimeStamp::now(), {mantissa,(int8_t)exponent}, date); result = sqlite3_step(select); if (result == SQLITE_ROW) { - price = sqlite3_column_double(select, 0); - date = Date(sqlite3_column_int(select, 1)); - handle.update_previous_settlement(TimeStamp::now(), price, date); + int64_t mantissa = sqlite3_column_int(select, 0); + int exponent = sqlite3_column_int(select, 1); + date = Date(sqlite3_column_int(select, 2)); + handle.update_previous_settlement(TimeStamp::now(), {mantissa,(int8_t)exponent}, date); } } sqlite3_reset(select); @@ -97,28 +100,29 @@ SettlementRecorder::SettlementRecorder(Platform platform) : sqlite3_exec_nofail(db, "PRAGMA journal_mode=OFF"); sqlite3_exec_nofail(db, "PRAGMA syncronous=0"); // key=(security_key+date) - sqlite3_exec_nofail(db, "CREATE TABLE IF NOT EXISTS settlements (key INTEGER NOT NULL, date INTEGER NOT NULL, price REAL NOT NULL, PRIMARY KEY (key,date));"); + sqlite3_exec_nofail(db, + "CREATE TABLE IF NOT EXISTS settlements2 (key INTEGER NOT NULL, date INTEGER NOT NULL, mantissa INTEGER NOT NULL, exponent INTEGER NOT NULL, PRIMARY KEY (key,date));"); // Delete any records > 10 days old auto cutoff = datetime::local_now() - boost::gregorian::days(10); auto encoded = datetime::ll_to_date(datetime::ptime_to_ll(cutoff)).to_short(); char query[256]; - sprintf(query, "delete from settlements where date < %i", encoded); + sprintf(query, "delete from settlements2 where date < %i", encoded); sqlite3_exec_nofail(db, query); // statements - _self->select_stmt = prepare_query(db, "select price,date from settlements where key=? order by date desc limit 2"); - _self->insert_stmt = prepare_query(db, "insert or replace into settlements(key, date, price) values(?,?,?)"); + _self->select_stmt = prepare_query(db, "select mantissa,exponent,date from settlements2 where key=? order by date desc limit 2"); + _self->insert_stmt = prepare_query(db, "insert or replace into settlements2(key, date, mantissa,exponent) values(?,?,?,?)"); } SettlementRecorder::~SettlementRecorder() { } void SettlementRecorder::restore_settlement(MDProducerHandle handle) { - Work work = { SNAPSHOT, handle, 0, Date() }; + Work work = { SNAPSHOT, handle, INT_NAN, Date() }; _self->work_queue.push(work); } -void SettlementRecorder::add_settlement(MDProducerHandle handle, Price price, datetime::Date date) { +void SettlementRecorder::add_settlement(MDProducerHandle handle, IntPrice price, datetime::Date date) { Work work = { UPDATE, handle, price, date }; _self->work_queue.push(work); } diff --git a/src/lib/md/SettlementRecorder.h b/src/lib/md/SettlementRecorder.h index 536ca374f..2dfc9844b 100644 --- a/src/lib/md/SettlementRecorder.h +++ b/src/lib/md/SettlementRecorder.h @@ -19,7 +19,7 @@ public: // Put settlement update in a queue // in do_work() drain queue and update settlement 'depth' void restore_settlement(MDProducerHandle handle); - void add_settlement(MDProducerHandle handle, Price price, datetime::Date date); + void add_settlement(MDProducerHandle handle, IntPrice price, datetime::Date date); void do_work(); private: diff --git a/src/lib/md/Shm/MDStructure.h b/src/lib/md/Shm/MDStructure.h index c9fa769ac..6a069fb60 100644 --- a/src/lib/md/Shm/MDStructure.h +++ b/src/lib/md/Shm/MDStructure.h @@ -26,7 +26,7 @@ namespace md { using utils::TimeStamp; -const uint32_t shm_version = 100; +const uint32_t shm_version = 101; struct ALIGNED_(CACHE_LINE) HotHeader { size_t count; @@ -45,6 +45,7 @@ class HotEntry { PriceLevel book; // 24 security_key key; // 4 uint32_t sequence; // 4 + IntPrice settlement; // 8 uint8_t status; // 1 void set_time(const TimeStamp& t) { timestamp.store(t, std::memory_order_release); } TimeStamp time() const { return timestamp.load(std::memory_order_acquire); } @@ -52,7 +53,7 @@ class HotEntry { static_assert(sizeof(HotEntry) <= CACHE_LINE, "HotEntry should be small"); const uint32_t COCONUT_MAX_DEPTH = 10; -const uint32_t COCONUT_IMPLIED_DEPTH = 2; +const uint32_t COCONUT_MAX_IMPLIED_DEPTH = 2; class HotDepthEntry { std::atomic timestamp; // 8 public: @@ -67,45 +68,34 @@ class HotDepthEntry { uint32_t size() const { return sizeof(HotDepthEntry); } security_key key; // 4 NOTE: key is at a different offset than it is in HotEntry uint32_t sequence; // 4 + IntPrice settlement; // 8 std::array direct; // 240 - std::array implied; // 48 + std::array implied; // 48 uint8_t status; // 1 - const PriceLevel* at(uint32_t depth) const { - if (depth < direct.size()) { - return &direct[depth]; - } - return nullptr; - } void read_direct(uint32_t depth, PriceSize* bid, PriceSize* ask) const { - if (depth < direct.size()) { - *bid = direct[depth].bid(); - *ask = direct[depth].ask(); - } + *bid = direct[depth].bid(); + *ask = direct[depth].ask(); } void read_implied(uint32_t depth, PriceSize* bid, PriceSize* ask) const { - if (depth < implied.size()) { - *bid = implied[depth].bid(); - *ask = implied[depth].ask(); - } + *bid = implied[depth].bid(); + *ask = implied[depth].ask(); } bool write_direct(uint32_t depth, const PriceSize& bid, const PriceSize& ask) { - if (depth < direct.size()) { - auto temp = PriceLevel(bid,ask); - if (direct[depth] != temp) { - direct[depth] = PriceLevel(bid,ask); - return true; - } + bts_assert_d(depth < direct.size()); + auto temp = PriceLevel(bid,ask); + if (direct[depth] != temp) { + direct[depth] = temp; + return true; } return false; } bool write_implied(uint32_t depth, const PriceSize& bid, const PriceSize& ask) { - if (depth < implied.size()) { - auto temp = PriceLevel(bid,ask); - if (implied[depth] != temp) { - implied[depth] = PriceLevel(bid,ask); - return true; - } + bts_assert_d(depth < implied.size()); + auto temp = PriceLevel(bid,ask); + if (implied[depth] != temp) { + implied[depth] = temp; + return true; } return false; } @@ -168,9 +158,8 @@ struct ALIGNED_(CACHE_LINE) ColdHeader { struct MiscData { MiscData(): misc_timestamp(), - settlement_price(NO_PRICE), settlement_date(), - previous_settlement_price(NO_PRICE), + previous_settlement_price(INT_NAN), previous_settlement_date(), open_price(NO_PRICE), open_date(), @@ -185,9 +174,8 @@ struct MiscData { { } TimeStamp misc_timestamp; // Settlement - Price settlement_price; datetime::Date settlement_date; - Price previous_settlement_price; + IntPrice previous_settlement_price; datetime::Date previous_settlement_date; // Opening/Closing @@ -206,7 +194,6 @@ struct MiscData { bool operator==(const MiscData& other) const { return - settlement_price == other.settlement_price && settlement_date == other.settlement_date && previous_settlement_price == other.previous_settlement_price && previous_settlement_date == other.previous_settlement_date && diff --git a/src/lib/md/SizeTypes.h b/src/lib/md/SizeTypes.h new file mode 100644 index 000000000..16484c8dd --- /dev/null +++ b/src/lib/md/SizeTypes.h @@ -0,0 +1,6 @@ +#pragma once +#include "lib/utils/ValueWrapper.h" +namespace bts { +namespace md { + using ImpliedSize = utils::ValueWrapper; +}} \ No newline at end of file diff --git a/src/lib/md/protos/co2.proto b/src/lib/md/protos/co2.proto index febeb7f29..369d33069 100644 --- a/src/lib/md/protos/co2.proto +++ b/src/lib/md/protos/co2.proto @@ -50,13 +50,13 @@ message MDRequest { repeated SecurityChannel channels = 1; enum Flags { SECURITY = 0; - BBO = 0x1; - DEPTH = 0x2; + BBO = 0x1; + DIRECT = 0x2; LAST_TRADE = 0x4; TRADE_LOG = 0x8; RFQ = 0x10; - // CUSTOMER_BBO = 0x20; - MISC = 0x40; + MISC = 0x30; + IMPLIED = 0x40; } string host = 2; } @@ -64,7 +64,8 @@ message MDRequest { message ChannelUpdate { uint32 key = 1; bts.api.Security security = 2; - BookMessage book = 3; + BookMessage direct = 3; + BookMessage implied = 4; MiscMessage misc = 5; TradeMessage last_trade = 6; } @@ -81,6 +82,11 @@ message MDReply { repeated ChannelReply channels = 1; } +message PriceMessage { + int64 mantissa = 1; + sint32 exponent = 2; +} + message PriceSizeMessage { int64 mantissa = 1; sint32 exponent = 2; @@ -103,20 +109,22 @@ message BookMessage { message MiscMessage { // int32 key = 1; - double settlement_price = 2; - uint32 settlement_date = 3; - double previous_settlement_price = 20; - uint32 previous_settlement_date = 21; - double open_price = 4; - uint32 open_date = 5; - double close_price = 6; - uint32 close_date = 7; - double session_high = 8; - double session_low = 9; - double open_interest = 10; - double session_high_bid = 11; - double session_low_offer = 12; - double fixing_price = 13; + reserved 1; + reserved 2; + PriceMessage settlement = 22; + uint32 settlement_date = 3; + PriceMessage previous_settlement_price = 23; + uint32 previous_settlement_date = 21; + double open_price = 4; + uint32 open_date = 5; + double close_price = 6; + uint32 close_date = 7; + double session_high = 8; + double session_low = 9; + double open_interest = 10; + double session_high_bid = 11; + double session_low_offer = 12; + double fixing_price = 13; } enum AggressorSide { @@ -139,25 +147,25 @@ message TradeMessage { } message QuoteRequestMessage { enum Side { - UNDEFINED = 0; - BUY = 49; - SELL = 50; - CROSS = 99; - } - uint32 key = 1; - int64 time = 2; - string request_id = 3; - int32 size = 5; - Side side = 6; - bool tradable = 7; - bool gap = 8; + UNDEFINED = 0; + BUY = 49; + SELL = 50; + CROSS = 99; + } + uint32 key = 1; + int64 time = 2; + string request_id = 3; + int32 size = 5; + Side side = 6; + bool tradable = 7; + bool gap = 8; } message TradeList { - repeated TradeMessage items = 1; + repeated TradeMessage items = 1; } message RFQList { - repeated QuoteRequestMessage items = 1; + repeated QuoteRequestMessage items = 1; } message CoconutReplayHeader { diff --git a/src/lib/md/relay/QueryServer.cpp b/src/lib/md/relay/QueryServer.cpp index d6252b220..05e08ec41 100644 --- a/src/lib/md/relay/QueryServer.cpp +++ b/src/lib/md/relay/QueryServer.cpp @@ -133,10 +133,15 @@ void set_price(PriceSize ps, co2::TradeMessage* ptrade) { ptrade->set_size(ps.size()); } -void copy_misc(MiscData misc, co2::MiscMessage* pmisc) { - pmisc->set_settlement_price(misc.settlement_price); +void set_price(const IntPrice& price, co2::PriceMessage* dest) { + dest->set_mantissa(price.mantissa()); + dest->set_exponent(price.exponent()); +} + +void copy_misc(IntPrice settlement, MiscData misc, co2::MiscMessage* pmisc) { + set_price(settlement, pmisc->mutable_settlement()); pmisc->set_settlement_date(misc.settlement_date.to_short()); - pmisc->set_previous_settlement_price(misc.previous_settlement_price); + set_price(misc.previous_settlement_price, pmisc->mutable_previous_settlement_price()); pmisc->set_previous_settlement_date(misc.previous_settlement_date.to_short()); pmisc->set_open_price(misc.open_price); pmisc->set_open_date(misc.open_date.to_short()); @@ -241,20 +246,20 @@ public: pupd->mutable_security()->CopyFrom(handle.get_normalized_security()); snapshot = true; } - if (HASFLAG(flags, MDRequest::BBO) || HASFLAG(flags, MDRequest::DEPTH)) { + if (HASFLAG(flags, MDRequest::BBO) || HASFLAG(flags, MDRequest::DIRECT)) { std::array bids, asks; TimeStamp time(snapshot ? 0 : chan.time()); - size_t copy_depth = HASFLAG(flags, MDRequest::DEPTH) ? COCONUT_MAX_DEPTH : 1; - auto result = handle.get_depth(time, copy_depth, bids.data(), asks.data()); + size_t copy_depth = HASFLAG(flags, MDRequest::DIRECT) ? COCONUT_MAX_DEPTH : 1; + auto result = handle.get_direct_book(time, copy_depth, bids.data(), asks.data()); if (result == HandleReadResult::SUCCESS) { - auto* p = pupd->mutable_book(); + auto* p = pupd->mutable_direct(); for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { PriceSize b = bids[x]; - if (b != PriceSize::NO_MARKET) { + if (b != NO_MARKET) { convert(b, p->add_bids()); } PriceSize a = asks[x]; - if (a != PriceSize::NO_MARKET) { + if (a != NO_MARKET) { convert(a, p->add_asks()); } } @@ -270,6 +275,29 @@ public: reply->mutable_channel()->set_time(chan.time()); } } + if ((HASFLAG(flags, MDRequest::IMPLIED) || HASFLAG(flags, MDRequest::BBO)) && !handle.is_synthetic()) { + std::array bids, asks; + TimeStamp time(snapshot ? 0 : chan.time()); + size_t copy_depth = HASFLAG(flags, MDRequest::IMPLIED) ? COCONUT_MAX_IMPLIED_DEPTH : 1; + auto result = handle.get_implied_book(time, copy_depth, bids.data(), asks.data()); + if (result == HandleReadResult::SUCCESS) { + auto* p = pupd->mutable_implied(); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + PriceSize b = bids[x]; + if (b != NO_MARKET) { + convert(b, p->add_bids()); + } + PriceSize a = asks[x]; + if (a != NO_MARKET) { + convert(a, p->add_asks()); + } + } + } + else if (result == HandleReadResult::BUSY) { + LOG(md,Info, "Busy read on handle (implied) {}. Retry on next request", instrument.handle.key()); + reply->mutable_channel()->set_time(chan.time()); + } + } if (HASFLAG(flags, MDRequest::LAST_TRADE)) { TimeStamp time(snapshot ? 0 : chan.time()); PriceSize price; @@ -286,10 +314,11 @@ public: if (HASFLAG(flags, MDRequest::MISC)) { TimeStamp time(snapshot ? 0 : chan.time()); if (handle.misc_timestamp() > time) { - copy_misc(handle.misc(), pupd->mutable_misc()); + copy_misc(handle.settlement_price(), handle.misc(), pupd->mutable_misc()); } } - if (pupd->has_book() || + if (pupd->has_direct() || + pupd->has_implied() || pupd->has_last_trade() || pupd->has_misc() || pupd->has_security()) { diff --git a/src/lib/md/relay/RelayClient.cpp b/src/lib/md/relay/RelayClient.cpp index f3aac8fb3..64aa81bfb 100644 --- a/src/lib/md/relay/RelayClient.cpp +++ b/src/lib/md/relay/RelayClient.cpp @@ -32,17 +32,21 @@ namespace md { using namespace co2; PriceSize convert(const PriceSizeMessage& psm) { - return psm.size() > 0 ? PriceSize(psm.mantissa(), psm.exponent(), psm.size()) : PriceSize::NO_MARKET; + return psm.size() > 0 ? PriceSize({psm.mantissa(), (int8_t)psm.exponent()}, psm.size()) : NO_MARKET; } PriceSize trade_pricesize(const co2::TradeMessage& trade) { return trade.size() > 0 ? - PriceSize(trade.mantissa(), trade.exponent(), trade.size()) : PriceSize::NO_MARKET; + PriceSize({trade.mantissa(), (int8_t)trade.exponent()}, trade.size()) : NO_MARKET; } PriceSize book_at(const google::protobuf::RepeatedPtrField& side, int index) { if (side.size() > index) { return convert(side.Get(index)); } - return PriceSize::NO_MARKET; + return NO_MARKET; +} + +IntPrice convert(const PriceMessage& price) { + return { price.mantissa(), (int8_t)price.exponent() }; } class RelayProducer : public IRelayWriter { @@ -54,18 +58,23 @@ class RelayProducer : public IRelayWriter { utils::TimeStamp updated = utils::TimeStamp::now(); bool snapshot = update.has_security(); // If this is a snapshot an empty book() means "no market" rather than "no change" - if (update.has_book() || snapshot) { + if (update.has_direct() || update.has_implied() || snapshot) { std::array bids, asks; + std::array implied_bids, implied_asks; for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { - bids[x] = book_at(update.book().bids(), x); - asks[x] = book_at(update.book().asks(), x); + bids[x] = book_at(update.direct().bids(), x); + asks[x] = book_at(update.direct().asks(), x); } - handle.update_book(updated, bids, asks); + for (size_t x = 0; x (7.2, 101), - PriceSize::from_double<2>(-9.7, 245)); + PriceSize_from_double<2>(7.2, 101), + PriceSize_from_double<2>(-9.7, 245)); entry.sequence = 777; entry.status = COCONUTSECURITYSTATUS_PACKET_LOSS; + entry.settlement = IntPrice_from_double<7>(98.6); entry.set_time(utils::TimeStamp::now()); } @@ -49,12 +50,18 @@ extern "C" BTS_EXPORT void co2_get_sample_depth(size_t& size, HotDepthEntry& ent entry.key = 1234; for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { entry.write_direct(x, - PriceSize::from_double<2>(x + 0.5, x + 1), - PriceSize::from_double<2>(x + 0.6, x + 2)); + PriceSize_from_double<2>(x + 0.5, x + 1), + PriceSize_from_double<2>(x + 0.6, x + 2)); + } + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + entry.write_implied(x, + PriceSize_from_double<3>(77, x + 1), + PriceSize_from_double<3>(88, x + 2)); } entry.set_time(utils::TimeStamp::now()); entry.status = COCONUTSECURITYSTATUS_PACKET_LOSS; entry.sequence = 888; + entry.settlement = IntPrice_from_double<7>(98.6); } extern "C" BTS_EXPORT void co2_get_sample_cold(size_t& size, ColdEntry& entry) { @@ -62,13 +69,12 @@ extern "C" BTS_EXPORT void co2_get_sample_cold(size_t& size, ColdEntry& entry) { entry.exchange_status = TRADINGSTATUS_NORMAL; // Last trade - entry.last_trade.store(PriceSize::from_double<2>(4.2, 101)); + entry.last_trade.store(PriceSize_from_double<2>(4.2, 101)); entry.total_volume = 12345; entry.last_trade_time = datetime::ll_now(); // Settlement - entry.misc.settlement_price = 3.3; entry.misc.settlement_date = datetime::Date(2015, 9, 3); - entry.misc.previous_settlement_price = 3.3; + entry.misc.previous_settlement_price = IntPrice_from_double<7>(3.3); entry.misc.previous_settlement_date = datetime::Date(2015, 9, 2); // Open/close entry.misc.open_price = 4.4; @@ -102,21 +108,31 @@ extern "C" BTS_EXPORT void co2_get_sample_cold_header(ColdHeader& header) { // Do not write to trade_buffer or rfq_buffer } -extern "C" BTS_EXPORT MDProducer* co2_produce_test_md() { +struct TestProducer { + std::shared_ptr md; + std::vector handles; +}; + +extern "C" BTS_EXPORT TestProducer* co2_produce_test_md() { config::internal::initialize_for_test( "PlatformMap {" " value {" " glbx_platform: 'PLATFORM_SIM'" " }" "}"); - auto* p = new MDProducer(PLATFORM_SIM); - auto producers = create_test_securities(*p); + auto* tp = new TestProducer(); + tp->md = std::make_shared(PLATFORM_SIM, false); + tp->handles = create_test_securities(*(tp->md)); + auto& producers = tp->handles; assign_ids(producers.begin(), producers.end()); for (auto& p : producers) { - p.update_bbo(utils::TimeStamp::now(), PriceSize::from_double<3>(1.1, 101), PriceSize::from_double<3>(2.2,202)); - p.add_trade(utils::TimeStamp::now(), PriceSize::from_double<3>(5.5, 505), datetime::ll_from_date(datetime::Date(2015, 9, 22)), AGGRESSOR_SIDE_NONE, 4200); - p.update_settlement(utils::TimeStamp::now(), 6.6, datetime::Date(2000, 8, 8)); - p.update_settlement(utils::TimeStamp::now(), 6.6, datetime::Date(2000, 8, 9)); + p.update_bbo(utils::TimeStamp::now(), PriceSize_from_double<3>(1.1, 101), PriceSize_from_double<3>(2.2,202)); + p.add_trade(utils::TimeStamp::now(), + PriceSize_from_double<3>(5.5, 505), + datetime::ll_from_date(datetime::Date(2015, 9, 22)), + AGGRESSOR_SIDE_NONE, 4200); + p.update_settlement(utils::TimeStamp::now(), IntPrice_from_double<7>(6.6), datetime::Date(2000, 8, 8)); + p.update_settlement(utils::TimeStamp::now(), IntPrice_from_double<7>(6.6), datetime::Date(2000, 8, 9)); p.update_opening(utils::TimeStamp::now(), 7.7, datetime::Date(2001, 1, 2)); auto t = utils::TimeStamp::now(); @@ -129,16 +145,24 @@ extern "C" BTS_EXPORT MDProducer* co2_produce_test_md() { p.update_fixing_price(t, 14.14); } - return p; + return tp; } -extern "C" BTS_EXPORT void co2_destroy_test_md(MDProducer* producer) { +extern "C" BTS_EXPORT void co2_destroy_test_md(TestProducer* producer) { if (producer != nullptr) delete producer; config::internal::reset(); } extern "C" BTS_EXPORT void co2_get_zero_and_nan_book(PriceLevel& price) { - price = { PriceSize::NO_MARKET, PriceSize::from_double<7>(0,1) }; + price = { NO_MARKET, PriceSize_from_double<7>(0,1) }; +} +extern "C" BTS_EXPORT void co2_get_nan_price_size(PriceSize& ps) { + ps = NO_MARKET; +} +extern "C" BTS_EXPORT void co2_test_get_constants(size_t& max_depth, size_t& max_implied_depth, uint8_t& nan_exponent) { + max_depth = COCONUT_MAX_DEPTH; + max_implied_depth = COCONUT_MAX_IMPLIED_DEPTH; + nan_exponent = INT_NAN.exponent(); } extern "C" BTS_EXPORT void co2_get_sample_datetime(datetime::LLDateTime& now, datetime::LLDateTime& utc_now) { diff --git a/src/lib/md/test/md2_tests.cpp b/src/lib/md/test/md2_tests.cpp index f98387ad6..114cf3f0a 100644 --- a/src/lib/md/test/md2_tests.cpp +++ b/src/lib/md/test/md2_tests.cpp @@ -55,8 +55,8 @@ Platform TEST_PLATFORM = PLATFORM_SIM; // Helper functions for Producer/Consumer tests // vector create_test_securities(MDProducer& producer, const char* postfix ="") { - FixedOptionSecurity fos; FixedFutureSecurity ffs; + FixedOptionSecurity fos; ffs.symbol("FOO"+string(postfix)); fos.symbol("FOOX"+string(postfix)); ffs.exchange = VENUE_BTS; @@ -109,18 +109,16 @@ Security mkoption(string symbol, security_key key, int year, int month, int day, result.mutable_option()->set_call_put(PUTORCALL_PUT); return result; } -unique_ptr mkfixedoption(string symbol, security_key key, int year, int month, int day, double strike, PutOrCall callput) { - unique_ptr fos(new FixedOptionSecurity()); - fos->type = SECURITYTYPE_OPTION; - fos->exchange = VENUE_BTS; - fos->key = key; - fos->strike = strike; - fos->expiration_day = day; - fos->expiration_month = month; - fos->expiration_year = year; - fos->call_put = callput; - fos->symbol(symbol); - return fos; +unique_ptr mkfixedfuture(string symbol, security_key key, int year, int month, int day) { + unique_ptr ffs(new FixedFutureSecurity()); + ffs->type = SECURITYTYPE_FUTURE; + ffs->exchange = VENUE_BTS; + ffs->key = key; + ffs->expiration_day = day; + ffs->expiration_month = month; + ffs->expiration_year = year; + ffs->symbol(symbol); + return ffs; } Security mkspread(string symbol, security_key key, const vector& legs) { Security result; @@ -198,8 +196,8 @@ public: // PriceSize violates some type punning rules // So it is important to have this test work correctly on the target platform PriceSize p2; - auto bid = PriceSize::from_double<2>(4.5, 14); - auto ask = PriceSize::from_double<9>(4.5, 14); + auto bid = PriceSize_from_double<2>(4.5, 14); + auto ask = PriceSize_from_double<9>(4.5, 14); PriceLevel memory; memory = PriceLevel(bid,ask); REQUIRE(bid.price() == memory.bid().price()); @@ -211,21 +209,21 @@ public: REQUIRE(p.size() == 14); } - auto p = PriceSize::from_double<2>(4.0, 5); + auto p = PriceSize_from_double<2>(4.0, 5); memory = PriceLevel(p,p); for (auto p2 : { memory.bid(), memory.ask() }) { REQUIRE(p2.price() == 4.0); REQUIRE(p2.size() == 5); } - p = PriceSize::from_double<2>(-4.2, 6); + p = PriceSize_from_double<2>(-4.2, 6); memory = PriceLevel(p,p); for (auto p2 : { memory.bid(), memory.ask() }) { REQUIRE(p2.price() == -4.2); REQUIRE(p2.size() == 6); } - p = PriceSize(4, -1, 2); + p = PriceSize({4, -1}, 2); memory = PriceLevel(p,p); for (auto p2 : { memory.bid(), memory.ask() }) { REQUIRE(p2.price() == 0.4); @@ -234,7 +232,7 @@ public: // Test requirement for more than 32 bit mantissa double bigval = -9999.9921875; - p = PriceSize::from_double<7>(bigval, 42); + p = PriceSize_from_double<7>(bigval, 42); memory = PriceLevel(p,p); for (auto p2 : { memory.bid(), memory.ask() }) { REQUIRE(p2.price() == bigval); @@ -244,14 +242,14 @@ public: void test_price_size_required_precision() { auto min_tick_2017 = 9999.0078125; - auto p17 = PriceSize::from_double<7>(min_tick_2017, 2017); + auto p17 = PriceSize_from_double<7>(min_tick_2017, 2017); PriceLevel m17(p17, p17); for (auto p : { m17.bid(), m17.ask() }) { REQUIRE(min_tick_2017 == p.price()); REQUIRE(p.size() == 2017); } auto min_tick_2018 = 1999.00390625; - auto p18 = PriceSize::from_double<8>(min_tick_2018, 2018); + auto p18 = PriceSize_from_double<8>(min_tick_2018, 2018); PriceLevel m18(p18, p18); for (auto p : { m18.bid(), m18.ask() }) { REQUIRE(min_tick_2018 == p.price()); @@ -285,29 +283,6 @@ public: // Equal if the delta is less than error return delta < acceptable_error; } - void test_compact_exponent() { - ExchangeDecimal test_values[] = { - { 2030000000000, -9 }, // mantissa > pow(2,40) - { -2030000000000, -9 }, // mantissa > pow(2,40) - { 2022750000000, -9 },// mantissa > pow(2,40) - { -2022750000000, -9 },// mantissa > pow(2,40) - { 20227500000000, -10 },// mantissa > pow(2,40) - { 202275000000000, -11 },// mantissa > pow(2,40) - { 2022750000000000, -12 },// mantissa > pow(2,40) - { 20227500000000000, -13 },// mantissa > pow(2,40) - { -20227500000000000, -13 },// mantissa > pow(2,40) - { 203, 1}, // control - { -14000000000, -8 }, // control - { -14, 1 }, // control - { 20227500, -4 }, // control - }; - for (auto p : test_values) { - double expected = p.mantissa * pow10(p.exponent); - compact_mantissa(p.mantissa, p.exponent); - double compacted = p.mantissa * pow10(p.exponent); - REQUIRE(close_enough(expected, compacted)); - } - } void test_producer_consumer() { MDProducer producer(TEST_PLATFORM); @@ -316,7 +291,7 @@ public: // Add bbo data for those expirations auto write_time = TimeStamp::now(); for (auto& p : producers) { - p.update_bbo(write_time, PriceSize::from_double<2>(5.0, 5), PriceSize::from_double<2>(5.0, 5)); + p.update_bbo(write_time, PriceSize_from_double<2>(5.0, 5), PriceSize_from_double<2>(5.0, 5)); } // Symbol list for the consumer @@ -349,7 +324,7 @@ public: PriceSize bid, ask; TimeStamp read_time; // get_boo returns true becuase this time is after the change - REQUIRE(c.get_bbo(read_time, bid, ask) == HandleReadResult::SUCCESS); + REQUIRE(c.get_direct_book(read_time, 1, &bid, &ask) == HandleReadResult::SUCCESS); REQUIRE((read_time - TimeStamp()).seconds() > 0); REQUIRE(bid.price() == 5.0); REQUIRE(ask.price() == 5.0); @@ -361,21 +336,21 @@ public: auto read_time = TimeStamp::now(); for (const auto& c : consumers) { PriceSize bid, ask; - REQUIRE(c.get_bbo(read_time, bid, ask) != HandleReadResult::SUCCESS); + REQUIRE(c.get_direct_book(read_time, 1, &bid, &ask) != HandleReadResult::SUCCESS); } // update everything once more write_time = TimeStamp::now(); for (auto& p : producers) { // Update bid/ask with one call - p.update_bbo(write_time, PriceSize::from_double<2>(6.55, 6), PriceSize::from_double<2>(6.55, 6)); + p.update_bbo(write_time, PriceSize_from_double<2>(6.55, 6), PriceSize_from_double<2>(6.55, 6)); } // Now read_time is < write_time for (const auto& c : consumers) { TimeStamp modified_time = read_time; PriceSize bid, ask; - // get_bbo returns true becuase this time is after the change - REQUIRE(c.get_bbo(modified_time, bid, ask) == HandleReadResult::SUCCESS); + // get_direct_book returns true becuase this time is after the change + REQUIRE(c.get_direct_book(modified_time, 1, &bid, &ask) == HandleReadResult::SUCCESS); REQUIRE( (modified_time - read_time).seconds() > 0); REQUIRE(bid.price() == 6.55); REQUIRE(ask.price() == 6.55); @@ -448,17 +423,17 @@ public: // Set bbo on the producers - still not public TimeStamp write_time = TimeStamp::now(); for (auto& p : producers) { - p.update_bbo(write_time, PriceSize::from_double<3>(4.5, 5), PriceSize::from_double<3>(6.75, 7)); + p.update_bbo(write_time, PriceSize_from_double<3>(4.5, 5), PriceSize_from_double<3>(6.75, 7)); } // Even though bbo is available it is not given out because the producing security does not have an id PriceSize bid, ask; for(auto& c : consumers) { TimeStamp zerotime; - REQUIRE(c.get_bbo(zerotime, bid, ask) != HandleReadResult::SUCCESS); + REQUIRE(c.get_direct_book(zerotime, 1, &bid, &ask) != HandleReadResult::SUCCESS); } for(auto& mc : managed_consumers) { TimeStamp zerotime; - REQUIRE(mc->get_bbo(zerotime, bid, ask) != HandleReadResult::SUCCESS); + REQUIRE(mc->get_direct_book(zerotime, 1, &bid, &ask) != HandleReadResult::SUCCESS); } assign_ids(producers.begin(), producers.end()); @@ -467,7 +442,7 @@ public: // Existing old handles remain invalid for(auto& c : consumers) { TimeStamp zerotime; - if (c.get_bbo(zerotime, bid, ask) == HandleReadResult::SUCCESS) { + if (c.get_direct_book(zerotime, 1, &bid, &ask) == HandleReadResult::SUCCESS) { bbo_count++; } } @@ -475,7 +450,7 @@ public: // Now, magically, the existing (managed) handles are valid for(auto& mc : managed_consumers) { TimeStamp zerotime; - if (mc->get_bbo(zerotime, bid, ask) == HandleReadResult::SUCCESS) { + if (mc->get_direct_book(zerotime, 1, &bid, &ask) == HandleReadResult::SUCCESS) { bbo_count++; } } @@ -506,11 +481,11 @@ public: PriceSize bid, ask; for(auto& c : consumers) { TimeStamp zerotime; - REQUIRE(c.get_bbo(zerotime, bid, ask) != HandleReadResult::SUCCESS); + REQUIRE(c.get_direct_book(zerotime, 1, &bid, &ask) != HandleReadResult::SUCCESS); } for(auto& mc : managed_consumers) { TimeStamp zerotime; - REQUIRE(mc->get_bbo(zerotime, bid, ask) != HandleReadResult::SUCCESS); + REQUIRE(mc->get_direct_book(zerotime, 1, &bid, &ask) != HandleReadResult::SUCCESS); } // Only NOW create the producers (and the shared memory) @@ -519,7 +494,7 @@ public: // Set bbo on the producers - still not public TimeStamp write_time = TimeStamp::now(); for (auto& p : producers) { - p.update_bbo(write_time, PriceSize::from_double<3>( 4.5, 5 ), PriceSize::from_double<3>(6.75, 7)); + p.update_bbo(write_time, PriceSize_from_double<3>( 4.5, 5 ), PriceSize_from_double<3>(6.75, 7)); } assign_ids(producers.begin(), producers.end()); @@ -528,7 +503,7 @@ public: // Existing old handles remain invalid for(auto& c : consumers) { TimeStamp zerotime; - if (c.get_bbo(zerotime, bid, ask) == HandleReadResult::SUCCESS) { + if (c.get_direct_book(zerotime, 1, &bid, &ask) == HandleReadResult::SUCCESS) { bbo_count++; } } @@ -536,7 +511,7 @@ public: // Now, magically, the existing (managed) handles are valid for(auto& mc : managed_consumers) { TimeStamp zerotime; - if (mc->get_bbo(zerotime, bid, ask) == HandleReadResult::SUCCESS) { + if (mc->get_direct_book(zerotime, 1, &bid, &ask) == HandleReadResult::SUCCESS) { bbo_count++; } } @@ -618,7 +593,7 @@ public: assign_ids(producers.begin(), producers.end()); producer1->get_channel("FOO", SECURITYTYPE_FUTURE, VENUE_BTS).heartbeat(); producer1->get_channel("FOOX", SECURITYTYPE_OPTION, VENUE_BTS).heartbeat(); - for (auto& pro : producers) { pro.update_bbo(TimeStamp::now(), PriceSize(1, 0, 1), PriceSize(2, 0, 2)); } + for (auto& pro : producers) { pro.update_bbo(TimeStamp::now(), PriceSize({1, 0}, 1), PriceSize({2, 0}, 2)); } // Symbol list for the consumer MDRecoverableConsumer consumer(get_test_symbols(), nullptr); @@ -680,7 +655,7 @@ public: // set bbo to a different value for (auto& pro : producers) { - pro.update_bbo(TimeStamp::now(), PriceSize(4, 0, 5), PriceSize(6, 0, 7)); + pro.update_bbo(TimeStamp::now(), PriceSize({4, 0}, 5), PriceSize({6, 0}, 7)); } // Attempt to recover the managed handles @@ -693,7 +668,7 @@ public: REQUIRE(handle->is_heart_beating()); TimeStamp tzero; PriceSize bid, ask; - REQUIRE(handle->get_bbo(tzero, bid, ask) == HandleReadResult::SUCCESS); + REQUIRE(handle->get_direct_book(tzero, 1, &bid, &ask) == HandleReadResult::SUCCESS); REQUIRE(bid.price() == 4.0); REQUIRE(bid.size() == 5); REQUIRE(ask.price() == 6.0); @@ -712,10 +687,10 @@ public: #pragma warning(pop) #endif TimeStamp read = TimeStamp::now(); - src->producer().update_bbo(TimeStamp::invalid(), PriceSize::from_double<3>(100.0, 100), PriceSize::from_double<3>(101.0, 101)); + src->producer().update_bbo(TimeStamp::invalid(), PriceSize_from_double<3>(100.0, 100), PriceSize_from_double<3>(101.0, 101)); PriceSize bid, ask; TimeStamp start = TimeStamp::now(); - auto result = src->consumer().get_bbo(read, bid, ask); + auto result = src->consumer().get_direct_book(read, 1, &bid, &ask); TimeStamp end = TimeStamp::now(); REQUIRE(result != HandleReadResult::SUCCESS); REQUIRE((end - start).seconds() < 5); @@ -728,7 +703,7 @@ public: vector p2_no_id = create_test_securities(producer, "2"); vector p3 = create_test_securities(producer, "3"); - for (auto& p : producers) { p.update_bbo(TimeStamp::now(), PriceSize::from_double<3>(5.0, 5), PriceSize::from_double<3>(5.0, 5)); } + for (auto& p : producers) { p.update_bbo(TimeStamp::now(), PriceSize_from_double<3>(5.0, 5), PriceSize_from_double<3>(5.0, 5)); } assign_ids(producers.begin(), producers.end()); assign_ids(p3.begin(), p3.end()); @@ -959,41 +934,41 @@ public: { co2::SettlementRecorder recorder(PLATFORM_UNSET); - recorder.add_settlement(ph1, 101, yesterday); - recorder.add_settlement(ph1, 102, today); - recorder.add_settlement(ph2, 199, yesterday); - recorder.add_settlement(ph2, 188, today); + recorder.add_settlement(ph1, {101,0}, yesterday); + recorder.add_settlement(ph1, {102,0}, today); + recorder.add_settlement(ph2, {199,0}, yesterday); + recorder.add_settlement(ph2, {188,0}, today); recorder.do_work(); recorder.do_work(); } - REQUIRE(h1.misc().settlement_price == 102); + REQUIRE(h1.settlement_price().price() == 102); REQUIRE(h1.misc().settlement_date == today); - REQUIRE(h1.misc().previous_settlement_price == 101); + REQUIRE(h1.misc().previous_settlement_price.price() == 101); REQUIRE(h1.misc().previous_settlement_date == yesterday); - REQUIRE(h2.misc().settlement_price == 188); + REQUIRE(h2.settlement_price().price() == 188); REQUIRE(h2.misc().settlement_date == today); - REQUIRE(h2.misc().previous_settlement_price == 199); + REQUIRE(h2.misc().previous_settlement_price.price() == 199); REQUIRE(h2.misc().previous_settlement_date == yesterday); // Update but skip persist auto tomorrow = datetime::add_days(today, 1); - ph1.update_settlement(TimeStamp::now(), 103, tomorrow); - REQUIRE(h1.misc().settlement_price == 103); + ph1.update_settlement(TimeStamp::now(), {103,0}, tomorrow); + REQUIRE(h1.settlement_price().price() == 103); REQUIRE(h1.misc().settlement_date == tomorrow); - REQUIRE(h1.misc().previous_settlement_price == 102); + REQUIRE(h1.misc().previous_settlement_price.price() == 102); REQUIRE(h1.misc().previous_settlement_date == today); - ph1.update_previous_settlement(TimeStamp::now(), 0, Date()); - REQUIRE(h1.misc().settlement_price == 103); + ph1.update_previous_settlement(TimeStamp::now(), {0,0}, Date()); + REQUIRE(h1.settlement_price().price() == 103); REQUIRE(h1.misc().settlement_date == tomorrow); - ph1.update_settlement(TimeStamp::now(), 0, Date()); - REQUIRE(h1.misc().settlement_price == 0); + ph1.update_settlement(TimeStamp::now(),{0,0}, Date()); + REQUIRE(h1.settlement_price().price() == 0); REQUIRE(h1.misc().settlement_date.day == 0); // pushing a new settlement for today doesn't invalidate yesterday - ph2.update_settlement(TimeStamp::now(), 201, today); - REQUIRE(h2.misc().settlement_price == 201); + ph2.update_settlement(TimeStamp::now(), {201,0}, today); + REQUIRE(h2.settlement_price().price() == 201); REQUIRE(h2.misc().settlement_date == today); - REQUIRE(h2.misc().previous_settlement_price == 199); + REQUIRE(h2.misc().previous_settlement_price.price() == 199); REQUIRE(h2.misc().previous_settlement_date == yesterday); // Restore persisted state @@ -1003,13 +978,13 @@ public: recorder.restore_settlement(ph2); recorder.do_work(); } - REQUIRE(h1.misc().settlement_price == 102); + REQUIRE(h1.settlement_price().price() == 102); REQUIRE(h1.misc().settlement_date == today); - REQUIRE(h1.misc().previous_settlement_price == 101); + REQUIRE(h1.misc().previous_settlement_price.price() == 101); REQUIRE(h1.misc().previous_settlement_date == yesterday); - REQUIRE(h2.misc().settlement_price == 188); + REQUIRE(h2.settlement_price().price() == 188); REQUIRE(h2.misc().settlement_date == today); - REQUIRE(h2.misc().previous_settlement_price == 199); + REQUIRE(h2.misc().previous_settlement_price.price() == 199); REQUIRE(h2.misc().previous_settlement_date == yesterday); } #endif @@ -1039,9 +1014,6 @@ TEST_CASE("CO2Tests","[md]") { SECTION("test_slow_double_conversion") { co2_tests->test_slow_double_conversion(); } - SECTION("test_compact_exponent") { - co2_tests->test_compact_exponent(); - } SECTION("test_producer_consumer") { co2_tests->test_producer_consumer(); } @@ -1214,35 +1186,8 @@ struct Co2IndexTests { } } ; - - -// TEST_CASE ("Co2IndexTests","[md]") { -// Co2IndexTests co2_index_tests; -// co2_index_tests.setup(); - -// SECTION("test_simple_index_search") { -// co2_index_tests.test_simple_index_search(); -// } - -// SECTION("test_simple_index_parallel_search") { -// co2_index_tests.test_simple_index_parallel_search(); -// } - -// SECTION("test_simple_index_iterate") { -// co2_index_tests.test_simple_index_iterate(); -// } -// SECTION("test_concurrent_index_search") { -// co2_index_tests.test_concurrent_index_search(); -// } -// SECTION("test_concurrent_index_iterate") { -// co2_index_tests.test_concurrent_index_iterate(); -// } -// SECTION("test_concurrent_index_parallel_search") { -// co2_index_tests.test_concurrent_index_parallel_search(); -// } -// } - struct Co2RelayTests { + Co2RelayTests() : _stats() {} void setup() { config::internal::initialize_for_test( "PlatformMap {" @@ -1250,6 +1195,7 @@ struct Co2RelayTests { " glbx_platform: 'PLATFORM_SIM'" " }" "}"); + } void teardown() {} @@ -1273,67 +1219,83 @@ struct Co2RelayTests { return temp; } + metrics::MetricsContainer _stats; + std::unique_ptr setup_query_server(MDProducer& producer, vector* out_producers) { + // Create a bunch of securities + for (size_t x = 0; x < 100; ++x) { + // That's right. Expirations daily. + out_producers->push_back( + producer.add_security( + *mkfixedfuture("FOO", invalid_security_key, 2017, 1+(x/28), x%28))); + out_producers->push_back( + producer.add_security( + *mkfixedfuture("FOOF", invalid_security_key, 2017, 1+(x/28), x%28))); + } + assign_ids(out_producers->begin(), out_producers->end(), 1000); + + // set query server + set symbols; + symbols.insert({"FOO", VENUE_BTS, SECURITYTYPE_FUTURE}); + symbols.insert({"FOOF", VENUE_BTS, SECURITYTYPE_FUTURE}); + shared_ptr empty; + auto server = create_query_server(symbols, empty, _stats); + REQUIRE(server); + server->scan_securities(); + return server; + } + + void relay_nans() { MDProducer producer(TEST_PLATFORM); - UserSymbol symbol("FOO", VENUE_BTS, SECURITYTYPE_OPTION); // initialize data vector producers; - for (size_t x = 0; x < 100; ++x) { - auto strike = 1000 + (100 * x); - producers.push_back( - producer.add_security( - *mkfixedoption("FOO", invalid_security_key, 2017, 1, 28, strike, PUTORCALL_PUT))); - producers.push_back( - producer.add_security( - *mkfixedoption("FOO", invalid_security_key, 2017, 1, 28, strike, PUTORCALL_CALL))); - } - assign_ids(producers.begin(), producers.end(), 1000); - PriceSize price35 = PriceSize::from_double<2>(3.5, 5); - PriceSize price5 = PriceSize::from_double<2>(5, 5); + auto server = setup_query_server(producer, &producers); + + // assign prices + PriceSize price35 = PriceSize_from_double<2>(3.5, 5); + PriceSize price5 = PriceSize_from_double<2>(5, 5); for(auto p : producers) { p.update_bbo(TimeStamp::now(), price35, price35); } - // Read using QueryServer - set symbols; - symbols.insert(symbol); - shared_ptr empty; - metrics::MetricsContainer stats; - auto server = create_query_server(symbols, empty, stats); - REQUIRE(server); - server->scan_securities(); - + // Request via query server co2::MDRequest request; - auto* chan = request.add_channels(); - chan->set_type(symbol.type()); - chan->set_symbol(symbol.symbol()); - chan->set_exchange(symbol.exchange()); - chan->set_flags(co2::MDRequest::BBO | co2::MDRequest::SECURITY); + for (const auto& symbol : { "FOO", "FOOF" }) { + auto* chan = request.add_channels(); + chan->set_type(SECURITYTYPE_FUTURE); + chan->set_symbol(symbol); + chan->set_exchange(VENUE_BTS); + chan->set_flags(co2::MDRequest::BBO | co2::MDRequest::SECURITY); + } // This request should include everything co2::MDReply reply; server->query(request, &reply); reply = simulate_wire_transfer(reply); - REQUIRE((size_t)reply.channels().size() == symbols.size()); - REQUIRE((size_t)reply.channels(0).updates().size() == producers.size()); + REQUIRE((size_t)reply.channels().size() == 2); // update the request to match the server time - for (auto& update : reply.channels(0).updates()) { - REQUIRE(update.has_security()); - REQUIRE(update.has_book()); - REQUIRE(update.book().asks_size() > 0); - REQUIRE(update.book().bids_size() > 0); - REQUIRE(book_at(update.book().bids(), 0) == price35); - REQUIRE(book_at(update.book().asks(), 0) == price35); + for (int ch = 0; ch < 2; ++ch) { + REQUIRE((size_t)reply.channels(ch).updates().size() == producers.size()/2); + for (auto& update : reply.channels(ch).updates()) { + REQUIRE(update.has_security()); + REQUIRE(update.has_direct()); + REQUIRE(update.direct().asks_size() > 0); + REQUIRE(update.direct().bids_size() > 0); + REQUIRE(book_at(update.direct().bids(), 0) == price35); + REQUIRE(book_at(update.direct().asks(), 0) == price35); + } } // Second request with updated time should include nothing request.mutable_channels(0)->set_time(reply.channels(0).channel().time()); + request.mutable_channels(1)->set_time(reply.channels(1).channel().time()); reply.Clear(); server->query(request, &reply); reply = simulate_wire_transfer(reply); - REQUIRE(reply.channels().size() == 1); + REQUIRE(reply.channels().size() == 2); REQUIRE(reply.channels(0).updates().size() == 0); + REQUIRE(reply.channels(1).updates().size() == 0); // Update a single price and make sure we get that back producers[0].update_bbo(TimeStamp::now(), price5, price5, 1); @@ -1341,27 +1303,31 @@ struct Co2RelayTests { reply.Clear(); server->query(request, &reply); reply = simulate_wire_transfer(reply); - REQUIRE(reply.channels().size() == 1); + REQUIRE(reply.channels().size() == 2); REQUIRE(reply.channels(0).updates().size() == 1); - REQUIRE(reply.channels(0).updates(0).has_book()); - REQUIRE(book_at(reply.channels(0).updates(0).book().bids(), 0) == price5); - REQUIRE(book_at(reply.channels(0).updates(0).book().asks(), 0) == price5); + REQUIRE(reply.channels(1).updates().size() == 0); + REQUIRE(reply.channels(0).updates(0).has_direct()); + REQUIRE(book_at(reply.channels(0).updates(0).direct().bids(), 0) == price5); + REQUIRE(book_at(reply.channels(0).updates(0).direct().asks(), 0) == price5); // NaN out all the prices - expect to get a full set of updates for(auto p : producers) { - p.update_bbo(TimeStamp::now(), PriceSize::NO_MARKET, PriceSize::NO_MARKET, 2); + p.update_bbo(TimeStamp::now(), NO_MARKET, NO_MARKET, 2); } request.mutable_channels(0)->set_time(reply.channels(0).channel().time()); reply.Clear(); server->query(request, &reply); reply = simulate_wire_transfer(reply); - REQUIRE(reply.channels().size() == 1); - REQUIRE((size_t)reply.channels(0).updates().size() == producers.size()); - for (auto& update : reply.channels(0).updates()) { - REQUIRE(update.has_book()); - // We need to have a book to signal that it was updated but it can have no rows. This signifies an empty book and will be written as such by the relay client. - REQUIRE(book_at(update.book().bids(), 0) == PriceSize::NO_MARKET); - REQUIRE(book_at(update.book().asks(), 0) == PriceSize::NO_MARKET); + REQUIRE(reply.channels().size() == 2); + REQUIRE((size_t)reply.channels(0).updates().size() == producers.size()/2); + REQUIRE((size_t)reply.channels(1).updates().size() == producers.size()/2); + for (auto& channel : reply.channels()) { + for (auto& update : channel.updates()) { + REQUIRE(update.has_direct()); + // We need to have a book to signal that it was updated but it can have no rows. This signifies an empty book and will be written as such by the relay client. + REQUIRE(book_at(update.direct().bids(), 0) == NO_MARKET); + REQUIRE(book_at(update.direct().asks(), 0) == NO_MARKET); + } } // Fill up the markets again @@ -1369,33 +1335,187 @@ struct Co2RelayTests { p.update_bbo(TimeStamp::now(), price35, price35, 3); } request.mutable_channels(0)->set_time(reply.channels(0).channel().time()); + request.mutable_channels(1)->set_time(reply.channels(1).channel().time()); reply.Clear(); server->query(request, &reply); reply = simulate_wire_transfer(reply); - REQUIRE(reply.channels().size() == 1); - REQUIRE((size_t)reply.channels(0).updates().size() == producers.size()); - for (auto& update : reply.channels(0).updates()) { - REQUIRE(update.has_book()); - REQUIRE(update.book().bids_size() > 0); - REQUIRE(update.book().asks_size() > 0); - REQUIRE(book_at(update.book().bids(), 0) == price35); - REQUIRE(book_at(update.book().asks(), 0) == price35); + REQUIRE(reply.channels().size() == 2); + REQUIRE((size_t)reply.channels(0).updates().size() == producers.size()/2); + REQUIRE((size_t)reply.channels(1).updates().size() == producers.size()/2); + for (auto& channel : reply.channels()) { + for (auto& update : channel.updates()) { + REQUIRE(update.has_direct()); + REQUIRE(update.direct().bids_size() > 0); + REQUIRE(update.direct().asks_size() > 0); + REQUIRE(book_at(update.direct().bids(), 0) == price35); + REQUIRE(book_at(update.direct().asks(), 0) == price35); + } } // NaN out the bids for(auto p : producers) { - p.update_bbo(TimeStamp::now(), PriceSize::NO_MARKET, price35, 4); + p.update_bbo(TimeStamp::now(), NO_MARKET, price35, 4); } request.mutable_channels(0)->set_time(reply.channels(0).channel().time()); + request.mutable_channels(1)->set_time(reply.channels(1).channel().time()); reply.Clear(); server->query(request, &reply); reply = simulate_wire_transfer(reply); - REQUIRE(reply.channels().size() == 1); - REQUIRE((size_t)reply.channels(0).updates().size() == producers.size()); - for (auto& update : reply.channels(0).updates()) { - REQUIRE(update.has_book()); - REQUIRE(book_at(update.book().bids(), 0) == PriceSize::NO_MARKET); - REQUIRE(book_at(update.book().asks(), 0) == price35); + REQUIRE(reply.channels().size() == 2); + REQUIRE((size_t)reply.channels(0).updates().size() == producers.size()/2); + REQUIRE((size_t)reply.channels(1).updates().size() == producers.size()/2); + for (auto& channel : reply.channels()) { + for (auto& update : channel.updates()) { + REQUIRE(update.has_direct()); + REQUIRE(book_at(update.direct().bids(), 0) == NO_MARKET); + REQUIRE(book_at(update.direct().asks(), 0) == price35); + } + } + } + + void relay_implied() { + MDProducer producer(TEST_PLATFORM); + + // initialize data + vector producers; + auto server = setup_query_server(producer, &producers); + + // assign prices to all levels + auto set_direct = [&](PriceSize bid, PriceSize ask) { + for(auto p : producers) { + std::array bids, asks; + for (auto& p : bids) p = bid; + for (auto& p : asks) p = ask; + p.update_direct(TimeStamp::now(), bids, asks); + } + }; + auto set_implied = [&](PriceSize bid, PriceSize ask) { + for(auto p : producers) { + std::array implied_bids, implied_asks; + for (auto& p : implied_bids) p = bid; + for (auto& p : implied_asks) p = ask; + p.update_implied(TimeStamp::now(), implied_bids, implied_asks); + } + }; + PriceSize price35 = PriceSize_from_double<7>(3.5, 5); + PriceSize price5 = PriceSize_from_double<7>(5, 5); + set_direct(price35, price5); + set_implied(price35, price5); + + // Form the request + co2::MDRequest request; + auto* chan = request.add_channels(); + chan->set_type(SECURITYTYPE_FUTURE); + chan->set_symbol("FOO"); + chan->set_exchange(VENUE_BTS); + chan->set_flags(co2::MDRequest::DIRECT | co2::MDRequest::IMPLIED); + + // Send the request + co2::MDReply reply; + server->query(request, &reply); + reply = simulate_wire_transfer(reply); + + // check price levels + REQUIRE(reply.channels_size() > 0); + for (auto& channel : reply.channels()) { + for (auto& update : channel.updates()) { + REQUIRE(update.has_direct()); + REQUIRE(update.has_implied()); + REQUIRE(update.direct().bids_size() == COCONUT_MAX_DEPTH); + REQUIRE(update.direct().asks_size() == COCONUT_MAX_DEPTH); + REQUIRE(update.implied().bids_size() == COCONUT_MAX_IMPLIED_DEPTH); + REQUIRE(update.implied().asks_size() == COCONUT_MAX_IMPLIED_DEPTH); + for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { + REQUIRE(book_at(update.direct().bids(), x) == price35); + REQUIRE(book_at(update.direct().asks(), x) == price5); + } + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(book_at(update.implied().bids(), x) == price35); + REQUIRE(book_at(update.implied().asks(), x) == price5); + } + } + } + + // + // Nan out implied + request.mutable_channels(0)->set_time(reply.channels(0).channel().time()); + set_implied(NO_MARKET, NO_MARKET); + reply.Clear(); + server->query(request, &reply); + reply = simulate_wire_transfer(reply); + + for (auto& channel : reply.channels()) { + for (auto& update : channel.updates()) { + REQUIRE(update.has_direct()); + REQUIRE(update.has_implied()); + REQUIRE(update.direct().bids_size() == COCONUT_MAX_DEPTH); + REQUIRE(update.direct().asks_size() == COCONUT_MAX_DEPTH); + REQUIRE(update.implied().bids_size() == 0); + REQUIRE(update.implied().asks_size() == 0); + for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { + REQUIRE(book_at(update.direct().bids(), x) == price35); + REQUIRE(book_at(update.direct().asks(), x) == price5); + } + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(book_at(update.implied().bids(), x) == NO_MARKET); + REQUIRE(book_at(update.implied().asks(), x) == NO_MARKET); + } + } + } + + // + // Nan out direct + request.mutable_channels(0)->set_time(reply.channels(0).channel().time()); + set_direct(NO_MARKET, NO_MARKET); + reply.Clear(); + server->query(request, &reply); + reply = simulate_wire_transfer(reply); + + for (auto& channel : reply.channels()) { + for (auto& update : channel.updates()) { + REQUIRE(update.has_direct()); + REQUIRE(update.has_implied()); + REQUIRE(update.direct().bids_size() == 0); + REQUIRE(update.direct().asks_size() == 0); + REQUIRE(update.implied().bids_size() == 0); + REQUIRE(update.implied().asks_size() == 0); + for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { + REQUIRE(book_at(update.direct().bids(), x) == NO_MARKET); + REQUIRE(book_at(update.direct().asks(), x) == NO_MARKET); + } + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(book_at(update.implied().bids(), x) == NO_MARKET); + REQUIRE(book_at(update.implied().asks(), x) == NO_MARKET); + } + } + } + + // + // restore values + request.mutable_channels(0)->set_time(reply.channels(0).channel().time()); + set_direct(price5, price35); + set_implied(price5, price35); + reply.Clear(); + server->query(request, &reply); + reply = simulate_wire_transfer(reply); + + for (auto& channel : reply.channels()) { + for (auto& update : channel.updates()) { + REQUIRE(update.has_direct()); + REQUIRE(update.has_implied()); + REQUIRE(update.direct().bids_size() == COCONUT_MAX_DEPTH); + REQUIRE(update.direct().asks_size() == COCONUT_MAX_DEPTH); + REQUIRE(update.implied().bids_size() == COCONUT_MAX_IMPLIED_DEPTH); + REQUIRE(update.implied().asks_size() == COCONUT_MAX_IMPLIED_DEPTH); + for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { + REQUIRE(book_at(update.direct().bids(), x) == price5); + REQUIRE(book_at(update.direct().asks(), x) == price35); + } + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(book_at(update.implied().bids(), x) == price5); + REQUIRE(book_at(update.implied().asks(), x) == price35); + } + } } } } ; @@ -1408,13 +1528,753 @@ TEST_CASE ("Co2RelayTests","[md]") SECTION("relay_nans") { co2_relay_tests.relay_nans(); } + SECTION("relay_implied") { + co2_relay_tests.relay_implied(); + } } +namespace ImpliedTests { + void test_merge(const std::array& direct_bid, + const std::array& implied_bid, + const std::array& direct_ask, + const std::array& implied_ask, + const std::function&)>& validate) { + MDProducer producer(TEST_PLATFORM); + vector producers = create_test_securities(producer); + auto write_time = TimeStamp::now(); + + // update both direct and implied + for (auto& p : producers) { + p.update_direct(write_time, direct_bid, direct_ask, 0); + p.update_implied(write_time, implied_bid, implied_ask, 1); + } + // read from both + set symbols; + UserSymbol future_symbol( "FOO", VENUE_BTS, SECURITYTYPE_FUTURE ); + symbols.insert(future_symbol); + UserSymbol option_symbol( "FOOX", VENUE_BTS, SECURITYTYPE_OPTION ); + symbols.insert(option_symbol); + MDConsumer consumer(symbols, nullptr); + vector consumer_securities; + // Poll for security list - none visible until they have keys + size_t count = consumer.walk_security_list([&](const Security& security, MDConsumerHandle) {consumer_securities.push_back(security);}); + // Since no security has an id none are "public" + REQUIRE(consumer_securities.size() == count); + REQUIRE(count == 0); + assign_ids(producers.begin(), producers.end()); + // Now securities can be enumerated by the client + count = consumer.walk_security_list([&](const Security& security, MDConsumerHandle) {consumer_securities.push_back(security);}); + REQUIRE(consumer_securities.size() == count); + REQUIRE(count == 24); + vector consumers; + for(const auto& security : consumer_securities) { + consumers.push_back(consumer.get_handle(security.id())); + } -TEST_CASE("Co2SyntheticsTests", "[md]") { - // test synthetics show up with other securities - // test synehtetics with legs of synthetics - // test adding a synthetic with synthetic legs later in the day + validate(consumers); + } +} + +TEST_CASE("MergeImpliedTests", "[md]") { + config::internal::initialize_for_test( + "PlatformMap {" + " value {" + " glbx_platform: 'PLATFORM_SIM'" + " }" + "}"); + // create a handle with implied and direct + std::array direct_bid, direct_ask; + std::array implied_bid, implied_ask; + + SECTION("equal_impiled_deep") { + for (size_t x = 0; x < direct_bid.size(); ++x) { + direct_bid[x] = PriceSize_from_double<7>(x * 100, 2); + if (x < implied_bid.size()) { + implied_bid[x] = PriceSize_from_double<7>(x * 100, 1); + } + } + direct_ask = direct_bid; + implied_ask = implied_bid; + ImpliedTests::test_merge(direct_bid,implied_bid, direct_ask, implied_ask, [](const vector& consumers) { + for (const auto& c : consumers) { + std::array bids, asks; + TimeStamp read_time; + // get_book returns true becuase this time is after the change + REQUIRE(c.get_direct_book(read_time, COCONUT_MAX_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { + REQUIRE(bids[x].size() == 2); + REQUIRE(bids[x].direct_size() == 2); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].size() == 2); + REQUIRE(asks[x].direct_size() == 2); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_implied_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 1); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 1); + } + read_time = TimeStamp(); + REQUIRE(c.get_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_DEPTH; ++x) { + if (x < COCONUT_MAX_IMPLIED_DEPTH) { + REQUIRE(bids[x].size() == 3); + REQUIRE(asks[x].size() == 3); + + REQUIRE(bids[x].direct_size() == 2); + REQUIRE(bids[x].implied_size() == 1); + REQUIRE(asks[x].direct_size() == 2); + REQUIRE(asks[x].implied_size() == 1); + } + else { + REQUIRE(bids[x].size() == 2); + REQUIRE(asks[x].size() == 2); + + REQUIRE(bids[x].direct_size() == 2); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 2); + REQUIRE(asks[x].implied_size() == 0); + } + } + } + }); + } + + SECTION("partial_Read") { + direct_bid[0] = { {700, 1}, 1 }; + direct_bid[1] = { {600, 1}, 1 }; + direct_bid[2] = { {500, 1}, 1 }; + direct_bid[3] = { {400, 1}, 1 }; + + direct_ask[0] = { {800, 1}, 1 }; + direct_ask[1] = { {900, 1}, 1 }; + direct_ask[2] = { {1000, 1}, 1 }; + direct_ask[3] = { {1100, 1}, 1 }; + + implied_bid[0] = { {700, 1}, 2 }; + implied_bid[1] = { {600, 1}, 2 }; + + implied_ask[0] = { {800, 1}, 2 }; + implied_ask[1] = { {900, 1}, 2 }; + + + ImpliedTests::test_merge(direct_bid,implied_bid,direct_ask,implied_ask, [](const vector& consumers) { + const size_t TEST_DEPTH = 4; + for (const auto& c : consumers) { + std::array bids, asks; + int canary = 0; + TimeStamp read_time; + // get_boo returns true becuase this time is after the change + REQUIRE(c.get_direct_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < TEST_DEPTH; ++x) { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_implied_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(bids[x].size() == 2); + REQUIRE(asks[x].size() == 2); + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 2); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 2); + } + read_time = TimeStamp(); + REQUIRE(c.get_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < TEST_DEPTH; ++x) { + if (x < COCONUT_MAX_IMPLIED_DEPTH) { + REQUIRE(bids[x].size() == 3); + REQUIRE(asks[x].size() == 3); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 2); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 2); + } + else { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 0); + } + } + REQUIRE(canary == 0); + } + }); + } + + SECTION("better_implied") { + direct_bid[0] = { {500, 1}, 1 }; + direct_bid[1] = { {400, 1}, 1 }; + direct_bid[2] = { {300, 1}, 1 }; + direct_bid[3] = { {200, 1}, 1 }; + + direct_ask[0] = { {1000, 1}, 1 }; + direct_ask[1] = { {1100, 1}, 1 }; + direct_ask[2] = { {1200, 1}, 1 }; + direct_ask[3] = { {1300, 1}, 1 }; + + implied_bid[0] = { {600, 1}, 2 }; + implied_bid[1] = { {500, 1}, 2 }; + + implied_ask[0] = { {900, 1}, 2 }; + implied_ask[1] = { {1000, 1}, 2 }; + + + ImpliedTests::test_merge(direct_bid,implied_bid,direct_ask,implied_ask, [=](const vector& consumers) { + const size_t TEST_DEPTH = 4; + for (const auto& c : consumers) { + std::array bids, asks; + int canary = 0; + TimeStamp read_time; + // get_boo returns true becuase this time is after the change + REQUIRE(c.get_direct_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < TEST_DEPTH; ++x) { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_implied_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(bids[x].size() == 2); + REQUIRE(asks[x].size() == 2); + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 2); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 2); + } + read_time = TimeStamp(); + REQUIRE(c.get_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + REQUIRE(bids[0].size() == 2); + REQUIRE(bids[0].mantissa() == 600); + REQUIRE(asks[0].size() == 2); + REQUIRE(asks[0].mantissa() == 900); + REQUIRE(bids[1].size() == 3); + REQUIRE(asks[1].size() == 3); + REQUIRE(bids[1].direct_size() == 1); + REQUIRE(bids[1].implied_size() == 2); + REQUIRE(asks[1].direct_size() == 1); + REQUIRE(asks[1].implied_size() == 2); + for (size_t x = 2; x < TEST_DEPTH; ++x) { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 0); + } + REQUIRE(canary == 0); + } + }); + } + + SECTION("worse_implied") { + direct_bid[0] = { {500, 1}, 1 }; + direct_bid[1] = { {400, 1}, 1 }; + direct_bid[2] = { {300, 1}, 1 }; + direct_bid[3] = { {200, 1}, 1 }; + + direct_ask[0] = { {1000, 1}, 1 }; + direct_ask[1] = { {1100, 1}, 1 }; + direct_ask[2] = { {1200, 1}, 1 }; + direct_ask[3] = { {1300, 1}, 1 }; + + implied_bid[0] = { {400, 1}, 2 }; + implied_bid[1] = { {300, 1}, 2 }; + + implied_ask[0] = { {1100, 1}, 2 }; + implied_ask[1] = { {1200, 1}, 2 }; + + + ImpliedTests::test_merge(direct_bid,implied_bid,direct_ask,implied_ask, [=](const vector& consumers) { + const size_t TEST_DEPTH = 4; + for (const auto& c : consumers) { + std::array bids, asks; + int canary = 0; + TimeStamp read_time; + // get_boo returns true becuase this time is after the change + REQUIRE(c.get_direct_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < TEST_DEPTH; ++x) { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_implied_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(bids[x].size() == 2); + REQUIRE(asks[x].size() == 2); + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 2); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 2); + } + read_time = TimeStamp(); + REQUIRE(c.get_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + REQUIRE(bids[0].mantissa() == 500); + REQUIRE(bids[0].size() == 1); + REQUIRE(asks[0].mantissa() == 1000); + REQUIRE(asks[0].size() == 1); + REQUIRE(bids[0].direct_size() == 1); + REQUIRE(bids[0].implied_size() == 0); + REQUIRE(asks[0].direct_size() == 1); + REQUIRE(asks[0].implied_size() == 0); + + REQUIRE(bids[1].mantissa() == 400); + REQUIRE(bids[1].size() == 3); + REQUIRE(asks[1].mantissa() == 1100); + REQUIRE(asks[1].size() == 3); + REQUIRE(bids[1].direct_size() == 1); + REQUIRE(bids[1].implied_size() == 2); + REQUIRE(asks[1].direct_size() == 1); + REQUIRE(asks[1].implied_size() == 2); + + REQUIRE(bids[2].mantissa() == 300); + REQUIRE(bids[2].size() == 3); + REQUIRE(asks[2].mantissa() == 1200); + REQUIRE(asks[2].size() == 3); + REQUIRE(bids[2].direct_size() == 1); + REQUIRE(bids[2].implied_size() == 2); + REQUIRE(asks[2].direct_size() == 1); + REQUIRE(asks[2].implied_size() == 2); + + REQUIRE(bids[3].mantissa() == 200); + REQUIRE(bids[3].size() == 1); + REQUIRE(asks[3].mantissa() == 1300); + REQUIRE(asks[3].size() == 1); + REQUIRE(bids[3].direct_size() == 1); + REQUIRE(bids[3].implied_size() == 0); + REQUIRE(asks[3].direct_size() == 1); + REQUIRE(asks[3].implied_size() == 0); + + REQUIRE(canary == 0); + } + }); + } + + SECTION("worse_implied_bid") { + direct_bid[0] = { {500, 1}, 1 }; + direct_bid[1] = { {400, 1}, 1 }; + direct_bid[2] = { {300, 1}, 1 }; + direct_bid[3] = { {200, 1}, 1 }; + + direct_ask[0] = { {1000, 1}, 1 }; + direct_ask[1] = { {1100, 1}, 1 }; + direct_ask[2] = { {1200, 1}, 1 }; + direct_ask[3] = { {1300, 1}, 1 }; + + implied_bid[0] = { {400, 1}, 2 }; + implied_bid[1] = { {300, 1}, 2 }; + + implied_ask[0] = { {1000, 1}, 2 }; + implied_ask[1] = { {1100, 1}, 2 }; + + + ImpliedTests::test_merge(direct_bid,implied_bid,direct_ask,implied_ask, [=](const vector& consumers) { + const size_t TEST_DEPTH = 4; + for (const auto& c : consumers) { + std::array bids, asks; + int canary = 0; + TimeStamp read_time; + // get_boo returns true becuase this time is after the change + REQUIRE(c.get_direct_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < TEST_DEPTH; ++x) { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_implied_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(bids[x].size() == 2); + REQUIRE(asks[x].size() == 2); + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 2); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 2); + } + read_time = TimeStamp(); + REQUIRE(c.get_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + REQUIRE(bids[0].mantissa() == 500); + REQUIRE(bids[0].size() == 1); + REQUIRE(asks[0].mantissa() == 1000); + REQUIRE(asks[0].size() == 3); + REQUIRE(bids[0].direct_size() == 1); + REQUIRE(bids[0].implied_size() == 0); + REQUIRE(asks[0].direct_size() == 1); + REQUIRE(asks[0].implied_size() == 2); + + REQUIRE(bids[1].mantissa() == 400); + REQUIRE(bids[1].size() == 3); + REQUIRE(asks[1].mantissa() == 1100); + REQUIRE(asks[1].size() == 3); + REQUIRE(bids[1].direct_size() == 1); + REQUIRE(bids[1].implied_size() == 2); + REQUIRE(asks[1].direct_size() == 1); + REQUIRE(asks[1].implied_size() == 2); + + REQUIRE(bids[2].mantissa() == 300); + REQUIRE(bids[2].size() == 3); + REQUIRE(asks[2].mantissa() == 1200); + REQUIRE(asks[2].size() == 1); + REQUIRE(bids[2].direct_size() == 1); + REQUIRE(bids[2].implied_size() == 2); + REQUIRE(asks[2].direct_size() == 1); + REQUIRE(asks[2].implied_size() == 0); + + REQUIRE(bids[3].mantissa() == 200); + REQUIRE(bids[3].size() == 1); + REQUIRE(asks[3].mantissa() == 1300); + REQUIRE(asks[3].size() == 1); + REQUIRE(bids[3].direct_size() == 1); + REQUIRE(bids[3].implied_size() == 0); + REQUIRE(asks[3].direct_size() == 1); + REQUIRE(asks[3].implied_size() == 0); + + REQUIRE(canary == 0); + } + }); + } + SECTION("worse_implied_ask") { + direct_bid[0] = { {500, 1}, 1 }; + direct_bid[1] = { {400, 1}, 1 }; + direct_bid[2] = { {300, 1}, 1 }; + direct_bid[3] = { {200, 1}, 1 }; + + direct_ask[0] = { {1000, 1}, 1 }; + direct_ask[1] = { {1100, 1}, 1 }; + direct_ask[2] = { {1200, 1}, 1 }; + direct_ask[3] = { {1300, 1}, 1 }; + + implied_bid[0] = { {500, 1}, 2 }; + implied_bid[1] = { {400, 1}, 2 }; + + implied_ask[0] = { {1300, 1}, 2 }; + implied_ask[1] = { {1400, 1}, 2 }; + + + ImpliedTests::test_merge(direct_bid,implied_bid,direct_ask,implied_ask, [=](const vector& consumers) { + const size_t TEST_DEPTH = 4; + for (const auto& c : consumers) { + std::array bids, asks; + int canary = 0; + TimeStamp read_time; + // get_boo returns true becuase this time is after the change + REQUIRE(c.get_direct_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < TEST_DEPTH; ++x) { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_implied_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(bids[x].size() == 2); + REQUIRE(asks[x].size() == 2); + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 2); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 2); + } + read_time = TimeStamp(); + REQUIRE(c.get_book(read_time, TEST_DEPTH+1, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + REQUIRE(bids[0].mantissa() == 500); + REQUIRE(bids[0].size() == 3); + REQUIRE(asks[0].mantissa() == 1000); + REQUIRE(asks[0].size() == 1); + REQUIRE(bids[0].direct_size() == 1); + REQUIRE(bids[0].implied_size() == 2); + REQUIRE(asks[0].direct_size() == 1); + REQUIRE(asks[0].implied_size() == 0); + + REQUIRE(bids[1].mantissa() == 400); + REQUIRE(bids[1].size() == 3); + REQUIRE(asks[1].mantissa() == 1100); + REQUIRE(asks[1].size() == 1); + REQUIRE(bids[1].direct_size() == 1); + REQUIRE(bids[1].implied_size() == 2); + REQUIRE(asks[1].direct_size() == 1); + REQUIRE(asks[1].implied_size() == 0); + + REQUIRE(bids[2].mantissa() == 300); + REQUIRE(bids[2].size() == 1); + REQUIRE(asks[2].mantissa() == 1200); + REQUIRE(asks[2].size() == 1); + REQUIRE(bids[2].direct_size() == 1); + REQUIRE(bids[2].implied_size() == 0); + REQUIRE(asks[2].direct_size() == 1); + REQUIRE(asks[2].implied_size() == 0); + + REQUIRE(bids[3].mantissa() == 200); + REQUIRE(bids[3].size() == 1); + REQUIRE(asks[3].mantissa() == 1300); + REQUIRE(asks[3].size() == 3); + REQUIRE(bids[3].direct_size() == 1); + REQUIRE(bids[3].implied_size() == 0); + REQUIRE(asks[3].direct_size() == 1); + REQUIRE(asks[3].implied_size() == 2); + + REQUIRE(bids[4].mantissa() == 0); + REQUIRE(bids[4].size() == 0); + REQUIRE(asks[4].mantissa() == 1400); + REQUIRE(asks[4].size() == 2); + REQUIRE(bids[4].direct_size() == 0); + REQUIRE(bids[4].implied_size() == 0); + REQUIRE(asks[4].direct_size() == 0); + REQUIRE(asks[4].implied_size() == 2); + + REQUIRE(canary == 0); + } + }); + } + + SECTION("nan_implied") { + direct_bid[0] = { {500, 1}, 1 }; + direct_bid[1] = { {400, 1}, 1 }; + direct_bid[2] = { {300, 1}, 1 }; + direct_bid[3] = { {200, 1}, 1 }; + + direct_ask[0] = { {1000, 1}, 1 }; + direct_ask[1] = { {1100, 1}, 1 }; + direct_ask[2] = { {1200, 1}, 1 }; + direct_ask[3] = { {1300, 1}, 1 }; + + implied_bid[0] = { INT_NAN, 0 }; + implied_bid[1] = { INT_NAN, 0 }; + + implied_ask[0] = { INT_NAN, 0 }; + implied_ask[1] = { INT_NAN, 0 }; + + + ImpliedTests::test_merge(direct_bid,implied_bid,direct_ask,implied_ask, [=](const vector& consumers) { + const size_t TEST_DEPTH = 4; + for (const auto& c : consumers) { + std::array bids, asks; + int canary = 0; + TimeStamp read_time; + // get_boo returns true becuase this time is after the change + REQUIRE(c.get_direct_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < TEST_DEPTH; ++x) { + REQUIRE(bids[x].size() == 1); + REQUIRE(asks[x].size() == 1); + REQUIRE(bids[x].direct_size() == 1); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 1); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_implied_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(bids[x].size() == 0); + REQUIRE(asks[x].size() == 0); + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + REQUIRE(bids[0].mantissa() == 500); + REQUIRE(bids[0].size() == 1); + REQUIRE(asks[0].mantissa() == 1000); + REQUIRE(asks[0].size() == 1); + REQUIRE(bids[0].direct_size() == 1); + REQUIRE(bids[0].implied_size() == 0); + REQUIRE(asks[0].direct_size() == 1); + REQUIRE(asks[0].implied_size() == 0); + + REQUIRE(bids[1].mantissa() == 400); + REQUIRE(bids[1].size() == 1); + REQUIRE(asks[1].mantissa() == 1100); + REQUIRE(asks[1].size() == 1); + REQUIRE(bids[1].direct_size() == 1); + REQUIRE(bids[1].implied_size() == 0); + REQUIRE(asks[1].direct_size() == 1); + REQUIRE(asks[1].implied_size() == 0); + + REQUIRE(bids[2].mantissa() == 300); + REQUIRE(bids[2].size() == 1); + REQUIRE(asks[2].mantissa() == 1200); + REQUIRE(asks[2].size() == 1); + REQUIRE(bids[2].direct_size() == 1); + REQUIRE(bids[2].implied_size() == 0); + REQUIRE(asks[2].direct_size() == 1); + REQUIRE(asks[2].implied_size() == 0); + + REQUIRE(bids[3].mantissa() == 200); + REQUIRE(bids[3].size() == 1); + REQUIRE(asks[3].mantissa() == 1300); + REQUIRE(asks[3].size() == 1); + REQUIRE(bids[3].direct_size() == 1); + REQUIRE(bids[3].implied_size() == 0); + REQUIRE(asks[3].direct_size() == 1); + REQUIRE(asks[3].implied_size() == 0); + + REQUIRE(canary == 0); + } + }); + } + SECTION("nan_direct") { + direct_bid[0] = { INT_NAN, 0 }; + direct_bid[1] = { INT_NAN, 0 }; + direct_bid[2] = { INT_NAN, 0 }; + direct_bid[3] = { INT_NAN, 0 }; + + direct_ask[0] = { INT_NAN, 0 }; + direct_ask[1] = { INT_NAN, 0 }; + direct_ask[2] = { INT_NAN, 0 }; + direct_ask[3] = { INT_NAN, 0 }; + + implied_bid[0] = { {500, 1}, 2 }; + implied_bid[1] = { {400, 1}, 2 }; + + implied_ask[0] = { {1000, 1}, 2 }; + implied_ask[1] = { {1100, 1}, 2 }; + + + ImpliedTests::test_merge(direct_bid,implied_bid,direct_ask,implied_ask, [=](const vector& consumers) { + const size_t TEST_DEPTH = 4; + for (const auto& c : consumers) { + std::array bids, asks; + int canary = 0; + TimeStamp read_time; + // get_boo returns true becuase this time is after the change + REQUIRE(c.get_direct_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < TEST_DEPTH; ++x) { + REQUIRE(bids[x].size() == 0); + REQUIRE(asks[x].size() == 0); + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 0); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 0); + } + read_time = TimeStamp(); + REQUIRE(c.get_implied_book(read_time, COCONUT_MAX_IMPLIED_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + for (size_t x = 0; x < COCONUT_MAX_IMPLIED_DEPTH; ++x) { + REQUIRE(bids[x].size() == 2); + REQUIRE(asks[x].size() == 2); + REQUIRE(bids[x].direct_size() == 0); + REQUIRE(bids[x].implied_size() == 2); + REQUIRE(asks[x].direct_size() == 0); + REQUIRE(asks[x].implied_size() == 2); + } + read_time = TimeStamp(); + REQUIRE(c.get_book(read_time, TEST_DEPTH, &bids[0], &asks[0]) == HandleReadResult::SUCCESS); + REQUIRE(bids[0].mantissa() == 500); + REQUIRE(bids[0].size() == 2); + REQUIRE(asks[0].mantissa() == 1000); + REQUIRE(asks[0].size() == 2); + REQUIRE(bids[0].direct_size() == 0); + REQUIRE(bids[0].implied_size() == 2); + REQUIRE(asks[0].direct_size() == 0); + REQUIRE(asks[0].implied_size() == 2); + + REQUIRE(bids[1].mantissa() == 400); + REQUIRE(bids[1].size() == 2); + REQUIRE(asks[1].mantissa() == 1100); + REQUIRE(asks[1].size() == 2); + REQUIRE(bids[1].direct_size() == 0); + REQUIRE(bids[1].implied_size() == 2); + REQUIRE(asks[1].direct_size() == 0); + REQUIRE(asks[1].implied_size() == 2); + + REQUIRE(bids[2].size() == 0); + REQUIRE(asks[2].size() == 0); + REQUIRE(bids[2].direct_size() == 0); + REQUIRE(bids[2].implied_size() == 0); + REQUIRE(asks[2].direct_size() == 0); + REQUIRE(asks[2].implied_size() == 0); + + REQUIRE(bids[3].size() == 0); + REQUIRE(asks[3].size() == 0); + REQUIRE(bids[3].direct_size() == 0); + REQUIRE(bids[3].implied_size() == 0); + REQUIRE(asks[3].direct_size() == 0); + REQUIRE(asks[3].implied_size() == 0); + + REQUIRE(canary == 0); + } + }); + } +} + +TEST_CASE("IntPrice", "[md]") { + SECTION("< equal exponent") { + auto a = IntPrice_from_double<4>(101.111); + auto b = IntPrice_from_double<4>(100.111); + REQUIRE((int)a.exponent() == (int)b.exponent()); + REQUIRE(b < a); + REQUIRE(!(a < b)); + } + + SECTION("< different exponent") { + auto a = IntPrice_from_double<4>(101.111); // -4 + auto b = IntPrice_from_double<0>(100); // 0 + REQUIRE((int)a.exponent() < (int)b.exponent()); + REQUIRE(b < a); + REQUIRE(!(a < b)); + auto c = std::max(a,b); + REQUIRE(c == a); + } + + // subtract with equal exponent + SECTION("subtract equal exponent") { + IntPrice a = IntPrice_from_double<4>(100.333); + IntPrice b = IntPrice_from_double<4>(100.222); + IntPrice delta = a - b; + REQUIRE(delta == IntPrice_from_double<4>(0.111)); + } + + // subtract smaller from greater + SECTION("subtract smaller from greater") { + IntPrice a = IntPrice_from_double<4>(100.333); + IntPrice b = IntPrice_from_double<5>(100.222); + IntPrice delta = a - b; + REQUIRE(delta == IntPrice_from_double<5>(0.111)); + } + // subtract greater from smaller + SECTION("subtract greater from smaller") { + IntPrice a = IntPrice_from_double<4>(100.333); + IntPrice b = IntPrice_from_double<3>(100.111); + IntPrice delta = a - b; + REQUIRE(delta == IntPrice_from_double<4>(0.222)); + } + + SECTION("subtract NAN from something") { + IntPrice a = IntPrice_from_double<4>(100.333); + IntPrice delta = a - INT_NAN; + REQUIRE(delta == INT_NAN); + } + SECTION("subtract something from NAN") { + IntPrice a = IntPrice_from_double<4>(100.333); + IntPrice delta = INT_NAN - a; + REQUIRE(delta == INT_NAN); + } } }}} diff --git a/src/lib/md/test/synthetic_book_merge_tests.cpp b/src/lib/md/test/synthetic_book_merge_tests.cpp index 8616446a9..e6067a64e 100644 --- a/src/lib/md/test/synthetic_book_merge_tests.cpp +++ b/src/lib/md/test/synthetic_book_merge_tests.cpp @@ -1,3 +1,4 @@ +#ifndef _WIN32 #include "catch/catch.hpp" #include "../MDConsumerHandle.h" #include "Co2TestInstrument.h" @@ -9,6 +10,10 @@ #include #include #include +#define ENABLE_BENCHMARK 0 +#if ENABLE_BENCHMARK +#include +#endif using namespace std; @@ -35,16 +40,16 @@ void setup_market(MDProducerHandle producer, const vector& bids, const vecto auto ts = TimeStamp::now(); std::array bids_a = { }, asks_a = { }; - std::transform(bids.begin(), bids.end(), bids_a.begin(), [](const PS& ps) { return PriceSize::from_double<7>(ps.price, ps.size); }); - std::transform(asks.begin(), asks.end(), asks_a.begin(), [](const PS& ps) { return PriceSize::from_double<7>(ps.price, ps.size); }); + std::transform(bids.begin(), bids.end(), bids_a.begin(), [](const PS& ps) { return PriceSize_from_double<7>(ps.price, ps.size); }); + std::transform(asks.begin(), asks.end(), asks_a.begin(), [](const PS& ps) { return PriceSize_from_double<7>(ps.price, ps.size); }); - producer.update_book(ts, bids_a, asks_a); + producer.update_direct(ts, bids_a, asks_a); } void expect_market(const MDConsumerHandle& handle, const vector bids, const vector& asks) { array raw_bids,raw_asks; TimeStamp t; - handle.get_depth(t, COCONUT_MAX_DEPTH, raw_bids.data(), raw_asks.data()); + handle.get_direct_book(t, COCONUT_MAX_DEPTH, raw_bids.data(), raw_asks.data()); vector src_bids, src_asks; sane::map(raw_bids, src_bids, [](const PriceSize& ps) -> PS {return {ps.price(), ps.size()};}); sane::map(raw_asks, src_asks, [](const PriceSize& ps) -> PS {return {ps.price(), ps.size()}; }); @@ -70,11 +75,15 @@ void expect_market(const MDConsumerHandle& handle, const vector bids, const tuple,vector> make_securities(const vector>& leg_descr) { vector legs; auto synthetic = make_shared(); + uint32_t security_key = 100; for (const auto& ldesc : leg_descr) { double quantity, tick_size; api::Side side; std::tie(quantity, tick_size, side) = ldesc; Co2TestInstrument instr("ES", api::SECURITYTYPE_FUTURE); + instr.producer().set_key(security_key); + ++security_key; + auto& s = synthetic->security; s.mutable_contract_spec()->set_low_tick(tick_size); s.mutable_contract_spec()->set_high_tick(tick_size); @@ -86,10 +95,16 @@ tuple,vector> make_securities(c l->set_side(side); synthetic->legs.push_back(instr.consumer()); legs.push_back(instr); + } return { synthetic, legs }; } +void set_leg_params(SyntheticStrategy& strat, uint security_key, const spark::LegSpreaderParams& params) { + auto* map = strat.strategy.mutable_leg_params_map(); + map->insert({ security_key, params }); +} + TEST_CASE("SyntheticBookMerge", "[md][synthetic]") { shared_ptr synthetic; vector test_instr; @@ -221,6 +236,93 @@ TEST_CASE("SPARK-955", "[md][synthetic]") { { {5.00, 3}, {5.25, 1}, {6.00,3}, {6.50,6} }); } +TEST_CASE("Synthetic_net_change", "[md][synthetic]") { + shared_ptr synthetic; + vector test_instr; + tie(synthetic, test_instr) = make_securities({ {1.0, 0.01, api::SIDE_BUY}, {2.0, 0.01, api::SIDE_SELL }}); + auto es_buy1 = test_instr[0]; + auto es_sell2 = test_instr[1]; + MDConsumerHandle synthetic_handle{synthetic}; + + // Set legs to use net change + spark::LegSpreaderParams lparam; + lparam.set_leg_md_calc(spark::LegSpreaderParams::NET_CHANGE); + set_leg_params(*synthetic, es_buy1.producer().key(), lparam); + set_leg_params(*synthetic, es_sell2.producer().key(), lparam); + + // set settlement prices + es_buy1.producer().update_settlement(TimeStamp::now(), { 100, 0 }, datetime::today()); + es_sell2.producer().update_settlement(TimeStamp::now(), { 50, 0 }, datetime::today()); + + SECTION("buy 1 sell 2 Net") { + setup_market(es_buy1.producer(), + {{190,1}, {180,11}, {170,4}, {160,10}}, + {{200,2}, {210,6}, {220,8}, {230,12}}); + setup_market(es_sell2.producer(), + {{90,5}, {80,9}, {70,11}, {60,10}, {50,1}}, + {{100,16}, {110,8}, {120,12}, {130,5}}); + + // Validate merge + expect_market(synthetic_handle, + {{-10,1}, {-20,7}, {-40,4}, {-70,4}, {-80,2}, {-100,2}}, + {{20,2}, {40,1}, {50,4}, {70,1}, {80,4}, {90,1}, {100,3}, {110,1}, {120,1}}); + } +} -}}}//namespace \ No newline at end of file +// +// Benchmarks +// +#if ENABLE_BENCHMARK +void synthetic_get_book_speed2(benchmark::State& state) { + shared_ptr synthetic; + vector test_instr; + tie(synthetic, test_instr) = make_securities({ {1.0, 0.25, api::SIDE_BUY}, {2.0, 0.25, api::SIDE_SELL } }); + auto esm8 = test_instr[0]; + auto esu8 = test_instr[1]; + MDConsumerHandle synthetic_handle(synthetic); + + setup_market(esm8.producer(), + {{2594.50, 47}, {2594.25,63}, {2594.00,117}, {2593.75,137}}, + {{2594.75, 52}, {2595.00,138}, {2594.25,119}, {2595.50,128}}); + setup_market(esu8.producer(), + {{2598.50, 14}, {2598.25, 15}, {2598.00,16}, {2597.75, 15}}, + {{2599.75, 1}, {2600.00, 16}, {2600.25,15}, {2600.50, 75}}); + + for (auto _ : state) { + array raw_bids,raw_asks; + TimeStamp t; + synthetic_handle.get_book(t, COCONUT_MAX_DEPTH, raw_bids.data(), raw_asks.data()); + } +} +BENCHMARK(synthetic_get_book_speed2); +void synthetic_get_book_speed3(benchmark::State& state) { + shared_ptr synthetic; + vector test_instr; + tie(synthetic, test_instr) = make_securities({ {1.0, 0.25, api::SIDE_BUY}, {2.0, 0.25, api::SIDE_SELL }, { 1.0, 0.25, api::SIDE_BUY}}); + auto esm8 = test_instr[0]; + auto esu8 = test_instr[1]; + auto esz8 = test_instr[2]; + MDConsumerHandle synthetic_handle(synthetic); + + setup_market(esm8.producer(), + {{2594.50, 47}, {2594.25,63}, {2594.00,117}, {2593.75,137}}, + {{2594.75, 52}, {2595.00,138}, {2594.25,119}, {2595.50,128}}); + setup_market(esu8.producer(), + {{2598.50, 14}, {2598.25, 15}, {2598.00,16}, {2597.75, 15}}, + {{2599.75, 1}, {2600.00, 16}, {2600.25,15}, {2600.50, 75}}); + setup_market(esz8.producer(), + {{2601.25, 20}, {2600.50, 3}, {2600.25, 100}}, + {{2607.25, 3}, {2607.50, 1}, {2608.25, 900}}); + + for (auto _ : state) { + array raw_bids,raw_asks; + TimeStamp t; + synthetic_handle.get_book(t, COCONUT_MAX_DEPTH, raw_bids.data(), raw_asks.data()); + } +} +BENCHMARK(synthetic_get_book_speed3); +#endif + +}}}//namespace +#endif \ No newline at end of file diff --git a/src/lib/orderman/actors/MDSource.h b/src/lib/orderman/actors/MDSource.h index 3b0336001..cbe55b940 100644 --- a/src/lib/orderman/actors/MDSource.h +++ b/src/lib/orderman/actors/MDSource.h @@ -82,7 +82,7 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor { for (auto &h : _handles) { auto &handle = h.handle; - if (handle.get_depth(h.last, std::tuple_size(), + if (handle.get_book(h.last, std::tuple_size(), &book.bid()[0], &book.ask()[0]) == md::HandleReadResult::SUCCESS) { book._security_key = SecurityKey(handle.key()); diff --git a/src/lib/orderman/actors/MDSupplementarySource.cpp b/src/lib/orderman/actors/MDSupplementarySource.cpp new file mode 100644 index 000000000..a9630b987 --- /dev/null +++ b/src/lib/orderman/actors/MDSupplementarySource.cpp @@ -0,0 +1,81 @@ +#include "MDSupplementarySource.h" +#include "lib/orderman/events/SettlementRequestEvent.h" +#include "lib/orderman/events/SettlementUpdateEvent.h" +#include "lib/logging/logging.h" +#include "lib/md/MDConsumerHandle.h" +#include "lib/md/MDConsumer.h" +#include "lib/md/MDConsumer.h" +#include "lib/os/os.h" + +namespace bts { +namespace orderman { + +struct HandleRecord { + md::MDConsumerHandle handle; + md::IntPrice settlement_price; +}; + +struct MDSupplementarySource::PImpl +{ + PImpl(md::MDConsumer& md_consumer) + : + md_consumer(md_consumer), + handle_map(), + settlement_update(){ + } + md::MDConsumer md_consumer; + // list of sec subscriptions vs their handles + std::map handle_map; + mercury::Stream settlement_update; +}; + +MDSupplementarySource::MDSupplementarySource(md::MDConsumer& md_consumer) +: +me(new PImpl(md_consumer)){ +} + +MDSupplementarySource::~MDSupplementarySource(){ +} + +void MDSupplementarySource::init(){ + me->settlement_update=make_stream(); + subscribe([this](auto const &update) { + if (update.events) { + for (auto const &ev : *update.events) { + auto k = ev.security_key.value(); + auto it = me->handle_map.find(k); + if(it ==me->handle_map.end()){ + LOG(OrderMan, Info, "MDSupplementarySource::init key:{}", k); + me->handle_map[k]={me->md_consumer.get_handle(k),md::INT_NAN}; + } + else { + // late joiners + auto const &hr = it->second; + if (!hr.settlement_price.is_nan()) { + SettlementUpdateEvent ev(hr.settlement_price,SecurityKey(k)); + publish(me->settlement_update,ev); + } + } + } + } + }); +} + +void MDSupplementarySource::on_poll(){ + for(auto& kvp : me->handle_map){ + SecurityKey key(kvp.first); + HandleRecord& hr=kvp.second; + auto settlement_price = hr.handle.settlement_price(); + if(!settlement_price.is_nan() && + settlement_price!=hr.settlement_price){ + LOG(OrderMan, Info, + "MDSupplementarySource::on_poll new settle key:{} price:{}", + key, hr.settlement_price.price()); + hr.settlement_price=settlement_price; + SettlementUpdateEvent ev(settlement_price,key); + publish(me->settlement_update,ev); + } + } +} + +}} diff --git a/src/lib/orderman/actors/MDSupplementarySource.h b/src/lib/orderman/actors/MDSupplementarySource.h new file mode 100644 index 000000000..4ea5fa261 --- /dev/null +++ b/src/lib/orderman/actors/MDSupplementarySource.h @@ -0,0 +1,32 @@ +#pragma once + +#include +#include "lib/mercury/actors/Actor.h" +#include "lib/mercury/actors/IPollableActor.h" + +namespace bts { + + +namespace md {class MDConsumer;} +namespace orderman { + +// provides non hot-path market data such as settles, daily high-low etc +// (only settles for now) +// like MDSource, watches for MDRequestEvent's and hence constructs handles +// for requested contracts only. these handles are then polled and settles are +// published if changed. +class MDSupplementarySource : public mercury::Actor, public mercury::IPollableActor +{ +public: + MDSupplementarySource(md::MDConsumer& md_consumer); + ~MDSupplementarySource(); + + void init() override; + void on_poll() override; +private: + struct PImpl; + std::unique_ptr me; +}; + +}} + diff --git a/src/lib/orderman/actors/order_manager/OrderManager.cpp b/src/lib/orderman/actors/order_manager/OrderManager.cpp index 181f2e2e7..176dd1246 100644 --- a/src/lib/orderman/actors/order_manager/OrderManager.cpp +++ b/src/lib/orderman/actors/order_manager/OrderManager.cpp @@ -370,6 +370,7 @@ void OrderManager::init_order_services() { me->services_data.create_fix_req_pub = make_stream(); me->services_data.md_request_pub = make_stream(); + me->services_data.settlement_request_pub= make_stream(); me->services_data.report_order_state_pub = make_stream(); me->pub_ready = make_stream(); @@ -382,8 +383,13 @@ void OrderManager::init_order_services() { std::function on_md_sub = [this](SecurityKey k) { publish(me->services_data.md_request_pub, k); }; + std::function on_settlement_sub = [this](SecurityKey k) { + publish(me->services_data.settlement_request_pub, k); + }; me->services_data.md_subs = std::make_unique>(*this, on_md_sub); + me->services_data.settlement_subs = + std::make_unique>(*this,on_settlement_sub); me->services_data.time_subs = std::make_unique>(*this); me->services_data.order_creation_subs = diff --git a/src/lib/orderman/actors/order_manager/OrderServices.h b/src/lib/orderman/actors/order_manager/OrderServices.h index 6fc014d54..4e50f804f 100644 --- a/src/lib/orderman/actors/order_manager/OrderServices.h +++ b/src/lib/orderman/actors/order_manager/OrderServices.h @@ -70,7 +70,9 @@ class SessionServices { class OrderServices { public: OrderServices(Order &owner, OrderServicesData &data) - : book_updates(*data.md_subs), child_updates(*data.child_order_subs), + : book_updates(*data.md_subs), + settlement_updates(*data.settlement_subs), + child_updates(*data.child_order_subs), time_change(*data.time_subs), order_creations(*data.order_creation_subs), _owner(owner), _data(data), _sessions() { @@ -79,6 +81,7 @@ class OrderServices { } EventStream book_updates; + EventStream settlement_updates; EventStream child_updates; EventStream time_change; diff --git a/src/lib/orderman/actors/order_manager/OrderServicesData.h b/src/lib/orderman/actors/order_manager/OrderServicesData.h index b16511ba0..b13513e63 100644 --- a/src/lib/orderman/actors/order_manager/OrderServicesData.h +++ b/src/lib/orderman/actors/order_manager/OrderServicesData.h @@ -11,6 +11,7 @@ #include "lib/mercury/events/TimerEvent.h" #include "lib/orderman/events/ActivateOrderEvent.h" #include "lib/orderman/events/BookUpdateEvent.h" +#include "lib/orderman/events/SettlementUpdateEvent.h" #include "lib/orderman/events/CancelOrderEvent.h" #include "lib/orderman/events/CancelRejectEvent.h" #include "lib/orderman/events/CrackOrderEvent.h" @@ -20,6 +21,7 @@ #include "lib/orderman/events/CreateOrderRequestEvent.h" #include "lib/orderman/events/GatewayStatusChangeEvent.h" #include "lib/orderman/events/MDRequestEvent.h" +#include "lib/orderman/events/SettlementRequestEvent.h" #include "lib/orderman/events/NewExecutionReportEvent.h" #include "lib/orderman/events/NewSecuritiesEvent.h" #include "lib/orderman/events/OrderStateUpdateEvent.h" @@ -75,6 +77,7 @@ class OrderServicesData { OrderFactory order_factory; std::unique_ptr> md_subs; + std::unique_ptr> settlement_subs; std::unique_ptr< mercury::ContentSubscription> child_order_subs; @@ -93,6 +96,7 @@ class OrderServicesData { mercury::Stream create_order_req_pub; mercury::Stream create_fix_req_pub; mercury::Stream md_request_pub; + mercury::Stream settlement_request_pub; mercury::Stream report_order_state_pub; mercury::TimerEvent last_timer_ev; diff --git a/src/lib/orderman/events/SettlementRequestEvent.h b/src/lib/orderman/events/SettlementRequestEvent.h new file mode 100644 index 000000000..2d895d0a1 --- /dev/null +++ b/src/lib/orderman/events/SettlementRequestEvent.h @@ -0,0 +1,24 @@ +#pragma once + +#include + +#include "lib/orderman/orderman_utils/orderman_common.h" +#include "lib/mercury/events/Event.h" + +namespace bts { +namespace orderman { + +class SettlementRequestEvent : public mercury::Event { +public: + using SynopsisPolicy = mercury::CacheAll; + SettlementRequestEvent(SecurityKey k) : security_key(k) {} + SettlementRequestEvent &operator=(SettlementRequestEvent const&rhs) + { + security_key = rhs.security_key; + return *this; + } + SecurityKey security_key; +}; + +}} + diff --git a/src/lib/orderman/events/SettlementUpdateEvent.h b/src/lib/orderman/events/SettlementUpdateEvent.h new file mode 100644 index 000000000..9211c081b --- /dev/null +++ b/src/lib/orderman/events/SettlementUpdateEvent.h @@ -0,0 +1,26 @@ +#pragma once + +#include + +#include "lib/mercury/events/Event.h" +#include "lib/orderman/orderman_utils/Book.h" +#include "lib/orderman/orderman_utils/orderman_common.h" +#include "lib/mercury/synopsis/MergeByKey.h" + +namespace bts { +namespace orderman { + +class SettlementUpdateEvent : public mercury::Event { +public: + using SynopsisPolicy = mercury::NoSynopsis; + md::IntPrice settlement_price; + SecurityKey security_key; + SecurityKey key() const { return security_key; } + + SettlementUpdateEvent(const md::IntPrice& price, + const SecurityKey& key) + : + settlement_price(price), + security_key(key) {} +}; +}} diff --git a/src/lib/orderman/orderman_utils/Book.h b/src/lib/orderman/orderman_utils/Book.h index 34ef074be..36b6ae386 100644 --- a/src/lib/orderman/orderman_utils/Book.h +++ b/src/lib/orderman/orderman_utils/Book.h @@ -18,6 +18,11 @@ public: _book() {} + Book (SecurityKey key) : + _security_key(key), + _book() + {} + Book (SecurityKey key, BookSide const&bid, BookSide const&ask) : diff --git a/src/lib/orderman/orderman_utils/orderman_utils.h b/src/lib/orderman/orderman_utils/orderman_utils.h new file mode 100644 index 000000000..32194c4c3 --- /dev/null +++ b/src/lib/orderman/orderman_utils/orderman_utils.h @@ -0,0 +1,30 @@ +#include "lib/api/services/OrderService.pb.h" + +namespace bts{ +namespace orderman{ + +using namespace bts::api::orderman; + +inline bool order_in_error_state(OrderStatus order_status){ + return order_status == OrderStatus::ORDER_STATUS_ERROR || + order_status == OrderStatus::ORDER_STATUS_DISCONNECTED || + order_status == OrderStatus::ORDER_STATUS_REJECTED_BY_SPARK; +} + +inline bool order_in_working_state(OrderStatus order_status){ + return order_status == OrderStatus::ORDER_STATUS_WORKING || + order_status == OrderStatus::ORDER_STATUS_PENDING_WORKING; +} + +inline bool is_exchange_order(const OrderState& state){ + return state.has_market() || + state.has_limit() || + state.has_stop() || + state.has_stop_limit(); +} + +inline bool is_parent_order(const OrderRecord& ord){ + return ord.parent_key()==0; +} + +}} \ No newline at end of file diff --git a/src/lib/orderman/orders/spreader_orders/SpreaderOrder.cpp b/src/lib/orderman/orders/spreader_orders/SpreaderOrder.cpp index 41eeebf06..63e2c8fae 100644 --- a/src/lib/orderman/orders/spreader_orders/SpreaderOrder.cpp +++ b/src/lib/orderman/orders/spreader_orders/SpreaderOrder.cpp @@ -6,6 +6,7 @@ #include "lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h" #include "lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.h" #include "lib/orderman/orders/spreader_orders/spreader/SpreaderBookkeepingData.h" +#include "lib/orderman/events/SettlementUpdateEvent.h" #include "lib/tracing/bts_trace.h" #include "lib/utils/helper_macros.h" @@ -100,6 +101,21 @@ void SpreaderOrder::_on_book_update(const BookUpdateEvent& book_event){ trace.spreader_on_book = utils::TimeStamp::now(); _state_worker->on_book (book_event.book,trace); } +void SpreaderOrder::_on_settlement_update(const SettlementUpdateEvent& + settlement_event){ + if (!_active) return; + LOG(OrderMan, Info, "SpreaderOrder::_on_settlement_update " + "key:{} sec_key:{} state:{} settlement:{}", + key(), settlement_event.key(), + static_cast(_state_worker->state()), + settlement_event.settlement_price.price()); + // this is a key->index lookup + SpreaderLegData& md_leg = + _bookkeeping_data().get_leg_data(settlement_event.key()); + md_leg.update_settlement_price(settlement_event.settlement_price); + bts::PerfTrace trace; + _state_worker->on_book (md_leg.book(),trace); +} void SpreaderOrder::_on_child_order_update(const OrderStateUpdateEvent& ord_ev){ auto trace = ord_ev.trace; trace.spreader_on_child_order_update = utils::TimeStamp::now(); @@ -114,7 +130,7 @@ void SpreaderOrder::_on_child_order_update(const OrderStateUpdateEvent& ord_ev){ if (!SpreaderUtils::order_acknowledged(status)){ LOG(OrderMan, Diag, "SpreaderOrder::_on_child_order_update, unack'ed " "callback for key:{} sec_key:{} order_key:{} " - "status:{} ignoring", + "status:{} ignoring", key(), sec_key, order_key, status); bts_assert(filled_qty.value() == 0); return; diff --git a/src/lib/orderman/orders/spreader_orders/SpreaderOrder.h b/src/lib/orderman/orders/spreader_orders/SpreaderOrder.h index 55a819528..8be458b9d 100644 --- a/src/lib/orderman/orders/spreader_orders/SpreaderOrder.h +++ b/src/lib/orderman/orders/spreader_orders/SpreaderOrder.h @@ -24,6 +24,8 @@ namespace bts { namespace api {namespace orderman {class OrderState; class CreateSpreaderOrder;}} namespace orderman { +class SettlementUpdateEvent; + class SpreaderOrder : public Order, public ISpreaderManager { friend class SpreaderStartStateWorker; @@ -124,6 +126,7 @@ private: ///////////////////////////////////////////////// void _on_time (const bts::mercury::TimerEvent&); void _on_book_update(const BookUpdateEvent&); + void _on_settlement_update(const SettlementUpdateEvent&); void _on_child_order_update(const OrderStateUpdateEvent&); // State Transformations //////////////////// ///////////////////////////////////////////// diff --git a/src/lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h b/src/lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h index 64e3fb66b..31d6457b5 100644 --- a/src/lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h +++ b/src/lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h @@ -40,6 +40,12 @@ public: bool is_active_quoting() const {return _leg_params.is_active_quoting();} const bts::api::orderman::OrderDefinition& quoter_order_definition() const {return _quoter_order_definition;} const bts::api::orderman::OrderDefinition& hedger_order_definition() const {return _hedger_order_definition;} + bts::spark::LegSpreaderParams::LegMarketDataCalculation leg_md_calc() const { + return _leg_params.leg_md_calc(); + } + bts::spark::LegSpreaderParams::MarketDataType md_type() const { + return _leg_params.market_data_type(); + } }; }} diff --git a/src/lib/orderman/orders/spreader_orders/spreader/SpreaderBookkeepingData.cpp b/src/lib/orderman/orders/spreader_orders/spreader/SpreaderBookkeepingData.cpp index 9c87cd6fb..160d6304a 100644 --- a/src/lib/orderman/orders/spreader_orders/spreader/SpreaderBookkeepingData.cpp +++ b/src/lib/orderman/orders/spreader_orders/spreader/SpreaderBookkeepingData.cpp @@ -28,7 +28,7 @@ _reported_state(){ for (const auto& lpd_iter : legs){ _leg_prop_data.push_back(lpd_iter); const LegPropertyData& leg_property_data = _leg_prop_data.back(); - _books.emplace_back(); + _books.emplace_back(lpd_iter.leg_security_key()); _quoter_order_builders.emplace_back(spreader_order_key, leg_property_data.quoter_order_definition(), spreader_pao_services, 10); _hedger_order_builders.emplace_back(spreader_order_key, leg_property_data.hedger_order_definition(), spreader_pao_services, 10); ChildOrder empty_order; diff --git a/src/lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.cpp b/src/lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.cpp index 57c4e776a..ceb595573 100644 --- a/src/lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.cpp +++ b/src/lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.cpp @@ -19,7 +19,8 @@ _hedger_order_builder (hedger_order_builder), _quoter_child_order_state (quoter_child_order_state), _hedger_data_vector (hedger_data_vector), _book(book), -_unhedged_delta(unhedged_delta) +_unhedged_delta(unhedged_delta), +_settlement_price(md::INT_NAN) {} SpreaderLegData::SpreaderLegData(SpreaderLegData&& other) diff --git a/src/lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.h b/src/lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.h index b3ad546f6..f96c8346e 100644 --- a/src/lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.h +++ b/src/lib/orderman/orders/spreader_orders/spreader/SpreaderLegData.h @@ -6,6 +6,8 @@ #include "lib/orderman/actors/order_manager/OrderBuilder.h" #include "lib/orderman/orders/spreader_orders/spreader/external_interfacing/SpreaderOrderBuilder.h" #include "lib/orderman/orders/spreader_orders/spreader/hedger/UnhedgedDelta.h" +#include "lib/os/os.h" +#include "lib/logging/logging.h" namespace bts{ namespace orderman{ @@ -30,6 +32,7 @@ private: std::vector& _hedger_data_vector; Book& _book; UnhedgedDelta& _unhedged_delta; + md::IntPrice _settlement_price; public: SpreaderLegData (const LegPropertyData& leg_property_data, SingleLegQuoter& quoter, @@ -56,6 +59,7 @@ public: SingleLegQuoter& quoter() {return _quoter;} const SingleLegQuoter& quoter() const {return _quoter;} const bts::orderman::Book& book () const {return _book;} + //md::Price settlement_price() const {return _settlement_price;} SpreaderChildOrderState& order_update_message () {return _quoter_child_order_state;} bts::orderman::OrderKey key() {return _quoter_child_order_state.order_key();} bts::orderman::OrderKey order_key() {return _quoter_child_order_state.order_key();} @@ -99,6 +103,21 @@ public: // mutators //////////////////// //////////////////////////////// // called for every book update - void update_book(const bts::orderman::Book& book_arg) {_book = book_arg;} + void update_book(const Book& book) { + SpreaderUtils::copy_book(book, + _settlement_price, + _leg_property_data.md_type(), + _leg_property_data.leg_md_calc(), + _book); + } + void update_settlement_price(md::IntPrice price){ + _settlement_price=price; + SpreaderUtils::copy_book(_book, + _settlement_price, + _leg_property_data.md_type(), + _leg_property_data.leg_md_calc(), + _book); + + } }; }} diff --git a/src/lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h b/src/lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h index d0766cef5..792b7a4c6 100644 --- a/src/lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h +++ b/src/lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h @@ -136,6 +136,80 @@ public: } bts_assert(out_quantity.value() <= target_quantity.value()); } + // copies the price/qty content of the source book + // into the target, accounting for md_type and leg_calc. + // (note that the security_key in the target is left untouched + // and should be set by the callee) + // this is not very clean for the reason that the spreader + // uses the "size()" function in the book, so we copy the + // "effective size" to the size var in the Book, which + // for different values of md_type would mean different + // things. the better way to do it is to make a SpreaderBook + // that exposes effective_size() and routes it to the + // right size. + void static copy_book ( + const Book& source, + md::IntPrice settlement_price, + bts::spark::LegSpreaderParams::MarketDataType md_type, + bts::spark::LegSpreaderParams::LegMarketDataCalculation leg_calc, + Book& target){ + SecurityKey target_key=target.security_key(); + if(md_type==bts::spark::LegSpreaderParams::DIRECT){ + // since the "size()" in Book is the merged size + // we put the direct in the size + size_t target_i=0; + for(size_t source_i=0;source_i0){ + target.bid()[target_i]=md::PriceSize(b.int_price(),b.direct_size()); + target_i++; + } + } + empty_out_book(target.bid(),target_i,target.n_levels()); + target_i=0; + for(size_t source_i=0;source_i0){ + target.ask()[target_i]=md::PriceSize(a.int_price(),a.direct_size()); + target_i++; + } + } + empty_out_book(target.ask(),target_i,target.n_levels()); + } + else if(md_type==bts::spark::LegSpreaderParams::MERGED){ + target.bid()=source.bid(); + target.ask()=source.ask(); + } + if(leg_calc==bts::spark::LegSpreaderParams::NET_CHANGE){ + if(settlement_price.is_nan()){ + empty_out_book(target.bid(),0,target.n_levels()); + empty_out_book(target.ask(),0,target.n_levels()); + } + else{ + offset_book(target.bid(), target.n_levels(),settlement_price); + offset_book(target.ask(), target.n_levels(),settlement_price); + } + } + bts_assert(target_key==target.security_key()); + } + void static offset_book(BookSide& book_side, + size_t n_levels, + md::IntPrice offset){ + for(size_t i=0;i_bookkeeping_data().get_leg_data(l_idx).leg_property_data(); LOG(OrderMan, Info, "SpreaderStartStateWorker::on_activate subbing to book sec_key: {}", leg_prop.security_key()); order_services.order_services().book_updates.sub(leg_prop.leg_security_key(), - [this](const bts::orderman::BookUpdateEvent& bk_event) + [this](const BookUpdateEvent& bk_event) {_spreader->_on_book_update (bk_event);}); + order_services.order_services().settlement_updates.sub(leg_prop.leg_security_key(), + [this](const SettlementUpdateEvent& s_event) + {_spreader->_on_settlement_update (s_event);}); } // 6. sub to time order_services.order_services().time_change.sub([this](const bts::mercury::TimerEvent& t_event){_spreader->_on_time(t_event);}); diff --git a/src/lib/orderman/orders/spreader_orders/tests/spreader_tests.cpp b/src/lib/orderman/orders/spreader_orders/tests/spreader_tests.cpp index c3ff6ed8e..4d2ab8287 100644 --- a/src/lib/orderman/orders/spreader_orders/tests/spreader_tests.cpp +++ b/src/lib/orderman/orders/spreader_orders/tests/spreader_tests.cpp @@ -2868,6 +2868,289 @@ TEST_CASE ("spreader_perf_4legs", "[spreader]") std::cout << "4 leg hedges:" << n_hedges << " unit time(usecs):" << hedge_time << std::endl; } } +TEST_CASE ("simple book addition", "[spreader]"){ + SECTION ("direct, book"){ + SecurityKey key(12345); + Book in_book (key); + fill_book(in_book, + new double[5] {2000, 2000.01, 2000.03, 2000.04, 2000.1}, + new uint16_t[5] {10, 1, 15, 14, 20}, + new uint16_t[5] {1, 12, 0, 0, 0}, + api::Side::SIDE_BUY, + true); + Book out_book(key); + SpreaderUtils::copy_book(in_book, + md::IntPrice_from_double<7>(1999.0), + bts::spark::LegSpreaderParams::DIRECT, + bts::spark::LegSpreaderParams::BOOK, + out_book); + + validate_book(out_book, + key, + api::Side::SIDE_BUY, + 0.01, + new double[5] {2000, 2000.01, 2000.03, 2000.04, 2000.1}, + new uint16_t[5] {10, 1, 15, 14, 20}, + true); + } + SECTION ("direct, book_holes"){ + SecurityKey key(12345); + Book in_book (key); + fill_book(in_book, + new double[5] {2000, 2000.01, 2000.03, 2000.04, 2000.1}, + new uint16_t[5] {0, 1, 0, 14, 20}, //directs + new uint16_t[5] {1, 12, 0, 0, 0}, //implieds + api::Side::SIDE_BUY, + true); + Book out_book(key); + SpreaderUtils::copy_book(in_book, + md::IntPrice_from_double<7>(1999.0), + bts::spark::LegSpreaderParams::DIRECT, + bts::spark::LegSpreaderParams::BOOK, + out_book); + + validate_book(out_book, + key, + api::Side::SIDE_BUY, + 0.01, + new double[5] {2000.01, 2000.04, 2000.10, os::nan(), os::nan()}, + new uint16_t[5] {1, 14, 20, 0, 0}, + true); + } + SECTION ("direct, net_change"){ + SecurityKey key(13579); + Book in_book (key); + fill_book(in_book, + new double[5] {2000, 2000.01, 2000.03, 2000.04, 2000.1}, + new uint16_t[5] {10, 1, 15, 14, 20}, + new uint16_t[5] {1, 12, 0, 0, 0}, + api::Side::SIDE_BUY, + true); + Book out_book(key); + SpreaderUtils::copy_book(in_book, + md::IntPrice_from_double<7>(1999.0), + bts::spark::LegSpreaderParams::DIRECT, + bts::spark::LegSpreaderParams::NET_CHANGE, + out_book); + + validate_book(out_book, + key, + api::Side::SIDE_BUY, + 0.01, + new double[5] {1, 1.01, 1.03, 1.04, 1.1}, + new uint16_t[5] {10, 1, 15, 14, 20}, + true); + } + SECTION ("direct, net_change_nan"){ + SecurityKey key(13579); + Book in_book (key); + fill_book(in_book, + new double[5] {2000, 2000.01, 2000.03, 2000.04, 2000.1}, + new uint16_t[5] {10, 1, 15, 14, 20}, + new uint16_t[5] {1, 12, 0, 0, 0}, + api::Side::SIDE_BUY, + true); + Book out_book(key); + SpreaderUtils::copy_book(in_book, + md::INT_NAN, + bts::spark::LegSpreaderParams::DIRECT, + bts::spark::LegSpreaderParams::NET_CHANGE, + out_book); + + validate_book(out_book, + key, + api::Side::SIDE_BUY, + 0.01, + new double[5] {os::nan(), os::nan(), os::nan(), os::nan(), os::nan()}, + new uint16_t[5] {0, 0, 0, 0, 0}, + true); + } + SECTION ("merged, book"){ + SecurityKey key(24680); + Book in_book (key); + fill_book(in_book, + new double[5] {2000, 2000.01, 2000.03, 2000.04, 2000.1}, + new uint16_t[5] {10, 1, 15, 14, 20}, + new uint16_t[5] {1, 12, 0, 0, 0}, + api::Side::SIDE_BUY, + true); + Book out_book(key); + SpreaderUtils::copy_book(in_book, + md::IntPrice_from_double<7>(1999.0), + bts::spark::LegSpreaderParams::MERGED, + bts::spark::LegSpreaderParams::BOOK, + out_book); + + validate_book(out_book, + key, + api::Side::SIDE_BUY, + 0.01, + new double[5] {2000, 2000.01, 2000.03, 2000.04, 2000.1}, + new uint16_t[5] {11, 13, 15, 14, 20}, + true); + } + SECTION ("merged, net_change"){ + SecurityKey key(54321); + Book in_book (key); + fill_book(in_book, + new double[5] {2000, 2000.01, 2000.03, 2000.04, 2000.1}, + new uint16_t[5] {10, 1, 15, 14, 20}, + new uint16_t[5] {1, 12, 0, 0, 0}, + api::Side::SIDE_BUY, + true); + Book out_book(key); + SpreaderUtils::copy_book(in_book, + md::IntPrice_from_double<7>(1999.0), + bts::spark::LegSpreaderParams::MERGED, + bts::spark::LegSpreaderParams::NET_CHANGE, + out_book); + + validate_book(out_book, + key, + api::Side::SIDE_BUY, + 0.01, + new double[5] {1, 1.01, 1.03, 1.04, 1.1}, + new uint16_t[5] {11, 13, 15, 14, 20}, + true); + } +} +TEST_CASE ("random book addition", "[spreader]"){ + size_t n_runs=100; + // generates random prices, with tick size=0.01 + size_t n_levels=5; + double* bid_prices=new double[n_levels], + *ask_prices=new double[n_levels]; + uint16_t* direct_bid_sizes=new uint16_t[n_levels], + *implied_bid_sizes=new uint16_t[n_levels], + *direct_ask_sizes=new uint16_t[n_levels], + *implied_ask_sizes=new uint16_t[n_levels]; + double* exp_bid_prices=new double[n_levels], + *exp_ask_prices=new double[n_levels]; + uint16_t* exp_bid_sizes=new uint16_t[n_levels], + *exp_ask_sizes=new uint16_t[n_levels]; + for(size_t i=0;i(settlement_price), + bts::spark::LegSpreaderParams::DIRECT, + bts::spark::LegSpreaderParams::BOOK, + out_book); + validate_book(out_book, + key, + api::Side::SIDE_BUY, + tick_size, + bid_prices, + direct_bid_sizes, + false); + validate_book(out_book, + key, + api::Side::SIDE_SELL, + tick_size, + ask_prices, + direct_ask_sizes, + false); + } + // "direct, net_change" + { + for(size_t i=0;i(settlement_price), + bts::spark::LegSpreaderParams::DIRECT, + bts::spark::LegSpreaderParams::NET_CHANGE, + out_book); + validate_book(out_book, + key, + api::Side::SIDE_BUY, + tick_size, + exp_bid_prices, + direct_bid_sizes, + false); + validate_book(out_book, + key, + api::Side::SIDE_SELL, + tick_size, + exp_ask_prices, + direct_ask_sizes, + false); + } + // merged, book + { + for(size_t i=0;i(settlement_price), + bts::spark::LegSpreaderParams::MERGED, + bts::spark::LegSpreaderParams::BOOK, + out_book); + validate_book(out_book, + key, + api::Side::SIDE_BUY, + tick_size, + bid_prices, + exp_bid_sizes, + false); + validate_book(out_book, + key, + api::Side::SIDE_SELL, + tick_size, + ask_prices, + exp_ask_sizes, + false); + } + // merged, net change + { + for(size_t i=0;i(settlement_price), + bts::spark::LegSpreaderParams::MERGED, + bts::spark::LegSpreaderParams::NET_CHANGE, + out_book); + validate_book(out_book, + key, + api::Side::SIDE_BUY, + tick_size, + exp_bid_prices, + exp_bid_sizes, + false); + validate_book(out_book, + key, + api::Side::SIDE_SELL, + tick_size, + exp_ask_prices, + exp_ask_sizes, + false); + } + } +} class RV { public: diff --git a/src/lib/orderman/orders/spreader_orders/tests/tests_common.h b/src/lib/orderman/orders/spreader_orders/tests/tests_common.h index a927df1b4..6d60f8527 100644 --- a/src/lib/orderman/orders/spreader_orders/tests/tests_common.h +++ b/src/lib/orderman/orders/spreader_orders/tests/tests_common.h @@ -14,6 +14,7 @@ #include "lib/orderman/orders/spreader_orders/spreader/SpreaderChildOrderState.h" #include "lib/orderman/orders/spreader_orders/spreader/external_interfacing/SpreaderOrderBuilder.h" #include +#include "lib/os/os.h" namespace bts { namespace orderman { @@ -250,13 +251,32 @@ SpreaderPreActivationOrderServices::TestOnlySpreaderPreActivationOrderServices _ /**********************************************************************/ /******************************book************************************/ /**********************************************************************/ + +void fill_book(bts::orderman::Book& book, + double* prices, + uint16_t* direct_sizes, + uint16_t* implied_sizes, + bts::api::Side side, + bool delete_arrays){ + bts::orderman::BookSide& bookside = book.side(side); + for (size_t i = 0; i < book.n_levels(); i++) + bookside[i] = md::PriceSize(md::IntPrice_from_double<7>(prices[i]), + direct_sizes[i], + md::ImpliedSize(implied_sizes[i])); + if (delete_arrays){ + delete[] prices; + delete[] direct_sizes; + delete[] implied_sizes; + } +} + void fill_book(bts::orderman::Book& book, double* prices, uint16_t* sizes, bts::api::Side side){ bts::orderman::BookSide& bookside = book.side(side); for (size_t i = 0; i < book.n_levels(); i++) - bookside[i] = md::PriceSize::from_double<6>(prices[i], sizes[i]); + bookside[i] = md::PriceSize_from_double<6>(prices[i], sizes[i]); } void fill_book(bts::orderman::Book& book, double* bid_prices, @@ -266,8 +286,8 @@ void fill_book(bts::orderman::Book& book, bts::orderman::BookSide& bidside = book.side(api::Side::SIDE_BUY); bts::orderman::BookSide& askside = book.side(api::Side::SIDE_SELL); for (size_t i = 0; i < book.n_levels(); i++){ - bidside[i] = md::PriceSize::from_double<6>(bid_prices[i], bid_sizes[i]); - askside[i] = md::PriceSize::from_double<6>(ask_prices[i], ask_sizes[i]); + bidside[i] = md::PriceSize_from_double<6>(bid_prices[i], bid_sizes[i]); + askside[i] = md::PriceSize_from_double<6>(ask_prices[i], ask_sizes[i]); } } bts::orderman::Book get_book(SecurityKey key, @@ -280,11 +300,11 @@ bts::orderman::Book get_book(SecurityKey key, bool delete_arrays){ bts::orderman::BookSide bidside, askside; for (size_t i = 0; i < n_bid_levels; i++){ - md::PriceSize bid = md::PriceSize::from_double<6>(bid_prices[i], bid_sizes[i]); + md::PriceSize bid = md::PriceSize_from_double<6>(bid_prices[i], bid_sizes[i]); bidside[i] = bid; } for (size_t i = 0; i < n_ask_levels; i++){ - md::PriceSize ask = md::PriceSize::from_double<6>(ask_prices[i], ask_sizes[i]); + md::PriceSize ask = md::PriceSize_from_double<6>(ask_prices[i], ask_sizes[i]); askside[i] = ask; } if (delete_arrays){ @@ -465,6 +485,30 @@ void validate_book (const bts::orderman::Book& b1, const bts::orderman::Book& b2 REQUIRE (b1.ask()[i].size() == b2.ask()[i].size()); } } +void validate_book (const bts::orderman::Book& book, + SecurityKey key, + api::Side side, + double tick_size, + double* prices, + uint16_t* sizes, + bool delete_arrays){ + REQUIRE(book.security_key()==key); + const BookSide& md=side==api::Side::SIDE_BUY ? book.bid() : book.ask(); + for(size_t i=0; i& drivers, size_t n_legs){ drivers.push_back(d); } } +void generate_random_book(size_t n_levels, + double tick_size, + double top_book, + double top_ask, + // out vars + double* bid_prices, + uint16_t* direct_bid_sizes, + uint16_t* implied_bid_sizes, + double* ask_prices, + uint16_t* direct_ask_sizes, + uint16_t* implied_ask_sizes, + Book& book){ + double b=top_book, + a=top_ask; + for(size_t i=0;i()), md_consumer(ss), - stats(stats) - {} + stats(stats) {} ServiceBridge *service_bridge; @@ -111,9 +111,8 @@ struct OrderServiceServer::PImpl { OrderServiceServer::OrderServiceServer( bts::api::ServiceContext &ctx, std::shared_ptr &ss, - metrics::MetricsContainer &stats, - PerfTraceLog &log) - : api::orderman::OrderServiceBase(ctx), me(new PImpl(ctx, ss, stats)) { + metrics::MetricsContainer &stats, PerfTraceLog &log) + : api::orderman::OrderServiceBase(ctx), me(new PImpl(ctx, ss, stats)) { me->publish_orders = make_orders_publisher(); me->publish_fills = make_fills_publisher(); @@ -186,6 +185,14 @@ OrderServiceServer::OrderServiceServer( "the OrdermanCfg key"); thread->executor.spawn(me->md_consumer); + // MDSupplementarySource + thread = find_thread_for_actor("MDSupplementarySource", me->actor_threads); + bts_assert_msg( + thread, + "Could not find a configured thread for MDSupplementarySource. See " + "the OrdermanCfg key"); + thread->executor.spawn(me->md_consumer); + // ServiceBridge thread = find_thread_for_actor("ServiceBridge", me->actor_threads); bts_assert_msg(thread, diff --git a/src/lib/sparkproto/SparkProtos.proto b/src/lib/sparkproto/SparkProtos.proto index e30e01b2e..77b71f857 100644 --- a/src/lib/sparkproto/SparkProtos.proto +++ b/src/lib/sparkproto/SparkProtos.proto @@ -28,8 +28,17 @@ message RulePriority int32 priority = 20; } + message LegSpreaderParams { + enum MarketDataType { + MERGED = 0; + DIRECT = 1; + } + enum LegMarketDataCalculation{ + BOOK=0; + NET_CHANGE=1; + } /// Legs can be quoted actively, or sent in reponse /// to fills bool is_active_quoting = 20; @@ -37,6 +46,8 @@ message LegSpreaderParams QuoteParams quote_params = 40; PreHedgeParams pre_hedge_params = 50; PostHedgeParams post_hedge_params = 60; + MarketDataType market_data_type = 70; + LegMarketDataCalculation leg_md_calc = 80; } // /************************************************************************************************* diff --git a/src/lib/utils/datetime.cpp b/src/lib/utils/datetime.cpp index 85fc9235e..5e858e6e9 100755 --- a/src/lib/utils/datetime.cpp +++ b/src/lib/utils/datetime.cpp @@ -1,446 +1,446 @@ -#include "datetime.h" - -#include "bts_assert.h" -#include "TimeStamp.h" - -#include -#include -#include - -namespace bts { -namespace datetime { - -const boost::gregorian::date anchor (1970, boost::gregorian::Jan, 1); -const uint64_t anchor_ticks = 621355968000000000; -const uint64_t micros_per_day = 86400000000; -const uint64_t ticks_per_micro = 10; +#include "datetime.h" + +#include "bts_assert.h" +#include "TimeStamp.h" + +#include +#include +#include + +namespace bts { +namespace datetime { + +const boost::gregorian::date anchor (1970, boost::gregorian::Jan, 1); +const uint64_t anchor_ticks = 621355968000000000; +const uint64_t micros_per_day = 86400000000; +const uint64_t ticks_per_micro = 10; const uint64_t ticks_per_day = micros_per_day * ticks_per_micro; const uint64_t ticks_mask = 0x3FFFFFFFFFFFFFFF; -// DateTimeKinds +// DateTimeKinds const uint64_t KindUnspecified = 0x0000000000000000; const uint64_t KindUtc = 0x4000000000000000; -const uint64_t KindLocal = 0x8000000000000000; - -unsigned long long ptime_to_ll(PTime const &t, uint64_t const &kind) { - uint64_t days = (t.date() - anchor).days(); - uint64_t usecs = t.time_of_day().total_microseconds(); - uint64_t ticks = anchor_ticks + usecs * ticks_per_micro + days * ticks_per_day; - return (ticks & ticks_mask) | kind; -} - -unsigned long long ptime_to_ll(PTime const &t) { - return ptime_to_ll(t, KindUnspecified); -} - -LLDateTime ll_now() { - return ptime_to_ll(boost::posix_time::microsec_clock::universal_time(), KindUtc); -} - -LLDateTime ll_local_now() { - return ptime_to_ll(boost::posix_time::microsec_clock::local_time (), KindLocal); -} - -long unix_now() { - return (boost::posix_time::microsec_clock::universal_time() - PTime(anchor)).total_microseconds() / (long)1E6; -} - -long long ll_diff_in_us (unsigned long long ll1, unsigned long long ll2) -{ -#ifndef NO_CPP0X - auto p1 = ll_to_ptime (ll1); - auto p2 = ll_to_ptime (ll2); // NOLINT - return (p2 - p1).total_microseconds (); -#else - return 0; -#endif -} - - -long long ptime_diff_in_us (PTime const &t1, PTime const &t2) -{ - return (t1 - t2).total_microseconds (); -} - -long long ptime_diff_in_ms (PTime const &t1, PTime const &t2) -{ - return (t1 - t2).total_milliseconds (); -} - -long long ptime_diff_in_secs (PTime const &t1, PTime const &t2) -{ - return (t1 - t2).total_seconds (); -} - -PTime timestamp_to_ptime(utils::TimeStamp const&t) -{ - auto now_ts = utils::TimeStamp::now(); - auto now = datetime::local_now(); - - auto delta = (now_ts - t).milliseconds(); - return now - boost::posix_time::milliseconds(delta); -} - -utils::TimeStamp ptime_to_timestamp(PTime const &t) -{ - auto now_ts = utils::TimeStamp::now(); - auto now = datetime::now(); - - auto delta = (now - t).total_milliseconds(); - return now_ts.shift_ms (delta); -} - -long long usecs_since (PTime const &t1) -{ - return (boost::posix_time::microsec_clock::universal_time () - t1).total_microseconds (); -} - -long long msecs_since (PTime const &t1) -{ - return (boost::posix_time::microsec_clock::universal_time () - t1).total_milliseconds (); -} - -PTime now () -{ - return boost::posix_time::microsec_clock::universal_time (); -} - -PTime local_now () -{ - return boost::posix_time::microsec_clock::local_time (); -} - -PTime make_ptime (int year, int month, int day) -{ - // January=1 - bts_assert_d(month > 0); - boost::gregorian::date dt (year, boost::gregorian::greg_month (month),day); - return PTime (dt); -} - -PTime make_ptime (int year, int month, int day, int hour, int minute, int second, int millisecond) -{ - boost::gregorian::date dt (year, boost::gregorian::greg_month (month),day); - return PTime (dt, boost::posix_time::time_duration(hour, minute, second) + boost::posix_time::milliseconds(millisecond)); -} - -PTime ll_to_ptime (LLDateTime ll) { - ll &= ticks_mask; - uint64_t ticks_part = ll % ticks_per_day; - uint64_t days_part = (ll - anchor_ticks) / ticks_per_day; - return PTime(anchor + boost::gregorian::date_duration(days_part), - boost::posix_time::time_duration(boost::posix_time::microseconds(ticks_part / ticks_per_micro))); -} - -extern "C" BTS_EXPORT int ll_to_cstr (LLDateTime ll, char*buffer, int buffer_sz) -{ - const int str_sz = sizeof ("20121204-14:44:02.000"); - bts_assert (buffer_sz >= str_sz); - auto str = FIX_timestamp (ll_to_ptime (ll)); - bts_assert (str.size () == str_sz-1); - memcpy (buffer,&str[0],str.size ()); - buffer[str.size ()] = '\0'; - return str.size (); -} - -std::string time_string (PTime const &t) -{ - int hours = t.time_of_day ().hours (); - int minutes = t.time_of_day ().minutes (); - int seconds = t.time_of_day ().seconds (); - int whole_ms = seconds * 1000 + minutes * 1000 * 60 + hours * 1000 * 60 * 60; - int frac_ms = t.time_of_day ().total_milliseconds () - whole_ms; - std::ostringstream out; - out << std::setw( 2 ) << std::setfill( '0' ) << hours << ":" - << std::setw( 2 ) << std::setfill( '0' ) << minutes << ":" - << std::setw( 2 ) << std::setfill( '0' ) << seconds << "." - << std::setw( 3 ) << std::setfill( '0' ) << frac_ms; - return out.str (); -} - -std::string date_string (PTime const &t) -{ - std::string result (10,' '); - sprintf (&result[0], "%02d/%02d/%04d", - t.date ().month ().as_number (), - t.date ().day ().as_number (), - (unsigned short) t.date ().year ()); - return result; -} - -std::string iso_date_time_string (PTime const &t) -{ - return boost::posix_time::to_iso_extended_string(t); -} - -std::string simple_date_time_string (PTime const &t) -{ - return boost::posix_time::to_simple_string(t); -} - -std::string FIX_date_time_string (PTime const &t) -{ - std::string result (strlen ("20121204-14:44:02"),' '); - sprintf (&result[0],"%d%02d%02d-%02d:%02d:%02d", - (unsigned short) t.date ().year (), - t.date ().month ().as_number (), - t.date ().day ().as_number (), - t.time_of_day ().hours (), - t.time_of_day ().minutes (), - t.time_of_day ().seconds ()); - return result; -} - -void FIX_timestamp (PTime const &t, std::string *out) -{ - int hours = t.time_of_day ().hours (); - int minutes = t.time_of_day ().minutes (); - int seconds = t.time_of_day ().seconds (); - int whole_ms = seconds * 1000 + minutes * 1000 * 60 + hours * 1000 * 60 * 60; - int frac_ms = t.time_of_day ().total_milliseconds () - whole_ms; - out->resize(sizeof ("20121204-14:44:02.000")); - -#ifdef WIN32 - _snprintf (&(*out)[0],out->size (), - "%04d%02d%02d-%02d:%02d:%02d.%03d", - (int) t.date ().year (), - t.date ().month ().as_number (), - t.date ().day ().as_number (), - hours, - minutes, - seconds, - frac_ms); -#else - snprintf (&(*out)[0],out->size (), - "%04d%02d%02d-%02d:%02d:%02d.%03d", - (int) t.date ().year (), - t.date ().month ().as_number (), - t.date ().day ().as_number (), - hours, - minutes, - seconds, - frac_ms); -#endif - out->resize (out->size ()-1); -} - -std::string FIX_timestamp (PTime const &t) -{ - std::string result; - FIX_timestamp(t,&result); - return result; -} - -bool is_from_today(PTime const &time) -{ - return (time.date() == boost::posix_time::microsec_clock::universal_time().date()); -} - -bool is_at_least_msecs_old (PTime const &time, uint64_t ms) -{ - PTime now = boost::posix_time::microsec_clock::universal_time (); - return (now - time).total_milliseconds () >= (int64_t) ms; -} - -bool is_at_least_secs_old (PTime const &time, uint64_t sec) -{ - PTime now = boost::posix_time::microsec_clock::universal_time (); - return (now - time).total_seconds () >= (int64_t) sec; -} - -bool is_at_least_mins_old (PTime const &time, uint64_t mins) -{ - PTime now = boost::posix_time::microsec_clock::universal_time (); - return ((now - time).total_seconds ()/60) >= (int64_t) mins; -} - -BTS_EXPORT bool is_at_least_hours_old (PTime const &time, uint64_t hours) -{ - PTime now = boost::posix_time::microsec_clock::universal_time (); - int64_t secs = (int64_t) ((now - time).total_seconds ()); - return (secs/60/60) >= (int64_t) hours; -} - -BTS_EXPORT bool is_at_least_days_old (PTime const &time, uint64_t days) -{ - return is_at_least_hours_old (time, days *24); -} - -BTS_EXPORT LLDateTime ll_from_unix_epoch(long seconds) -{ - PTime result(anchor, boost::posix_time::seconds(seconds)); - return ptime_to_ll(result, KindUnspecified); -} - -BTS_EXPORT LLDateTime ll_from_unix_epoch_ms(long long milliseconds) -{ - PTime result(anchor, boost::posix_time::millisec(milliseconds)); - return ptime_to_ll(result, KindUnspecified); -} - -BTS_EXPORT void ll_to_parts(LLDateTime ll_time, int& year, int& month, int& day, int& hour, int& minute, int& second, int& millisecond) -{ - PTime t = ll_to_ptime(ll_time); - year = t.date().year(); - month = t.date().month(); - day = t.date().day(); - hour = t.time_of_day().hours(); - minute = t.time_of_day().minutes(); - second = t.time_of_day().seconds(); - int whole_ms = second * 1000 + minute * 1000 * 60 + hour * 1000 * 60 * 60; - millisecond = t.time_of_day().total_milliseconds() - whole_ms; -} - -BTS_EXPORT LLDateTime ll_from_parts(int year, int month, int day, int hour, int minute, int second, int millisecond) -{ - return ptime_to_ll(make_ptime(year, month, day, hour, minute, second, millisecond)); // NOLINT -} - -BTS_EXPORT LLDateTime ll_from_date(const Date& date) { - if (date.year == 0 && date.day == 0 && date.month == 0) { - return 0; - } - return ll_from_parts(date.year, date.month, date.day, 0, 0, 0, 0); -} - -Date ll_to_date(LLDateTime time) { - Date date; - int year, month, day, unused; - ll_to_parts(time, year, month, day, unused, unused, unused, unused); - date.year = year; - date.month = month; - date.day = day; - return date; -} - -struct UtcTimeStamp { - int hour; - int min; - int sec; - int msec; - int day; - int month; - int year; -}; - -struct UtcTimeStampConvertor -{ - static UtcTimeStamp convert( const std::string& value) - { - bool haveMilliseconds = false; - - switch( value.size() ) - { - case 21: haveMilliseconds = true; - case 17: break; - default: bts_assert(false); - } - - int i = 0; - int c = 0; - for( c = 0; c < 8; ++c ) - if( !isdigit(value[i++]) ) bts_assert(false); - if (value[i++] != '-') bts_assert(false); - for( c = 0; c < 2; ++c ) - if( !isdigit(value[i++]) ) bts_assert(false); - if( value[i++] != ':' ) bts_assert(false); - for( c = 0; c < 2; ++c ) - if( !isdigit(value[i++]) ) bts_assert(false); - if( value[i++] != ':' ) bts_assert(false); - for( c = 0; c < 2; ++c ) - if( !isdigit(value[i++]) ) bts_assert(false); - - if( haveMilliseconds ) - { - if( value[i++] != '.' ) bts_assert(false); - for( c = 0; c < 3; ++c ) - if( !isdigit(value[i++]) ) bts_assert(false); - } - - int year, mon, mday, hour, min, sec, millis; - - i = 0; - - year = value[i++] - '0'; - year = 10 * year + value[i++] - '0'; - year = 10 * year + value[i++] - '0'; - year = 10 * year + value[i++] - '0'; - - mon = value[i++] - '0'; - mon = 10 * mon + value[i++] - '0'; - if( mon < 1 || 12 < mon ) bts_assert(false); - - mday = value[i++] - '0'; - mday = 10 * mday + value[i++] - '0'; - if( mday < 1 || 31 < mday ) bts_assert(false); - - ++i; // skip '-' - - hour = value[i++] - '0'; - hour = 10 * hour + value[i++] - '0'; - // No check for >= 0 as no '-' are converted here - if( 23 < hour ) bts_assert(false); - - ++i; // skip ':' - - min = value[i++] - '0'; - min = 10 * min + value[i++] - '0'; - // No check for >= 0 as no '-' are converted here - if( 59 < min ) bts_assert(false); - - ++i; // skip ':' - - sec = value[i++] - '0'; - sec = 10 * sec + value[i++] - '0'; - - // No check for >= 0 as no '-' are converted here - if( 60 < sec ) bts_assert(false); - - if( haveMilliseconds ) - { - millis = (100 * (value[i+1] - '0') - + 10 * (value[i+2] - '0') - + (value[i+3] - '0')); - } - else - millis = 0; - - return UtcTimeStamp {hour, min, sec, millis, mday, mon, year}; - } -}; - -PTime parse_FIX_timestamp (const std::string &ts_str) -{ - auto ts = UtcTimeStampConvertor::convert (ts_str); - boost::gregorian::date dt(ts.year, ts.month, ts.day); - // fucking boost. unbelieavable. - auto ticks_per_second = boost::posix_time::time_duration::rep_type::res_adjust(); - boost::posix_time::time_duration td (ts.hour, ts.min, ts.sec, ts.msec * (ticks_per_second / 1000)); - return PTime(dt, td); -} - -#ifndef NO_CPP0X -extern "C" BTS_EXPORT LLDateTime ll_today() { - return datetime::ll_from_date(today()); -} - -BTS_EXPORT Date today() { - auto now = datetime::now(); - Date date; - date.year = now.date().year(); - date.month = now.date().month(); - date.day = now.date().day(); - return date; -} -BTS_EXPORT Date add_days(Date indate, int days) { - boost::gregorian::date dt(indate.year, indate.month, indate.day); - auto adjusted_dt = dt + boost::gregorian::days(days); - Date date(adjusted_dt.year(), adjusted_dt.month(), adjusted_dt.day()); - return date; -} - -extern "C" BTS_EXPORT LLDateTime ll_yesterday() { - auto now = datetime::now() - boost::gregorian::days(1); - return datetime::ll_from_parts(now.date().year(), now.date().month(), now.date().day(), 0, 0, 0, 0); -} -#endif -}} +const uint64_t KindLocal = 0x8000000000000000; + +unsigned long long ptime_to_ll(PTime const &t, uint64_t const &kind) { + uint64_t days = (t.date() - anchor).days(); + uint64_t usecs = t.time_of_day().total_microseconds(); + uint64_t ticks = anchor_ticks + usecs * ticks_per_micro + days * ticks_per_day; + return (ticks & ticks_mask) | kind; +} + +unsigned long long ptime_to_ll(PTime const &t) { + return ptime_to_ll(t, KindUnspecified); +} + +LLDateTime ll_now() { + return ptime_to_ll(boost::posix_time::microsec_clock::universal_time(), KindUtc); +} + +LLDateTime ll_local_now() { + return ptime_to_ll(boost::posix_time::microsec_clock::local_time (), KindLocal); +} + +long unix_now() { + return (boost::posix_time::microsec_clock::universal_time() - PTime(anchor)).total_microseconds() / (long)1E6; +} + +long long ll_diff_in_us (unsigned long long ll1, unsigned long long ll2) +{ +#ifndef NO_CPP0X + auto p1 = ll_to_ptime (ll1); + auto p2 = ll_to_ptime (ll2); // NOLINT + return (p2 - p1).total_microseconds (); +#else + return 0; +#endif +} + + +long long ptime_diff_in_us (PTime const &t1, PTime const &t2) +{ + return (t1 - t2).total_microseconds (); +} + +long long ptime_diff_in_ms (PTime const &t1, PTime const &t2) +{ + return (t1 - t2).total_milliseconds (); +} + +long long ptime_diff_in_secs (PTime const &t1, PTime const &t2) +{ + return (t1 - t2).total_seconds (); +} + +PTime timestamp_to_ptime(utils::TimeStamp const&t) +{ + auto now_ts = utils::TimeStamp::now(); + auto now = datetime::local_now(); + + auto delta = (now_ts - t).milliseconds(); + return now - boost::posix_time::milliseconds(delta); +} + +utils::TimeStamp ptime_to_timestamp(PTime const &t) +{ + auto now_ts = utils::TimeStamp::now(); + auto now = datetime::now(); + + auto delta = (now - t).total_milliseconds(); + return now_ts.shift_ms (delta); +} + +long long usecs_since (PTime const &t1) +{ + return (boost::posix_time::microsec_clock::universal_time () - t1).total_microseconds (); +} + +long long msecs_since (PTime const &t1) +{ + return (boost::posix_time::microsec_clock::universal_time () - t1).total_milliseconds (); +} + +PTime now () +{ + return boost::posix_time::microsec_clock::universal_time (); +} + +PTime local_now () +{ + return boost::posix_time::microsec_clock::local_time (); +} + +PTime make_ptime (int year, int month, int day) +{ + // January=1 + bts_assert_d(month > 0); + boost::gregorian::date dt (year, boost::gregorian::greg_month (month),day); + return PTime (dt); +} + +PTime make_ptime (int year, int month, int day, int hour, int minute, int second, int millisecond) +{ + boost::gregorian::date dt (year, boost::gregorian::greg_month (month),day); + return PTime (dt, boost::posix_time::time_duration(hour, minute, second) + boost::posix_time::milliseconds(millisecond)); +} + +PTime ll_to_ptime (LLDateTime ll) { + ll &= ticks_mask; + uint64_t ticks_part = ll % ticks_per_day; + uint64_t days_part = (ll - anchor_ticks) / ticks_per_day; + return PTime(anchor + boost::gregorian::date_duration(days_part), + boost::posix_time::time_duration(boost::posix_time::microseconds(ticks_part / ticks_per_micro))); +} + +extern "C" BTS_EXPORT int ll_to_cstr (LLDateTime ll, char*buffer, int buffer_sz) +{ + const int str_sz = sizeof ("20121204-14:44:02.000"); + bts_assert (buffer_sz >= str_sz); + auto str = FIX_timestamp (ll_to_ptime (ll)); + bts_assert (str.size () == str_sz-1); + memcpy (buffer,&str[0],str.size ()); + buffer[str.size ()] = '\0'; + return str.size (); +} + +std::string time_string (PTime const &t) +{ + int hours = t.time_of_day ().hours (); + int minutes = t.time_of_day ().minutes (); + int seconds = t.time_of_day ().seconds (); + int whole_ms = seconds * 1000 + minutes * 1000 * 60 + hours * 1000 * 60 * 60; + int frac_ms = t.time_of_day ().total_milliseconds () - whole_ms; + std::ostringstream out; + out << std::setw( 2 ) << std::setfill( '0' ) << hours << ":" + << std::setw( 2 ) << std::setfill( '0' ) << minutes << ":" + << std::setw( 2 ) << std::setfill( '0' ) << seconds << "." + << std::setw( 3 ) << std::setfill( '0' ) << frac_ms; + return out.str (); +} + +std::string date_string (PTime const &t) +{ + std::string result (10,' '); + sprintf (&result[0], "%02d/%02d/%04d", + t.date ().month ().as_number (), + t.date ().day ().as_number (), + (unsigned short) t.date ().year ()); + return result; +} + +std::string iso_date_time_string (PTime const &t) +{ + return boost::posix_time::to_iso_extended_string(t); +} + +std::string simple_date_time_string (PTime const &t) +{ + return boost::posix_time::to_simple_string(t); +} + +std::string FIX_date_time_string (PTime const &t) +{ + std::string result (strlen ("20121204-14:44:02"),' '); + sprintf (&result[0],"%d%02d%02d-%02d:%02d:%02d", + (unsigned short) t.date ().year (), + t.date ().month ().as_number (), + t.date ().day ().as_number (), + t.time_of_day ().hours (), + t.time_of_day ().minutes (), + t.time_of_day ().seconds ()); + return result; +} + +void FIX_timestamp (PTime const &t, std::string *out) +{ + int hours = t.time_of_day ().hours (); + int minutes = t.time_of_day ().minutes (); + int seconds = t.time_of_day ().seconds (); + int whole_ms = seconds * 1000 + minutes * 1000 * 60 + hours * 1000 * 60 * 60; + int frac_ms = t.time_of_day ().total_milliseconds () - whole_ms; + out->resize(sizeof ("20121204-14:44:02.000")); + +#ifdef WIN32 + _snprintf (&(*out)[0],out->size (), + "%04d%02d%02d-%02d:%02d:%02d.%03d", + (int) t.date ().year (), + t.date ().month ().as_number (), + t.date ().day ().as_number (), + hours, + minutes, + seconds, + frac_ms); +#else + snprintf (&(*out)[0],out->size (), + "%04d%02d%02d-%02d:%02d:%02d.%03d", + (int) t.date ().year (), + t.date ().month ().as_number (), + t.date ().day ().as_number (), + hours, + minutes, + seconds, + frac_ms); +#endif + out->resize (out->size ()-1); +} + +std::string FIX_timestamp (PTime const &t) +{ + std::string result; + FIX_timestamp(t,&result); + return result; +} + +bool is_from_today(PTime const &time) +{ + return (time.date() == boost::posix_time::microsec_clock::universal_time().date()); +} + +bool is_at_least_msecs_old (PTime const &time, uint64_t ms) +{ + PTime now = boost::posix_time::microsec_clock::universal_time (); + return (now - time).total_milliseconds () >= (int64_t) ms; +} + +bool is_at_least_secs_old (PTime const &time, uint64_t sec) +{ + PTime now = boost::posix_time::microsec_clock::universal_time (); + return (now - time).total_seconds () >= (int64_t) sec; +} + +bool is_at_least_mins_old (PTime const &time, uint64_t mins) +{ + PTime now = boost::posix_time::microsec_clock::universal_time (); + return ((now - time).total_seconds ()/60) >= (int64_t) mins; +} + +BTS_EXPORT bool is_at_least_hours_old (PTime const &time, uint64_t hours) +{ + PTime now = boost::posix_time::microsec_clock::universal_time (); + int64_t secs = (int64_t) ((now - time).total_seconds ()); + return (secs/60/60) >= (int64_t) hours; +} + +BTS_EXPORT bool is_at_least_days_old (PTime const &time, uint64_t days) +{ + return is_at_least_hours_old (time, days *24); +} + +BTS_EXPORT LLDateTime ll_from_unix_epoch(long seconds) +{ + PTime result(anchor, boost::posix_time::seconds(seconds)); + return ptime_to_ll(result, KindUnspecified); +} + +BTS_EXPORT LLDateTime ll_from_unix_epoch_ms(long long milliseconds) +{ + PTime result(anchor, boost::posix_time::millisec(milliseconds)); + return ptime_to_ll(result, KindUnspecified); +} + +BTS_EXPORT void ll_to_parts(LLDateTime ll_time, int& year, int& month, int& day, int& hour, int& minute, int& second, int& millisecond) +{ + PTime t = ll_to_ptime(ll_time); + year = t.date().year(); + month = t.date().month(); + day = t.date().day(); + hour = t.time_of_day().hours(); + minute = t.time_of_day().minutes(); + second = t.time_of_day().seconds(); + int whole_ms = second * 1000 + minute * 1000 * 60 + hour * 1000 * 60 * 60; + millisecond = t.time_of_day().total_milliseconds() - whole_ms; +} + +BTS_EXPORT LLDateTime ll_from_parts(int year, int month, int day, int hour, int minute, int second, int millisecond) +{ + return ptime_to_ll(make_ptime(year, month, day, hour, minute, second, millisecond)); // NOLINT +} + +BTS_EXPORT LLDateTime ll_from_date(const Date& date) { + if (date.year == 0 && date.day == 0 && date.month == 0) { + return 0; + } + return ll_from_parts(date.year, date.month, date.day, 0, 0, 0, 0); +} + +Date ll_to_date(LLDateTime time) { + Date date; + int year, month, day, unused; + ll_to_parts(time, year, month, day, unused, unused, unused, unused); + date.year = year; + date.month = month; + date.day = day; + return date; +} + +struct UtcTimeStamp { + int hour; + int min; + int sec; + int msec; + int day; + int month; + int year; +}; + +struct UtcTimeStampConvertor +{ + static UtcTimeStamp convert( const std::string& value) + { + bool haveMilliseconds = false; + + switch( value.size() ) + { + case 21: haveMilliseconds = true; + case 17: break; + default: bts_assert(false); + } + + int i = 0; + int c = 0; + for( c = 0; c < 8; ++c ) + if( !isdigit(value[i++]) ) bts_assert(false); + if (value[i++] != '-') bts_assert(false); + for( c = 0; c < 2; ++c ) + if( !isdigit(value[i++]) ) bts_assert(false); + if( value[i++] != ':' ) bts_assert(false); + for( c = 0; c < 2; ++c ) + if( !isdigit(value[i++]) ) bts_assert(false); + if( value[i++] != ':' ) bts_assert(false); + for( c = 0; c < 2; ++c ) + if( !isdigit(value[i++]) ) bts_assert(false); + + if( haveMilliseconds ) + { + if( value[i++] != '.' ) bts_assert(false); + for( c = 0; c < 3; ++c ) + if( !isdigit(value[i++]) ) bts_assert(false); + } + + int year, mon, mday, hour, min, sec, millis; + + i = 0; + + year = value[i++] - '0'; + year = 10 * year + value[i++] - '0'; + year = 10 * year + value[i++] - '0'; + year = 10 * year + value[i++] - '0'; + + mon = value[i++] - '0'; + mon = 10 * mon + value[i++] - '0'; + if( mon < 1 || 12 < mon ) bts_assert(false); + + mday = value[i++] - '0'; + mday = 10 * mday + value[i++] - '0'; + if( mday < 1 || 31 < mday ) bts_assert(false); + + ++i; // skip '-' + + hour = value[i++] - '0'; + hour = 10 * hour + value[i++] - '0'; + // No check for >= 0 as no '-' are converted here + if( 23 < hour ) bts_assert(false); + + ++i; // skip ':' + + min = value[i++] - '0'; + min = 10 * min + value[i++] - '0'; + // No check for >= 0 as no '-' are converted here + if( 59 < min ) bts_assert(false); + + ++i; // skip ':' + + sec = value[i++] - '0'; + sec = 10 * sec + value[i++] - '0'; + + // No check for >= 0 as no '-' are converted here + if( 60 < sec ) bts_assert(false); + + if( haveMilliseconds ) + { + millis = (100 * (value[i+1] - '0') + + 10 * (value[i+2] - '0') + + (value[i+3] - '0')); + } + else + millis = 0; + + return UtcTimeStamp {hour, min, sec, millis, mday, mon, year}; + } +}; + +PTime parse_FIX_timestamp (const std::string &ts_str) +{ + auto ts = UtcTimeStampConvertor::convert (ts_str); + boost::gregorian::date dt(ts.year, ts.month, ts.day); + // fucking boost. unbelieavable. + auto ticks_per_second = boost::posix_time::time_duration::rep_type::res_adjust(); + boost::posix_time::time_duration td (ts.hour, ts.min, ts.sec, ts.msec * (ticks_per_second / 1000)); + return PTime(dt, td); +} + +#ifndef NO_CPP0X +extern "C" BTS_EXPORT LLDateTime ll_today() { + return datetime::ll_from_date(today()); +} + +BTS_EXPORT Date today() { + auto now = datetime::now(); + Date date; + date.year = now.date().year(); + date.month = now.date().month(); + date.day = now.date().day(); + return date; +} +BTS_EXPORT Date add_days(Date indate, int days) { + boost::gregorian::date dt(indate.year, indate.month, indate.day); + auto adjusted_dt = dt + boost::gregorian::days(days); + Date date(adjusted_dt.year(), adjusted_dt.month(), adjusted_dt.day()); + return date; +} + +extern "C" BTS_EXPORT LLDateTime ll_yesterday() { + auto now = datetime::now() - boost::gregorian::days(1); + return datetime::ll_from_parts(now.date().year(), now.date().month(), now.date().day(), 0, 0, 0, 0); +} +#endif +}} diff --git a/src/sdk/example/example.cpp b/src/sdk/example/example.cpp index ad7905e35..28b2c5d61 100644 --- a/src/sdk/example/example.cpp +++ b/src/sdk/example/example.cpp @@ -4,10 +4,11 @@ #include "spark/lib/md/MDProducer.h" #include "spark/lib/utils/PBUtils.h" #include "spark/lib/tracing/bts_trace_c.c" +#include "spark/lib/md/cme/MDP3Utils.h" using namespace bts; using namespace bts::md; - + void produce() { md::MDProducer producer(co2::PLATFORM_SIM); @@ -41,34 +42,34 @@ void produce() // initialized bid side book data std::array bids = { - md::PriceSize::from_double<7> (1.0,10), - md::PriceSize::from_double<7> (2.0,20), - md::PriceSize::from_double<7> (3.0,30), - md::PriceSize::from_double<7> (4.0,40), - md::PriceSize::from_double<7> (5.0,50), - md::PriceSize::from_double<7> (6.0,60), - md::PriceSize::from_double<7> (7.0,70), - md::PriceSize::from_double<7> (8.0,80), - md::PriceSize::from_double<7> (9.0,90), - md::PriceSize::from_double<7> (10.0,100) + md::PriceSize_from_double<7> (1.0,10), + md::PriceSize_from_double<7> (2.0,20), + md::PriceSize_from_double<7> (3.0,30), + md::PriceSize_from_double<7> (4.0,40), + md::PriceSize_from_double<7> (5.0,50), + md::PriceSize_from_double<7> (6.0,60), + md::PriceSize_from_double<7> (7.0,70), + md::PriceSize_from_double<7> (8.0,80), + md::PriceSize_from_double<7> (9.0,90), + md::PriceSize_from_double<7> (10.0,100) }; // initialized ask side book data std::array asks = { - md::PriceSize::from_double<7> (1.0,10), - md::PriceSize::from_double<7> (2.0,20), - md::PriceSize::from_double<7> (3.0,30), - md::PriceSize::from_double<7> (4.0,40), - md::PriceSize::from_double<7> (5.0,50), - md::PriceSize::from_double<7> (6.0,60), - md::PriceSize::from_double<7> (7.0,70), - md::PriceSize::from_double<7> (8.0,80), - md::PriceSize::from_double<7> (9.0,90), - md::PriceSize::from_double<7> (10.0,100) + md::PriceSize_from_double<7> (1.0,10), + md::PriceSize_from_double<7> (2.0,20), + md::PriceSize_from_double<7> (3.0,30), + md::PriceSize_from_double<7> (4.0,40), + md::PriceSize_from_double<7> (5.0,50), + md::PriceSize_from_double<7> (6.0,60), + md::PriceSize_from_double<7> (7.0,70), + md::PriceSize_from_double<7> (8.0,80), + md::PriceSize_from_double<7> (9.0,90), + md::PriceSize_from_double<7> (10.0,100) }; // Update book - this call does not use locks so should not be called concurrently - producer_handles[0].update_book(utils::TimeStamp::now(), bids, asks); + producer_handles[0].update_direct(utils::TimeStamp::now(), bids, asks); } int main() diff --git a/src/server/sparkd/CMakeDeps.cmake b/src/server/sparkd/CMakeDeps.cmake index 36b86a8a3..d739bee6c 100644 --- a/src/server/sparkd/CMakeDeps.cmake +++ b/src/server/sparkd/CMakeDeps.cmake @@ -1 +1 @@ -set(sources bts_trace.cpp;cmds/AutoCert.cpp;cmds/Benchmark.cpp;cmds/CmdRegistry.cpp;cmds/ConfigCheck.cpp;cmds/Help.cpp;cmds/Md.cpp;cmds/QuickTest.cpp;cmds/Run.cpp;cmds/RunSim.cpp;cmds/Span.cpp;cmds/Test.cpp;cmds/TestCeloxica.cpp;cmds/Web.cpp;dspark/mirage/quick_queue/quick_queue.cpp;exchange_sim/ExchangeSim.cpp;exchange_sim/ExchangeSimApplication.cpp;exchange_sim/book/Book.cpp;exchange_sim/book/ExchangeSim.proto;exchange_sim/book/Order.cpp;exchange_sim/book/OrderMatcher.cpp;exchange_sim/book/book_transform.cpp;exchange_sim/tests/test_book_transform.cpp;http/ApiGateway.cpp;http/WebServer.cpp;main.cpp;md/platforms/celoxica/CXListener.cpp;md/platforms/celoxica/CXMain.cpp;md/platforms/celoxica/tests/cx_tests.cpp;md/platforms/mdp3_onixs/MDP3Listener.cpp;md/platforms/mdp3_onixs/MDP3Main.cpp;md/platforms/mdp3_onixs/tests/mdp3_tests.cpp;md/run_md.cpp;position/IMarginObserver.cpp;position/IPositionObserver.cpp;position/MarginServiceCore.cpp;position/PositionServiceCore.cpp;position/PositionServiceServer.cpp;position/tests/PositionTests.cpp;support/ErrorReportService.cpp;support/ShellLatencyServer.cpp;user/AdminServiceServer.cpp;user/UserServiceServer.cpp;user/tests/admin_service_tests.cpp) \ No newline at end of file +set(sources bts_trace.cpp;cmds/AutoCert.cpp;cmds/Benchmark.cpp;cmds/CmdRegistry.cpp;cmds/ConfigCheck.cpp;cmds/Help.cpp;cmds/Md.cpp;cmds/QuickTest.cpp;cmds/Run.cpp;cmds/RunSim.cpp;cmds/Span.cpp;cmds/Test.cpp;cmds/TestCeloxica.cpp;cmds/Web.cpp;dspark/mirage/quick_queue/quick_queue.cpp;exchange_sim/ExchangeSim.cpp;exchange_sim/ExchangeSimApplication.cpp;exchange_sim/book/Book.cpp;exchange_sim/book/ExchangeSim.proto;exchange_sim/book/Order.cpp;exchange_sim/book/OrderMatcher.cpp;exchange_sim/book/book_transform.cpp;exchange_sim/tests/test_book_transform.cpp;http/ApiGateway.cpp;http/WebServer.cpp;main.cpp;md/ChannelVolume.cpp;md/MarketDataAuxiliaryServiceServer.cpp;md/VolumeAtPriceState.cpp;md/platforms/celoxica/CXListener.cpp;md/platforms/celoxica/CXMain.cpp;md/platforms/celoxica/tests/cx_tests.cpp;md/platforms/mdp3_onixs/MDP3Listener.cpp;md/platforms/mdp3_onixs/MDP3Main.cpp;md/platforms/mdp3_onixs/tests/mdp3_tests.cpp;md/run_md.cpp;position/IMarginObserver.cpp;position/IPositionObserver.cpp;position/MarginServiceCore.cpp;position/PositionServiceCore.cpp;position/PositionServiceServer.cpp;position/tests/PositionTests.cpp;support/ErrorReportService.cpp;support/ShellLatencyServer.cpp;user/AdminServiceServer.cpp;user/UserServiceServer.cpp;user/tests/admin_service_tests.cpp) \ No newline at end of file diff --git a/src/server/sparkd/cmds/Benchmark.cpp b/src/server/sparkd/cmds/Benchmark.cpp index 106d0cb58..76c83eab7 100644 --- a/src/server/sparkd/cmds/Benchmark.cpp +++ b/src/server/sparkd/cmds/Benchmark.cpp @@ -28,7 +28,9 @@ #include #include #include +#include #include + #include namespace bts { @@ -444,18 +446,6 @@ static void tbb_concurrent_queue(benchmark::State &state) { } BENCHMARK(tbb_concurrent_queue); -static void moody_camel_queue(benchmark::State &state) { - using Q = moodycamel::ConcurrentQueue; - Q queue(1000000); - for (auto _ : state) { - void *result; - void *arg = &queue; - queue.enqueue(arg); // NOLINT - benchmark::DoNotOptimize(queue.try_dequeue(result)); - } -} - -BENCHMARK(moody_camel_queue); static void boost_spsc(benchmark::State &state) { boost::lockfree::spsc_queue q(10000); @@ -658,6 +648,30 @@ static void same_thread_mpmc_spdlog(benchmark::State &state) { } BENCHMARK(same_thread_mpmc_spdlog); +static void moodycamel_concurrent_queue(benchmark::State &state) { + using Q = moodycamel::ConcurrentQueue; + Q queue(8); + int i = 0; + + for (auto _ : state) { + queue.enqueue(++i); // NOLINT + benchmark::DoNotOptimize(queue.try_dequeue(i)); + } +} +BENCHMARK(moodycamel_concurrent_queue); + +static void moodycamel_spsc_queue(benchmark::State &state) { + using Queue = moodycamel::ReaderWriterQueue; + Queue q1(8); + int i = 0; + + for (auto _ : state) { + q1.enqueue(++i); + benchmark::DoNotOptimize(q1.try_dequeue(i)); + } +} +BENCHMARK(moodycamel_spsc_queue); + std::string Benchmark::help() { return "Run micro-benchmarks. Use --benchmark_filter=regex to run only " "specific benchmarks."; diff --git a/src/server/sparkd/cmds/Run.cpp b/src/server/sparkd/cmds/Run.cpp index 1972c12b6..45bb356f1 100644 --- a/src/server/sparkd/cmds/Run.cpp +++ b/src/server/sparkd/cmds/Run.cpp @@ -25,8 +25,8 @@ #include "server/sparkd/support/ErrorReportService.h" #include "server/sparkd/support/ShellLatencyServer.h" #include "server/sparkd/user/AdminServiceServer.h" +#include "server/sparkd/md/MarketDataAuxiliaryServiceServer.h" #include "server/sparkd/user/UserServiceServer.h" - #include namespace bts { @@ -60,7 +60,8 @@ int Run::execute(std::vector const &) { orderman::OrderServiceServer order_service_server(sc, synthetic_service, stats, perf_log); support::ErrorReportServer error_report_service(sc); - admin::AdminServiceServer admin_service(sc); + admin::AdminServiceServer admin_service(sc); + md::auxiliary::MarketDataAuxiliaryServiceServer md_aux_server(sc); position::PositionServiceServer position_service(sc); metrics::ShellLatencyServer shell_latency_service( sc, stats, utils::Dispatcher::background()); diff --git a/src/server/sparkd/exchange_sim/ExchangeSim.cpp b/src/server/sparkd/exchange_sim/ExchangeSim.cpp index bdba0e269..621b382df 100644 --- a/src/server/sparkd/exchange_sim/ExchangeSim.cpp +++ b/src/server/sparkd/exchange_sim/ExchangeSim.cpp @@ -11,6 +11,7 @@ #include "lib/os/os.h" #include "lib/config/protos/Ports.pb.h" +#include "lib/config/protos/ExchangeSimCfg.pb.h" #include "lib/config/config.h" #include @@ -46,7 +47,8 @@ struct MDPlayer : md::IRelayWriter { MDPlayer (IMDPlayerObserver &obs,std::map &known_securities) : obs (obs), - known_securities(known_securities) + known_securities(known_securities), + forward_implied(config::get().forward_implied()) {} @@ -65,21 +67,51 @@ struct MDPlayer : md::IRelayWriter else sec_ptr = &it->second; - auto const &book = cr.book(); + auto const &book = cr.direct(); md::PriceSize bids[md::COCONUT_MAX_DEPTH], asks[md::COCONUT_MAX_DEPTH]; size_t i = 0; for (auto &b : book.bids()) - bids[i++] = md::PriceSize(b.mantissa(),b.exponent(),b.size()); + bids[i++] = md::PriceSize({b.mantissa(),(int8_t)b.exponent()},b.size()); size_t n = 0; for (auto &a : book.asks()) - asks[n++] = md::PriceSize(a.mantissa(),a.exponent(),a.size()); - obs.on_depth(*sec_ptr, bids, asks); + asks[n++] = md::PriceSize({a.mantissa(),(int8_t)a.exponent()},a.size()); + + + std::array implied_bids, implied_asks; + bool has_implied = false; + {size_t x = 0; + for (const auto& b : cr.implied().bids()) { + implied_bids[x] = md::PriceSize({b.mantissa(), (int8_t)b.exponent()}, b.size()); + x += 1; + has_implied = true; + }} + {size_t x = 0; + for (const auto& b : cr.implied().asks()) { + implied_asks[x] = md::PriceSize({b.mantissa(), (int8_t)b.exponent()}, b.size()); + x += 1; + has_implied = true; + }} + + if (has_implied && forward_implied) { + obs.on_depth(*sec_ptr, bids, asks, &implied_bids, &implied_asks); + } + else { + obs.on_depth(*sec_ptr, bids, asks, nullptr, nullptr); + } + + if (cr.has_misc()) { + if (cr.misc().has_settlement()) { + const auto& s = cr.misc().settlement(); + obs.on_settlement(*sec_ptr, { s.mantissa(), (int8_t)s.exponent() }); + } + } } } IMDPlayerObserver &obs; std::map &known_securities; + bool forward_implied; }; diff --git a/src/server/sparkd/exchange_sim/ExchangeSimApplication.cpp b/src/server/sparkd/exchange_sim/ExchangeSimApplication.cpp index 9b2c50dfa..ab08c3098 100644 --- a/src/server/sparkd/exchange_sim/ExchangeSimApplication.cpp +++ b/src/server/sparkd/exchange_sim/ExchangeSimApplication.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace bts { namespace ex_sim { @@ -454,7 +455,11 @@ void ExchangeSimApplication::update_md(std::string const &sec_desc) { persist_book(order_book); auto book = matcher.get_book(); - + // if (sec_desc == "SIM:SIZ8") { + // std::stringstream s; + // s << book; + // LOG(ExSim,Info,"{}", s.str()); + // } auto price_factor = pow(10, handle.security().display_factor); const auto cmax = md::COCONUT_MAX_DEPTH; @@ -464,7 +469,7 @@ void ExchangeSimApplication::update_md(std::string const &sec_desc) { : book.bids.end(); std::transform(book.bids.begin(), bids_end, bids_temp.begin(), [&](const auto &bs) { - return md::PriceSize::from_double<7>( + return md::PriceSize_from_double<7>( bs.first.value() * price_factor, bs.second.value()); }); auto asks_end = book.asks.size() > cmax @@ -472,16 +477,16 @@ void ExchangeSimApplication::update_md(std::string const &sec_desc) { : book.asks.end(); std::transform(book.asks.begin(), asks_end, asks_temp.begin(), [&](const auto &bs) { - return md::PriceSize::from_double<7>( + return md::PriceSize_from_double<7>( bs.first.value() * price_factor, bs.second.value()); }); - handle.update_book(utils::TimeStamp::now(), bids_temp, asks_temp,me->md_sn++); + handle.update_direct(utils::TimeStamp::now(), bids_temp, asks_temp, me->md_sn++); auto new_trades = matcher.get_new_trades(); for (auto &t : new_trades) handle.add_trade( utils::TimeStamp::now(), - md::PriceSize::from_double<7>(t.price.value() * price_factor, + md::PriceSize_from_double<7>(t.price.value() * price_factor, t.quantity.value()), datetime::ll_now(), co2::AGGRESSOR_SIDE_NONE); @@ -529,9 +534,21 @@ void ExchangeSimApplication::on_new_security(api::Security const &s) { } } +void ExchangeSimApplication::on_settlement(md::Security const& sec, md::IntPrice price) { + auto sec_desc = get_sim_sec_desc(sec); + auto iter = me->md_producer_handles.find(sec_desc); + if (iter != me->md_producer_handles.end()) { + auto &handle = iter->second; + handle.update_settlement(utils::TimeStamp::now(), price, datetime::today()); + } +} + void ExchangeSimApplication::on_depth( - md::Security const &sec, md::PriceSize bids[md::COCONUT_MAX_DEPTH], - md::PriceSize asks[md::COCONUT_MAX_DEPTH]) { + md::Security const &sec, + md::PriceSize bids[md::COCONUT_MAX_DEPTH], + md::PriceSize asks[md::COCONUT_MAX_DEPTH], + std::array* implied_bids, + std::array* implied_asks) { std::lock_guard l(me->lock); if (sec.has_synthetic()) @@ -566,6 +583,15 @@ void ExchangeSimApplication::on_depth( // we generate a set of orders that we apply to our order book merge_with_order_book(it->second.security.glbx().security_desc(), previous_md_book, current_md_book); + + // pass on implied market unchanged + if (implied_bids || implied_asks) { + auto iter = me->md_producer_handles.find(get_sim_sec_desc(sec)); + if (iter != me->md_producer_handles.end()) { + auto &handle = iter->second; + handle.update_implied(utils::TimeStamp::now(), *implied_bids, *implied_asks); + } + } } void ExchangeSimApplication::merge_with_order_book(std::string const &sec_desc, diff --git a/src/server/sparkd/exchange_sim/ExchangeSimApplication.h b/src/server/sparkd/exchange_sim/ExchangeSimApplication.h index 8f7cdc54e..248017519 100644 --- a/src/server/sparkd/exchange_sim/ExchangeSimApplication.h +++ b/src/server/sparkd/exchange_sim/ExchangeSimApplication.h @@ -99,9 +99,12 @@ public: void persist_book(OrderBook const &b); void on_new_security(md::Security const&) override; + void on_settlement(md::Security const&, md::IntPrice) override; void on_depth(md::Security const&s, md::PriceSize bids[md::COCONUT_MAX_DEPTH], - md::PriceSize asks[md::COCONUT_MAX_DEPTH]) override; + md::PriceSize asks[md::COCONUT_MAX_DEPTH], + std::array* implied_bids, + std::array* implied_asks) override; void merge_with_order_book(std::string const &sec_desc, Book const&previous,Book const¤t); }; }} diff --git a/src/server/sparkd/exchange_sim/IMDPlayerObserver.h b/src/server/sparkd/exchange_sim/IMDPlayerObserver.h index 6ba748133..b2e194b34 100644 --- a/src/server/sparkd/exchange_sim/IMDPlayerObserver.h +++ b/src/server/sparkd/exchange_sim/IMDPlayerObserver.h @@ -9,9 +9,12 @@ namespace sim { struct IMDPlayerObserver { virtual void on_new_security(md::Security const&) = 0; + virtual void on_settlement(const md::Security&, md::IntPrice) = 0; virtual void on_depth(md::Security const&s, md::PriceSize bids[md::COCONUT_MAX_DEPTH], - md::PriceSize asks[md::COCONUT_MAX_DEPTH]) = 0; + md::PriceSize asks[md::COCONUT_MAX_DEPTH], + std::array* implied_bids, + std::array* implied_asks) = 0; }; }} diff --git a/src/server/sparkd/exchange_sim/book/Book.h b/src/server/sparkd/exchange_sim/book/Book.h index b98439952..9ecf17621 100644 --- a/src/server/sparkd/exchange_sim/book/Book.h +++ b/src/server/sparkd/exchange_sim/book/Book.h @@ -1,7 +1,7 @@ #pragma once - #include "lib/domain_model/Security.pb.h" #include "lib/orderman/orderman_utils/orderman_common.h" +#include "server/sparkd/exchange_sim/book/Order.h" #include namespace bts { @@ -24,6 +24,9 @@ struct PriceCmp { PriceCmp(bool ascending) : ascending(ascending) {} bool operator()(Price p1, Price p2) const { + if (price_equals(p1.value(), p2.value())) + return false; + if (ascending) return p1 < p2; else @@ -44,7 +47,8 @@ struct Book { Quantity result(0); for (auto &kvp : bids) - if (level.value() <= kvp.first.value()) + if (price_equals(level.value(), kvp.first.value()) || + level.value() < kvp.first.value()) result.value() += kvp.second.value(); else break; @@ -53,7 +57,8 @@ struct Book { return BookOp{BookOp::ORDER, api::SIDE_BUY, level, result}; for (auto &kvp : asks) - if (level.value() >= kvp.first.value()) + if (price_equals(level.value(), kvp.first.value()) || + level.value() > kvp.first.value()) result.value() += kvp.second.value(); return BookOp{BookOp::ORDER, api::SIDE_SELL, level, result}; @@ -81,11 +86,11 @@ struct Book { return; BookSide *side; - if (op.side == api::SIDE_BUY) + if (op.side == api::SIDE_BUY) side = &bids; else side = &asks; - + Quantity *current_qty; current_qty = &(*side)[op.price]; @@ -93,9 +98,9 @@ struct Book { case BookOp::ORDER: current_qty->value() += op.quantity.value(); break; - case BookOp::CANCEL: + case BookOp::CANCEL: bts_assert(current_qty->value() >= op.quantity.value()); - current_qty->value()-=op.quantity.value(); + current_qty->value() -= op.quantity.value(); if (current_qty->value() == 0) side->erase(op.price); break; @@ -105,7 +110,8 @@ struct Book { void match() { if ((bids.size() && asks.size()) && - (bids.begin()->first.value() >= asks.begin()->first.value())) { + (price_equals(bids.begin()->first.value(),asks.begin()->first.value()) || + bids.begin()->first.value() > asks.begin()->first.value())) { auto bsz = bids.begin()->second.value(); auto asz = asks.begin()->second.value(); @@ -132,5 +138,5 @@ inline std::ostream &operator<<(std::ostream &out, const Book &book) { out << it->first.value() << "\t\t" << it->second.value() << "\n"; return out; } -} -} +} // namespace ex_sim +} // namespace bts diff --git a/src/server/sparkd/exchange_sim/book/Order.h b/src/server/sparkd/exchange_sim/book/Order.h index fd0ca0ce8..26821620c 100644 --- a/src/server/sparkd/exchange_sim/book/Order.h +++ b/src/server/sparkd/exchange_sim/book/Order.h @@ -14,18 +14,22 @@ namespace ex_sim { void execute(Order *order, Price price, Quantity quantity); +inline bool price_equals(double p1, double p2) { + return std::abs(p1-p2) <= 1e-9; +} + struct OrderCmp { OrderCmp(bool ascending) : ascending(ascending) {} bool operator()(Order const &lhs, Order const &rhs) const { if (ascending) { - if (lhs.price() != rhs.price()) + if (!price_equals(lhs.price(),rhs.price())) return lhs.price() < rhs.price(); else return utils::pb::smaller(lhs.create_time(),rhs.create_time()); } else { - if (lhs.price() != rhs.price()) + if (!price_equals(lhs.price(),rhs.price())) return lhs.price() > rhs.price(); else return utils::pb::smaller(lhs.create_time(),rhs.create_time()); diff --git a/src/server/sparkd/exchange_sim/book/OrderMatcher.cpp b/src/server/sparkd/exchange_sim/book/OrderMatcher.cpp index 5af642c97..a4a5c0977 100644 --- a/src/server/sparkd/exchange_sim/book/OrderMatcher.cpp +++ b/src/server/sparkd/exchange_sim/book/OrderMatcher.cpp @@ -60,10 +60,10 @@ OrderMatcher::find_level(OrdersSide &side, double level) for (auto it = side.begin() ; it != side.end(); it++) { - if (it->price() == level) { + if (price_equals(it->price(),level)) { result.first = it; for (;it != side.end(); it++) { - if (it->price() != level) + if (!price_equals(it->price(),level)) break; } result.second = it; diff --git a/src/server/sparkd/md/ChannelVolume.cpp b/src/server/sparkd/md/ChannelVolume.cpp new file mode 100644 index 000000000..89a6132a0 --- /dev/null +++ b/src/server/sparkd/md/ChannelVolume.cpp @@ -0,0 +1,56 @@ +#include "ChannelVolume.h" +#include "lib/api/services/MarketDataRelay.pb.h" +#include "lib/md/PriceSize.h" + +namespace bts{ +namespace md{ +namespace auxiliary{ + +using namespace bts::co2; +using namespace bts::api::md::auxiliary; + +ChannelVolume::ChannelVolume(SecurityChannel* channel) +: +_channel(channel){ +} + +const VolumeAtPriceLevel& ChannelVolume::add_trade(const TradeMessage& m, + int& record_id){ + auto price_map_it=_sec_map.find(m.key()); + if(price_map_it==_sec_map.end()){ + _sec_map[m.key()]=std::unordered_map(); + price_map_it=_sec_map.find(m.key()); + } + auto& price_map=price_map_it->second; + auto price_it=price_map.find(m.mantissa()); + if(price_it==price_map.end()){ + VolumeAtPriceLevel vol; + vol.set_record_id(record_id++); + vol.set_security_id(m.key()); + IntPrice p = md::IntPrice (m.mantissa(),(int8_t)m.exponent()); + vol.set_price(p.price()); + price_map[m.mantissa()] = vol; + price_it=price_map.find(m.mantissa()); + } + VolumeAtPriceLevel& vol=price_it->second; + vol.set_volume(vol.volume()+m.size()); + if(m.aggressor_side()==AggressorSide::AGGRESSOR_SIDE_BUY) + vol.set_buy_volume(vol.buy_volume()+m.size()); + else if(m.aggressor_side()==AggressorSide::AGGRESSOR_SIDE_SELL) + vol.set_sell_volume(vol.sell_volume()+m.size()); + else if(m.aggressor_side()==AggressorSide::AGGRESSOR_SIDE_NONE) + vol.set_passive_volume(vol.passive_volume()+m.size()); + return vol; +} + +bool ChannelVolume::match_update(const SecurityChannel& channel){ + if(_channel->type()==channel.type() && + _channel->exchange()==channel.exchange() && + _channel->symbol()==channel.symbol()){ + _channel->CopyFrom(channel); + return true; + } + return false; +} + +}}} \ No newline at end of file diff --git a/src/server/sparkd/md/ChannelVolume.h b/src/server/sparkd/md/ChannelVolume.h new file mode 100644 index 000000000..7d3bae68a --- /dev/null +++ b/src/server/sparkd/md/ChannelVolume.h @@ -0,0 +1,28 @@ +#pragma once +#include "lib/api/services/MarketDataAuxiliaryService.pb.h" +#include + +namespace bts{ +namespace co2 {class TradeMessage;class SecurityChannel;} +namespace md{ +namespace auxiliary{ + +// volume at the various price levels for all +// securities belonging to a single channel. +class ChannelVolume{ +private: + bts::co2::SecurityChannel* const _channel; + std::unordered_map> _sec_map; +public: + ChannelVolume(bts::co2::SecurityChannel* channel); + // finds the VolumeAtPriceLevel for the + // in the trade and updates the volume. if + // is not found, it increases the record_id before assigning + // it to VolumeAtPriceLevel's record_id + const bts::api::md::auxiliary::VolumeAtPriceLevel& + add_trade(const bts::co2::TradeMessage& m, int& record_id); + // if the channel is matched, it updated with it with + // the data in the arg, and returns true + bool match_update(const bts::co2::SecurityChannel& channel); +}; +}}} \ No newline at end of file diff --git a/src/server/sparkd/md/MarketDataAuxiliaryServiceServer.cpp b/src/server/sparkd/md/MarketDataAuxiliaryServiceServer.cpp new file mode 100644 index 000000000..17886e386 --- /dev/null +++ b/src/server/sparkd/md/MarketDataAuxiliaryServiceServer.cpp @@ -0,0 +1,86 @@ +#include "MarketDataAuxiliaryServiceServer.h" +#include "lib/api/services/MarketDataRelay.pb.h" +#include "lib/api/services/MarketDataRelay.client.pb.h" +#include "lib/api/public/ServiceContext.h" +#include "lib/logging/logging.h" +#include "lib/utils/PBUtils.h" +#include "lib/utils/Dispatcher.h" +#include "lib/md/get_symbols.h" +#include "VolumeAtPriceState.h" +#include "lib/utils/bts_chrono.h" +#include "lib/config/config.h" +#include "lib/config/protos/toggles.pb.h" + +namespace bts{ +namespace md{ +namespace auxiliary{ + +using namespace bts::co2; +using namespace bts::api::md::auxiliary; + +struct MarketDataAuxiliaryServiceServer::PImpl{ + bool vap_enabled; + bts::api::ServiceContext& context; + MarketDataRelay relay_service; + MarketDataAuxiliaryServiceBase::Volume_at_priceStatefulPublisher publisher; + std::unique_ptr trade_timer; + int64_t poll_period_ms; + VolumeAtPriceState state; + uint16_t log_counter, log_trunc; + PImpl(bts::api::ServiceContext& context, + const MarketDataAuxiliaryServiceBase::Volume_at_priceStatefulPublisher& publisher) + : + vap_enabled(config::get().enabled()), + context(context), + relay_service(context.locate()), + publisher(publisher), + trade_timer(), + poll_period_ms(100), + state(), + log_counter(0), + log_trunc(300){ + } +}; + +MarketDataAuxiliaryServiceServer::MarketDataAuxiliaryServiceServer( + bts::api::ServiceContext& context) +: +MarketDataAuxiliaryServiceBase(context), +me(new PImpl(context, + make_volume_at_price_stateful_publisher())){ + if(me->vap_enabled) + me->context.post([this](){_poll_md();},me->poll_period_ms); +} + +void MarketDataAuxiliaryServiceServer::_poll_md(){ + me->relay_service.requestmd(me->state.md_req(), + [this](const bts::api::Result& md_reply) + { + if(md_reply.result()!=nullptr) + _scan_trades(*md_reply.result()); + me->context.post([this](){_poll_md();},me->poll_period_ms); + }); +} + +void MarketDataAuxiliaryServiceServer::_scan_trades(const MDReply& md_reply){ + uint32_t n_trades=0, total_unique_levels=0; + bts::TimeStamp t0=bts::Timer::now(); + for(const auto& cr : md_reply.channels()) + me->state.on_new_trades(cr, + me->publisher, + n_trades, + total_unique_levels); + bts::TimeStamp t1=bts::Timer::now(); + double usecs=static_cast(us_diff (t1,t0)); + if(me->log_counter++ >= me->log_trunc){ + LOG(OrderMan,Info,"MarketDataAuxiliaryServiceServer::_scan_trades " + "trades:{} tot_unique:{} usecs:{}", + n_trades,total_unique_levels,usecs); + me->log_counter=0; + } + me->publisher.flush(); +} + +MarketDataAuxiliaryServiceServer::~MarketDataAuxiliaryServiceServer(){ +} +}}} \ No newline at end of file diff --git a/src/server/sparkd/md/MarketDataAuxiliaryServiceServer.h b/src/server/sparkd/md/MarketDataAuxiliaryServiceServer.h new file mode 100644 index 000000000..04bbd3b3e --- /dev/null +++ b/src/server/sparkd/md/MarketDataAuxiliaryServiceServer.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include "lib/api/services/MarketDataAuxiliaryService.server.pb.h" +#include "lib/utils/bts_no_copy.h" + + +namespace bts{ +namespace api {class ServiceContext;} +namespace co2 {class MDReply;} +namespace md{ +namespace auxiliary{ + +class MarketDataAuxiliaryServiceServer : +public bts::api::md::auxiliary::MarketDataAuxiliaryServiceBase +{ +private: + struct PImpl; + std::unique_ptr me; +private: + void _poll_md(); + void _scan_trades(const bts::co2::MDReply&); +public: + MarketDataAuxiliaryServiceServer(bts::api::ServiceContext& context); + ~MarketDataAuxiliaryServiceServer(); +}; +}}} diff --git a/src/server/sparkd/md/VolumeAtPriceState.cpp b/src/server/sparkd/md/VolumeAtPriceState.cpp new file mode 100644 index 000000000..45a89f92b --- /dev/null +++ b/src/server/sparkd/md/VolumeAtPriceState.cpp @@ -0,0 +1,60 @@ +#include "VolumeAtPriceState.h" +#include "lib/md/get_symbols.h" +#include "lib/api/services/MarketDataRelay.pb.h" +//#include "lib/api/services/MarketDataRelay.client.pb.h" + +namespace bts{ +namespace md{ +namespace auxiliary{ + +using namespace bts::api::md::auxiliary; +using namespace bts::co2; +using namespace std; + +VolumeAtPriceState::VolumeAtPriceState() +: +_channels(), +_md_req(), +_record_id(0){ + std::set syms; + bts::md::get_system_md_symbols(syms); + for(const auto& s : syms){ + SecurityChannel c; + c.set_flags(MDRequest::TRADE_LOG); + c.set_type(s.type()); + c.set_exchange(s.exchange()); + c.set_symbol(s.symbol()); + c.set_trade_cursor(0); + c.set_segment_id(0); + _update_or_add(c); + } +} + +ChannelVolume& VolumeAtPriceState::_update_or_add(const SecurityChannel& channel){ + for(auto& v : _channels) + if(v.match_update(channel)) + return v; + SecurityChannel* new_channel_ptr = _md_req.add_channels(); + new_channel_ptr->CopyFrom(channel); + _channels.emplace_back(new_channel_ptr); + return _channels.back(); +} + +void VolumeAtPriceState::on_new_trades(const ChannelReply& reply, +MarketDataAuxiliaryServiceBase::Volume_at_priceStatefulPublisher& publisher, + uint32_t& n_trades, + uint32_t& total_unique_levels){ + ChannelVolume& channel_vol=_update_or_add(reply.channel()); + for(const TradeMessage& t : reply.trade_log()){ + const VolumeAtPriceLevel& sec_level_vol=channel_vol.add_trade(t, + _record_id); + publisher.add_or_update(sec_level_vol); + n_trades++; + } + total_unique_levels = _record_id; +} + +const bts::co2::MDRequest& VolumeAtPriceState::md_req() const{ + return _md_req; +} +}}} diff --git a/src/server/sparkd/md/VolumeAtPriceState.h b/src/server/sparkd/md/VolumeAtPriceState.h new file mode 100644 index 000000000..195543bd2 --- /dev/null +++ b/src/server/sparkd/md/VolumeAtPriceState.h @@ -0,0 +1,32 @@ +#pragma once +#include "lib/api/services/MarketDataAuxiliaryService.server.pb.h" +#include "lib/api/services/MarketDataRelay.pb.h" +#include "ChannelVolume.h" + +namespace bts{ +namespace co2 {class SecurityChannel;class ChannelReply;} +namespace md{ +namespace auxiliary{ + +// this is the entire state for the volume at +// price levels for all channels. + +class VolumeAtPriceState{ +private: + std::vector _channels; + bts::co2::MDRequest _md_req; + int _record_id; +private: + // if the channel is present, its updated (trade_cursor etc) + // else its created from the channel + ChannelVolume& _update_or_add(const bts::co2::SecurityChannel& channel); +public: + VolumeAtPriceState (); + //encorporates the reply, and publishes it + void on_new_trades(const bts::co2::ChannelReply& channel, +bts::api::md::auxiliary::MarketDataAuxiliaryServiceBase::Volume_at_priceStatefulPublisher& publisher, + uint32_t& n_trades, + uint32_t& total_unique_levels); + const bts::co2::MDRequest& md_req() const; +}; +}}} \ No newline at end of file diff --git a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Listener.cpp b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Listener.cpp index e8468f865..de7fd2d39 100644 --- a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Listener.cpp +++ b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Listener.cpp @@ -16,9 +16,8 @@ MDP3Listener::MDP3Listener( CMESecurityIndex& index, MDProducer& producer, SecurityIDRecorder& recorder, - co2::SettlementRecorder& , - const std::vector channels, - CMEOptions::BookType bt) : + // co2::SettlementRecorder& , + const std::vector channels) : _index(index), _producer(producer), _packet_arrived(), @@ -28,7 +27,6 @@ MDP3Listener::MDP3Listener( _symbol_filter_storage(), _symbol_filter(), _channels(channels), - _booktype(bt), _concurrency_monitor(0), _book_updates() { @@ -115,15 +113,11 @@ void MDP3Listener::onEndOfPacket(onixs::Handler&, const onixs::PacketArgs&) { // Security discovery void MDP3Listener::onBookUpdate(onixs::Handler&, const onixs::Security& sec, const onixs::DirectBook& book) { - if (_booktype == CMEOptions::BOOKTYPE_DIRECT) { - onBookUpdate(sec, book); - } + onBookUpdate(sec, book, book_type_direct); } -void MDP3Listener::onBookUpdate(onixs::Handler&, const onixs::Security& sec, const onixs::ConsolidatedBook& book) { - if (_booktype == CMEOptions::BOOKTYPE_CONSOLIDATED) { - onBookUpdate(sec, book); - } +void MDP3Listener::onBookUpdate(onixs::Handler&, const onixs::Security& sec, const onixs::ImpliedBook& book) { + onBookUpdate(sec, book, book_type_implied); } // @@ -245,8 +239,21 @@ void MDP3Listener::flush_books() { // This is called at the end of the packet // Write all book updates to shared memory and reset for the next packet for (auto& entry : _book_updates) { - uint16_t sequence = (_latest_sequence & 0xFFFF); - entry.handle.update_book(_packet_arrived, entry.bids, entry.asks, sequence); + uint32_t sequence = _latest_sequence; + if (entry.type_flags == book_type_direct) { + entry.handle.update_direct(_packet_arrived, + entry.bids.first, entry.asks.first, sequence); + } + else if (entry.type_flags == book_type_implied) { + entry.handle.update_implied(_packet_arrived, + entry.bids.second, entry.asks.second, sequence); + } + else if (entry.type_flags == book_type_both) { + entry.handle.update_books(_packet_arrived, + entry.bids.first, entry.asks.first, + entry.bids.second, entry.asks.second, + sequence); + } } // Reset pending updates _book_updates.clear(); @@ -258,7 +265,7 @@ void MDP3Listener::update_all_status(CoconutSecurityStatus status) { }); } -md::MDP3Listener::BookData::BookData(MDProducerHandle & handle) : handle(handle), bids(), asks() { } +md::MDP3Listener::BookData::BookData(MDProducerHandle & handle) : handle(handle), type_flags(0), bids(), asks() { } }}//namespace diff --git a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Listener.h b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Listener.h index 50d64bc24..f0dfb43f6 100644 --- a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Listener.h +++ b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Listener.h @@ -12,22 +12,28 @@ #include #include #include +#include namespace bts { namespace md { + class MDP3Listener : public onixs::HandlerListener, public onixs::MarketDataListener, public onixs::SecurityListener { + typedef uint32_t BookType; + static constexpr BookType book_type_direct=1; + static constexpr BookType book_type_implied=2; + static constexpr BookType book_type_both=(book_type_direct|book_type_implied); + public: MDP3Listener(CMESecurityIndex& index, MDProducer&, SecurityIDRecorder&, - co2::SettlementRecorder&, - const std::vector, - CMEOptions::BookType); + // co2::SettlementRecorder&, + const std::vector); virtual ~MDP3Listener(); // @@ -71,11 +77,11 @@ public: // Book // void onBookUpdate(onixs::Handler&, const onixs::Security&, const onixs::MboBook&) override; void onBookUpdate(onixs::Handler&, const onixs::Security&, const onixs::DirectBook&) override; - // void onBookUpdate(onixs::Handler&, const onixs::Security&, const onixs::ImpliedBook&) override; - void onBookUpdate(onixs::Handler&, const onixs::Security&, const onixs::ConsolidatedBook&) override; + void onBookUpdate(onixs::Handler&, const onixs::Security&, const onixs::ImpliedBook&) override; + // void onBookUpdate(onixs::Handler&, const onixs::Security&, const onixs::ConsolidatedBook&) override; - template - void onBookUpdate(const onixs::Security& sec, const TBook& book); + template + void onBookUpdate(const onixs::Security& sec, const TBook& book, BookType booktype); // // Trades @@ -105,8 +111,18 @@ public: // Misc template void onSettlementPrice(const onixs::Security& sec, const StatisticsArgs& args); - void onSettlementPrice(onixs::Handler&, const onixs::Security& s, const onixs::DailyStatistics49Args& arg) override { onSettlementPrice(s, arg); } - void onSettlementPrice(onixs::Handler&, const onixs::Security& s, const onixs::DailyStatistics33Args& arg) override { onSettlementPrice(s, arg); } + void onSettlementPrice(onixs::Handler&, const onixs::Security& s, const onixs::DailyStatistics49Args& arg) override { + auto type = arg.entry().settlPriceType(); + if (type.finalDaily() && type.actual()) { + onSettlementPrice(s, arg); + } + } + void onSettlementPrice(onixs::Handler&, const onixs::Security& s, const onixs::DailyStatistics33Args& arg) override { + auto type = arg.entry().settlPriceType(); + if (type.finalDaily() && type.actual()) { + onSettlementPrice(s, arg); + } + } void onSettlementPrice(onixs::Handler&, const onixs::Security& s, const onixs::Recovery52EntryArgs& arg) override { onSettlementPrice(s, arg); } void onSettlementPrice(onixs::Handler&, const onixs::Security& s, const onixs::Recovery38EntryArgs& arg) override { onSettlementPrice(s, arg); } @@ -181,17 +197,16 @@ private: std::vector _symbol_filter_storage; std::set _symbol_filter; std::vector _channels; - CMEOptions::BookType _booktype; std::atomic _concurrency_monitor; struct BookData { BookData(MDProducerHandle& handle); MDProducerHandle handle; - std::array bids; - std::array asks; + BookType type_flags; + std::pair, std::array> bids; + std::pair, std::array> asks; }; std::vector _book_updates; - }; ////////////////////////////////////////// @@ -341,9 +356,8 @@ void MDP3Listener::onSpread(onixs::Handler&, const onixs::Security&, const Instr _index.add_spread_legs(fsec.numeric_id, leg_exchange_ids); } - -template -void MDP3Listener::onBookUpdate(const onixs::Security& sec, const TBook& book) { +template +void MDP3Listener::onBookUpdate(const onixs::Security& sec, const TBook& onixs_book, BookType booktype) { tracepoint (bts_trace,trace_order,(char*) "onBookUpdate", _latest_sequence, (uint16_t) _latest_sequence, 0); auto data = _index.get_security(sec.id()); @@ -361,26 +375,31 @@ void MDP3Listener::onBookUpdate(const onixs::Security& sec, const TBook& book) { _book_updates.emplace_back(data->handle); } BookData& cache = found ? *find_iter : _book_updates.back(); + cache.type_flags |= booktype; + auto& cache_bids = std::get>(cache.bids); + auto& cache_asks = std::get>(cache.asks); // Overwrite all entries every time - size_t bids_size = book.bids().size(); - size_t asks_size = book.offers().size(); + size_t bids_size = onixs_book.bids().size(); + size_t asks_size = onixs_book.offers().size(); if (!data->handle.has_depth()) { bids_size = std::min(bids_size, size_t { 1 }); asks_size = std::min(asks_size, size_t { 1 }); } for (size_t x = 0; x < bids_size; ++x) { - cache.bids[x] = pricesize_of_price_size(book.bids()[x].price(), display_factor, book.bids()[x].quantity()); + cache_bids[x] = pricesize_of_price_size(onixs_book.bids()[x].price(), display_factor, onixs_book.bids()[x].quantity()); } for (size_t x = 0; x < asks_size; ++x) { - cache.asks[x] = pricesize_of_price_size(book.offers()[x].price(), display_factor, book.offers()[x].quantity()); + cache_asks[x] = pricesize_of_price_size(onixs_book.offers()[x].price(), display_factor, onixs_book.offers()[x].quantity()); } + if (!found) { // NaN out the remaining depth - for (size_t x = bids_size; x < COCONUT_MAX_DEPTH; ++x) { - cache.bids[x] = PriceSize::NO_MARKET; - } - for (size_t x = asks_size; x < COCONUT_MAX_DEPTH; ++x) { - cache.asks[x] = PriceSize::NO_MARKET; + for (size_t x = bids_size; x < MAX_DEPTH; ++x) { + cache_bids[x] = NO_MARKET; + } + for (size_t x = asks_size; x < MAX_DEPTH; ++x) { + cache_asks[x] = NO_MARKET; + } } } @@ -434,10 +453,11 @@ void MDP3Listener::onSettlementPrice(const onixs::Security& sec, const Statistic onixs::Timestamp time; args.entry().entryPx(price); args.entry().tradingReferenceDate(time); + // args.entry(). auto display_factor = data->handle.security().display_factor; data->handle.update_settlement( utils::TimeStamp::now(), - double_of_price(price, display_factor), + convert_price(price, display_factor), date_of_timestamp(time)); } } @@ -457,16 +477,19 @@ void MDP3Listener::onOpenInterest(const onixs::Security& sec, const InterestArgs template void MDP3Listener::onSessionOpeningPrice(const onixs::Security& sec, const SessionArgs& args) { tracepoint (bts_trace,trace_order,(char*) "onOpeningPrice.session", _latest_sequence, (uint16_t) _latest_sequence, 0); - auto* data = _index.get_security(sec.id()); - if (data != nullptr) { - onixs::Decimal price = args.entry().entryPx(); - onixs::Timestamp time = args.message().transactTime(); - auto display_factor = data->handle.security().display_factor; - data->handle.update_opening( - utils::TimeStamp::now(), - double_of_price(price, display_factor), - date_of_timestamp(time)); - } + // auto type = args.entry().settlPriceType(); + // if (type.final() && type.actual()) { + auto* data = _index.get_security(sec.id()); + if (data != nullptr) { + onixs::Decimal price = args.entry().entryPx(); + onixs::Timestamp time = args.message().transactTime(); + auto display_factor = data->handle.security().display_factor; + data->handle.update_opening( + utils::TimeStamp::now(), + double_of_price(price, display_factor), + date_of_timestamp(time)); + } + // } template void MDP3Listener::onRecoveryOpeningPrice(const onixs::Security& sec, const RecoveryArgs& args) { diff --git a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3ListenerHelpers.h b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3ListenerHelpers.h index 4c3972592..5d323c5db 100644 --- a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3ListenerHelpers.h +++ b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3ListenerHelpers.h @@ -12,16 +12,22 @@ namespace bts { namespace md { inline PriceSize pricesize_of_price_size(const onixs::Decimal price, int8_t display_factor, Size size) { - PriceSize result(price.mantissa(), price.exponent() + display_factor, size); + PriceSize result({price.mantissa(), (int8_t)(price.exponent() + display_factor)}, size); return result; } -template -double double_of_price(const Price& price, int8_t display_factor) { +template +double double_of_price(const OnixPrice& price, int8_t display_factor) { // order of multiplication is important because of floating point error return (price.mantissa() * pow10(price.exponent())) * pow10(display_factor); } +template +IntPrice convert_price(const OnixsPrice& price, int8_t display_factor) { + return IntPrice(price.mantissa(), price.exponent() + display_factor); +} + + inline int floorlog10(int64_t m) { bts_assert(m >= 0); if (m < 10) return 0; diff --git a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Main.cpp b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Main.cpp index 115a23e64..4f316d553 100644 --- a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Main.cpp +++ b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Main.cpp @@ -28,7 +28,7 @@ namespace md { MDP3Main::MDP3Main() : _producer(PLATFORM_MDP3ONIXS), _security_recorder(), - _settlement_recorder(PLATFORM_MDP3ONIXS), + // _settlement_recorder(PLATFORM_MDP3ONIXS), _engines(), _handlers(), _listeners(), @@ -171,10 +171,10 @@ void MDP3Main::run(std::vector& symbols) { }); spread_resolve_timer.start(); - utils::DispatcherTimer settlement_timer(disp, 100, [&]() { - _settlement_recorder.do_work(); - }); - settlement_timer.start(); + // utils::DispatcherTimer settlement_timer(disp, 100, [&]() { + // _settlement_recorder.do_work(); + // }); + // settlement_timer.start(); utils::DispatcherTimer heartbeat_timer(disp, 250, [&]() { for (const auto& listener: _listeners) { @@ -223,12 +223,9 @@ unique_ptr MDP3Main::prepare_handler(const string& channel_id) { settings.feeds().connectivityConfigurationFile(config_path()); // Market data options - if (options.book_type() == CMEOptions::BOOKTYPE_CONSOLIDATED) { - settings.bookManagement().consolidatedBooks().maintain(true); - } - else { - settings.bookManagement().directBooks().maintain(true); - } + settings.bookManagement().directBooks().maintain(true); + settings.bookManagement().impliedBooks().maintain(true); + settings.bookManagement().consolidatedBooks().maintain(false); // 10k default is *far* too low settings.recoveryQueueMaxSize(options.recovery_queue_size()); @@ -357,7 +354,7 @@ unique_ptr MDP3Main::prepare_listener(onixs::Handler& handler, con const auto& options = channel_settings_for(channel.id); - unique_ptr listener(new MDP3Listener(_index, _producer, _security_recorder, _settlement_recorder, producer_channels, options.book_type())); + unique_ptr listener(new MDP3Listener(_index, _producer, _security_recorder, /*_settlement_recorder,*/ producer_channels)); for (auto& symbol : channel.symbols) { listener->add_filter_symbol(symbol.symbol()); } diff --git a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Main.h b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Main.h index e6059c7fd..1ebfdb99b 100644 --- a/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Main.h +++ b/src/server/sparkd/md/platforms/mdp3_onixs/MDP3Main.h @@ -40,7 +40,7 @@ private: CMESecurityIndex _index; MDProducer _producer; SecurityIDRecorder _security_recorder; - co2::SettlementRecorder _settlement_recorder; + // co2::SettlementRecorder _settlement_recorder; std::map> _engines; std::map> _handlers; std::vector> _listeners; diff --git a/src/server/sparkd/position/PositionServiceServer.cpp b/src/server/sparkd/position/PositionServiceServer.cpp index ef1eac047..274ae22b9 100644 --- a/src/server/sparkd/position/PositionServiceServer.cpp +++ b/src/server/sparkd/position/PositionServiceServer.cpp @@ -15,6 +15,9 @@ namespace bts { namespace position{ +using namespace bts::api::position; +using namespace bts::api::orderman; + struct PositionServiceServer::PImpl { PositionServiceBase::Position_dataStatefulPublisher publisher; bts::api::orderman::OrderService order_service; diff --git a/src/server/sparkd/position/PositionServiceServer.h b/src/server/sparkd/position/PositionServiceServer.h index e5b6c441a..67f7b1384 100644 --- a/src/server/sparkd/position/PositionServiceServer.h +++ b/src/server/sparkd/position/PositionServiceServer.h @@ -10,10 +10,7 @@ namespace api {class ServiceContext;} namespace api {namespace orderman {class Fill;}} namespace position{ -using namespace bts::api::position; -using namespace bts::api::orderman; - -class PositionServiceServer : public PositionServiceBase +class PositionServiceServer : public bts::api::position::PositionServiceBase { private: struct PImpl; @@ -22,11 +19,13 @@ private: // this is the function that aggrgates the // position. is_add is to indicate if a row // is added or removed - const PositionUnit& _on_fill (const Fill& fill, bool is_add); + const bts::api::position::PositionUnit& _on_fill ( + const bts::api::orderman::Fill& fill, + bool is_add); // given an account and key, finds (or adds if absent) // the PositionUnit entry - PositionUnit& _get_or_add (const std::string& acct, - uint32_t sec_id); + bts::api::position::PositionUnit& _get_or_add (const std::string& acct, + uint32_t sec_id); static void _avg_price_qty(double base_p, double base_q, double fill_p, @@ -35,7 +34,7 @@ private: const std::string& fill_id, double& avg_price, double& tot_qty); - static std::string _fill_account(const Fill& fill); + static std::string _fill_account(const bts::api::orderman::Fill& fill); public: PositionServiceServer(bts::api::ServiceContext& context); ~PositionServiceServer(); diff --git a/src/server/sparkd/user/AdminServiceServer.cpp b/src/server/sparkd/user/AdminServiceServer.cpp index 056314272..5ae3c2319 100644 --- a/src/server/sparkd/user/AdminServiceServer.cpp +++ b/src/server/sparkd/user/AdminServiceServer.cpp @@ -2,8 +2,10 @@ #include "lib/persistence/SqliteStore.h" #include "lib/logging/logging.h" +#include "lib/utils/bts_assert.h" #include "lib/utils/PBUtils.h" #include "lib/api/service_helpers/AdminServiceUtils.h" +#include "lib/orderman/orderman_utils/orderman_utils.h" #include #include @@ -22,10 +24,14 @@ AdminServiceServer::AdminServiceServer(bts::api::ServiceContext& context) _unique_key("0"), _state(), _store(new bts::persistence::SqliteStore(_db_name)), - _publisher(make_admin_data_stateful_publisher()) + _publisher(make_admin_data_stateful_publisher()), + _md_entit_publisher(make_md_entitlement_data_stateful_publisher()), + _md_perm_publisher(make_md_permissions_data_stateful_publisher()), + _order_service(context.locate()), + _order_map() { LOG(Gen, Info, "AdminServiceServer: service started"); - std::map> all_data = _store->retrieve_all(); + map> all_data = _store->retrieve_all(); auto table = all_data.find(_table_name); bool data_found = false; if (table != all_data.end()) @@ -48,52 +54,81 @@ AdminServiceServer::AdminServiceServer(bts::api::ServiceContext& context) AdminServiceUtils::fill_connections_from_config(_state); AdminServiceUtils::bootstrap_data(_state); _update_and_publish(true); - } + } + _order_service.subscribe_orders() | + rxcpp::operators::subscribe( + [this](auto const& update) { + for (const auto& o : update.removed()){ + const auto& it = _order_map.find(o->key()); + if(it != _order_map.end()) + _order_map.erase(it); + } + for (const auto& o : update.added()){ + _order_map[o->key()]=o; + } + }); } AdminServiceServer::~AdminServiceServer() {} -void AdminServiceServer::create_account(const api::admin::AccountCreateAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceCreateAccountResult>& reply){ +void AdminServiceServer::create_account( + const AccountCreateAPIArgument& request, + const api::Response& reply){ bts::api::admin::AdminServiceCreateAccountResult result; - AdminServiceUtils::create_account(_state, request.parent_account_id(), request.account_name(), request.description(), false, result); + AdminServiceUtils::create_account(_state, + request.parent_account_id(), + request.account_name(), + request.description(), + false, + result); _update_and_publish(false); reply(result); } -void AdminServiceServer::update_account(const api::admin::AccountUpdateAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceOperationResult>& reply){ +void AdminServiceServer::update_account( + const AccountUpdateAPIArgument& request, + const api::Response& reply){ bts::api::admin::AdminServiceOperationResult result; AdminServiceUtils::update_account(_state, request.complete_data(), result); _update_and_publish(false); reply(result); } -void AdminServiceServer::create_user(const api::admin::UserCreateAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceCreateEntityResult>& reply){ - bts::api::admin::AdminServiceCreateEntityResult result; - AdminServiceUtils::create_user(_state, request.parent_group_id(), request.name(), request.password(), false, - request.license_data(), request.license_entitlements(), result); +void AdminServiceServer::create_user( + const UserCreateAPIArgument& request, + const api::Response& reply){ + AdminServiceCreateEntityResult result; + AdminServiceUtils::create_user(_state, + request.parent_group_id(), + request.name(), + request.password(), + false, + request.license_data(), + request.license_entitlements(), + result); _update_and_publish(false); reply(result); } -void AdminServiceServer::update_user(const api::admin::UserUpdateAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceOperationResult>& reply){ - bts::api::admin::AdminServiceOperationResult result; +void AdminServiceServer::update_user( + const UserUpdateAPIArgument& request, + const api::Response& reply){ + AdminServiceOperationResult result; AdminServiceUtils::update_user(_state, request.complete_data(), result); _update_and_publish(false); reply(result); } void AdminServiceServer::get_admin_data(const bts::api::VoidMessage&, - const api::Response<::bts::api::admin::AdminData>& reply){ - reply(_state); + const api::Response& reply){ + reply(_state); } -void AdminServiceServer::delete_user(const bts::api::admin::IDAPIArgument& id, - const api::Response& reply){ - bts::api::admin::AdminServiceOperationResult result; result.set_is_success(true); - bts::admin::AdminServiceUtils::delete_user(_state, id, result); +void AdminServiceServer::delete_user( + const IDAPIArgument& id, + const api::Response& reply){ + AdminServiceOperationResult result; result.set_is_success(true); + AdminServiceUtils::delete_user(_state, id, result); _update_and_publish(false); reply(result); } -void AdminServiceServer::delete_account(const bts::api::admin::IDAPIArgument& id, - const api::Response& reply){ - bts::api::admin::AdminServiceOperationResult result; result.set_is_success(true); +void AdminServiceServer::delete_account( + const IDAPIArgument& id, + const api::Response& reply){ + AdminServiceOperationResult result; result.set_is_success(true); bts::admin::AdminServiceUtils::delete_account(_state, id, result); _update_and_publish(false); reply(result); @@ -109,8 +144,106 @@ void AdminServiceServer::_update_and_publish(bool is_insert){ _store->update(_table_name, _unique_key, ser_state); _publisher.add_or_update(_state); _publisher.flush(); + _publish_md_entitlements(); + _publish_md_permissions(); } else LOG(Gen, Error, "Could not serialize admin data, saving failed"); + +} +void AdminServiceServer::get_order_summary( + const bts::api::VoidMessage&, + const api::Response& reply){ + std::map> agg; + for(auto it=_order_map.begin();it!=_order_map.end();it++){ + const OrderRecord& ord=*(it->second); + if(bts::orderman::order_in_working_state(ord.state().status()) && + bts::orderman::is_parent_order(ord)) + _add_live_order(agg,ord); + } + OrdersSummary rv; + for (auto acct_it=agg.begin();acct_it!=agg.end();acct_it++){ + const std::map& user_map=acct_it->second; + for(auto user_it=user_map.begin();user_it!=user_map.end();user_it++){ + const OrdersSummaryUnit& u=user_it->second; + OrdersSummaryUnit* nv=rv.add_units(); + nv->CopyFrom(u); + } + } + reply(rv); +} +void AdminServiceServer::cancel_orders( + const OrderCancelRequest& req, + const api::Response&){ + vector accts, users; + for(auto it=req.accounts().begin();it!=req.accounts().end();it++) + accts.push_back(*it); + for(auto it=req.users().begin();it!=req.users().end();it++) + users.push_back(*it); + _cancel_orders(accts,users); +} + +void AdminServiceServer::_add_live_order( + map>& agg, + const OrderRecord& ord){ + const std::string& acct=ord.def().account(); + const std::string& user=ord.def().user(); + auto acct_it=agg.find(acct); + if(acct_it==agg.end()) + agg[acct]=std::map(); + acct_it=agg.find(acct); + bts_assert(acct_it!=agg.end()); + std::map& user_map=acct_it->second; + auto user_it=user_map.find(user); + if(user_it==user_map.end()){ + OrdersSummaryUnit nu; + nu.set_account(acct); + nu.set_user(user); + user_map[user]=nu; + } + user_it=user_map.find(user); + bts_assert(user_it!=user_map.end()); + OrdersSummaryUnit& u=user_it->second; + u.set_total_orders(u.total_orders()+1); + u.set_total_contracts(u.total_contracts()+ord.state().quantity()); +} +void AdminServiceServer::_cancel_orders(const vector& accounts, + const vector& users){ + map acct_map, user_map; + vector keys; + for(const std::string& a : accounts) + acct_map[a]=true; + for(const std::string& u : users) + user_map[u]=true; + for(auto it=_order_map.begin();it!=_order_map.end();it++){ + const OrderRecord& ord=*(it->second); + if(bts::orderman::order_in_working_state(ord.state().status()) && + bts::orderman::is_parent_order(ord) && + acct_map.find(ord.def().account()) != acct_map.end() && + user_map.find(ord.def().user()) != user_map.end()) + keys.push_back(ord.key()); + } + auto cb = [&](bts::api::Result<::bts::api::orderman::Response>) -> void {}; + for(uint32_t k : keys){ + Cancel c; + c.set_order_key(k); + _order_service.cancel_order(c, cb); + } +} +void AdminServiceServer::_publish_md_entitlements(){ + // TODO, need to delete all state + std::vector entitlements; + AdminServiceUtils::get_md_entitlement_report(_state,entitlements); + for (const auto& e : entitlements) + _md_entit_publisher.add_or_update(e); + _md_entit_publisher.flush(); +} +void AdminServiceServer::_publish_md_permissions(){ + std::vector perms; + for (const BTSUser* user : AdminServiceUtils::users(_state,false)) + AdminServiceUtils::get_user_symbol_permissions(_state,*user,perms); + for (const auto& p : perms) + _md_perm_publisher.add_or_update(p); + _md_perm_publisher.flush(); } }} diff --git a/src/server/sparkd/user/AdminServiceServer.h b/src/server/sparkd/user/AdminServiceServer.h index 1fb0adf12..32bbe126c 100644 --- a/src/server/sparkd/user/AdminServiceServer.h +++ b/src/server/sparkd/user/AdminServiceServer.h @@ -5,10 +5,16 @@ #include "lib/utils/bts_no_copy.h" #include "lib/persistence/IStore.h" #include "lib/api/public/ServiceContext.h" +#include "lib/api/services/OrderService.pb.h" +#include "lib/api/services/OrderService.client.pb.h" namespace bts { namespace admin{ +using namespace bts::api::orderman; +using namespace bts::api::admin; +using namespace std; + class AdminServiceServer : public api::admin::AdminServiceBase { private: @@ -17,33 +23,57 @@ private: const std::string _unique_key; bts::api::admin::AdminData _state; std::unique_ptr _store; - bts::api::admin::AdminServiceBase::Admin_dataStatefulPublisher _publisher; + AdminServiceBase::Admin_dataStatefulPublisher _publisher; + AdminServiceBase::Md_entitlement_dataStatefulPublisher _md_entit_publisher; + AdminServiceBase::Md_permissions_dataStatefulPublisher _md_perm_publisher; + OrderService _order_service; + map> _order_map; public: //BTS_NO_COPY(UserModelServiceServer); AdminServiceServer(bts::api::ServiceContext& context); ~AdminServiceServer(); - void create_account(const api::admin::AccountCreateAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceCreateAccountResult>& reply) override; + void create_account( + const AccountCreateAPIArgument& request, + const api::Response& reply) override; - void update_account(const api::admin::AccountUpdateAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceOperationResult>& reply) override; + void update_account( + const AccountUpdateAPIArgument& request, + const api::Response& reply) override; - void create_user(const api::admin::UserCreateAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceCreateEntityResult>& reply) override; + void create_user( + const UserCreateAPIArgument& request, + const api::Response& reply) override; - void update_user(const api::admin::UserUpdateAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceOperationResult>& reply) override; + void update_user( + const UserUpdateAPIArgument& request, + const api::Response& reply) override; void get_admin_data(const bts::api::VoidMessage& request, - const api::Response<::bts::api::admin::AdminData>& reply) override; + const api::Response& reply) override; - void delete_user(const bts::api::admin::IDAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceOperationResult>& reply) override; + void delete_user( + const IDAPIArgument& request, + const api::Response& reply) override; + + void delete_account( + const IDAPIArgument& request, + const api::Response& reply) override; + + void get_order_summary(const bts::api::VoidMessage& request, + const api::Response& reply) override; + + void cancel_orders( + const OrderCancelRequest& request, + const api::Response& reply) override; - void delete_account(const bts::api::admin::IDAPIArgument& request, - const api::Response<::bts::api::admin::AdminServiceOperationResult>& reply) override; private: void _update_and_publish (bool is_insert); + void _publish_md_entitlements(); + void _publish_md_permissions(); + void _add_live_order(map>& agg, + const OrderRecord& order); + void _cancel_orders(const std::vector& accounts, + const std::vector& users); }; }} diff --git a/src/web/CMakeDeps.cmake b/src/web/CMakeDeps.cmake index 21fe04d3f..050dc8062 100644 --- a/src/web/CMakeDeps.cmake +++ b/src/web/CMakeDeps.cmake @@ -1 +1 @@ -set(sources ../../externals/linux/include/google/protobuf/descriptor.proto;../../externals/linux/include/google/protobuf/timestamp.proto;../lib/api/apicore.proto;../lib/api/apigateway.proto;../lib/api/apioptions.proto;../lib/api/services/AdminService.proto;../lib/api/services/ApiUnitTestService.proto;../lib/api/services/ErrorReportService.proto;../lib/api/services/HelloService.proto;../lib/api/services/MarketDataRelay.proto;../lib/api/services/OrderService.proto;../lib/api/services/OutOfProcessURLListener.proto;../lib/api/services/PersistenceService.proto;../lib/api/services/PositionService.proto;../lib/api/services/ShellDiagnosticService.proto;../lib/api/services/ShellLatencyService.proto;../lib/api/services/SyntheticSecurityService.proto;../lib/api/services/UserService.proto;../lib/domain_model/Security.proto;../lib/md/protos/co2.proto;../lib/sparkproto/SparkProtos.proto;api/build.sh;api/node_modules/cssstyle/scripts/run_tests.sh;api/node_modules/exit/test/fixtures/create-files.sh;api/node_modules/protobufjs/cli/node_modules/acorn-jsx/node_modules/acorn/bin/update_authors.sh;api/node_modules/watch/release.sh;api/quiet_build.sh;api/src/DynamicService.ts;api/src/ServiceContext.ts;api/src/bts_assert.ts;ui/src/Access.fs;ui/src/Accounts/ClipLimits.fs;ui/src/Accounts/ProductLimits.fs;ui/src/Accounts/SecurityLimits.fs;ui/src/Accounts/State.fs;ui/src/Accounts/Types.fs;ui/src/Accounts/View.fs;ui/src/App.fs;ui/src/Cmd.fs;ui/src/Controls/Button.fs;ui/src/Controls/DataGrid.fs;ui/src/Controls/Fields.fs;ui/src/Controls/Modal.fs;ui/src/Controls/ReactGrid.fs;ui/src/Controls/Table.fs;ui/src/Controls/nbsp.fs;ui/src/Global.fs;ui/src/Imports.fs;ui/src/Lens.fs;ui/src/Login/Login.fs;ui/src/Nav.fs;ui/src/State.fs;ui/src/Types.fs;ui/src/Users/State.fs;ui/src/Users/Types.fs;ui/src/Users/View.fs;ui/src/Util/EnumUtils.fs;ui/src/Util/JSMap.fs) \ No newline at end of file +set(sources ../../externals/linux/include/google/protobuf/descriptor.proto;../../externals/linux/include/google/protobuf/timestamp.proto;../lib/api/apicore.proto;../lib/api/apigateway.proto;../lib/api/apioptions.proto;../lib/api/services/AdminService.proto;../lib/api/services/ApiUnitTestService.proto;../lib/api/services/ErrorReportService.proto;../lib/api/services/HelloService.proto;../lib/api/services/MarketDataAuxiliaryService.proto;../lib/api/services/MarketDataRelay.proto;../lib/api/services/OrderService.proto;../lib/api/services/OutOfProcessURLListener.proto;../lib/api/services/PersistenceService.proto;../lib/api/services/PositionService.proto;../lib/api/services/ShellDiagnosticService.proto;../lib/api/services/ShellLatencyService.proto;../lib/api/services/SyntheticSecurityService.proto;../lib/api/services/UserService.proto;../lib/domain_model/Security.proto;../lib/md/protos/co2.proto;../lib/sparkproto/SparkProtos.proto;api/build.sh;api/node_modules/cssstyle/scripts/run_tests.sh;api/node_modules/exit/test/fixtures/create-files.sh;api/node_modules/protobufjs/cli/node_modules/acorn-jsx/node_modules/acorn/bin/update_authors.sh;api/node_modules/watch/release.sh;api/quiet_build.sh;api/src/DynamicService.ts;api/src/ServiceContext.ts;api/src/bts_assert.ts;ui/src/Access.fs;ui/src/Accounts/ClipLimits.fs;ui/src/Accounts/ProductLimits.fs;ui/src/Accounts/SecurityLimits.fs;ui/src/Accounts/State.fs;ui/src/Accounts/Types.fs;ui/src/Accounts/View.fs;ui/src/App.fs;ui/src/Cmd.fs;ui/src/Controls/Button.fs;ui/src/Controls/DataGrid.fs;ui/src/Controls/Fields.fs;ui/src/Controls/Modal.fs;ui/src/Controls/ReactGrid.fs;ui/src/Controls/Table.fs;ui/src/Controls/nbsp.fs;ui/src/Global.fs;ui/src/Imports.fs;ui/src/Lens.fs;ui/src/Login/Login.fs;ui/src/Nav.fs;ui/src/State.fs;ui/src/Types.fs;ui/src/Users/State.fs;ui/src/Users/Types.fs;ui/src/Users/View.fs;ui/src/Util/EnumUtils.fs;ui/src/Util/JSMap.fs) \ No newline at end of file diff --git a/src/web/ui/src/Access.fs b/src/web/ui/src/Access.fs index 840de8fd8..9d3ba6411 100644 --- a/src/web/ui/src/Access.fs +++ b/src/web/ui/src/Access.fs @@ -12,6 +12,14 @@ let getAdminData (admin:IAdminService) (fsuc) (ffail) = fsuc (fun e -> ffail e.Message) +let getAccountOrdersData (admin:IAdminService) (success) (fail) = + Cmd.ofPromise + admin.get_order_summary + () + success + (fun e -> fail e.Message) + + module Lens = // type Setter<'a,'b> = { type Setter<'a,'b> = 'b -> 'a -> 'a diff --git a/src/web/ui/src/Accounts/State.fs b/src/web/ui/src/Accounts/State.fs index f297d27cd..e1d11ae06 100644 --- a/src/web/ui/src/Accounts/State.fs +++ b/src/web/ui/src/Accounts/State.fs @@ -19,6 +19,8 @@ let init (context:ServiceContext.ServiceContext) = { tab = General admin = admin adminData = None + ordersData = [] + SelectedOrders = [] mdRelay = mdr mdRelayProductData = [||] mdRelayExpNameData = Array.empty @@ -94,9 +96,19 @@ let deleteAccount (admin:IAdminService) (account:BTSAccount) = promise { return ErrorOccured (sprintf "Error deleting account: %s" result.errorText) } +let cancelOrders (admin:IAdminService) (accountName:string) (orders:OrdersSummaryUnit list) = promise { + let users = (Array.map (fun (ord:OrdersSummaryUnit) -> ord.user) (Array.ofList orders)) + let accounts = [| accountName |] + let! result = admin.cancel_orders {accounts = accounts; users = users } + + let! data = admin.get_admin_data() + return UpdateAdminData data +} + + let requestProducts (mdRelay:IMarketDataRelay) = promise { - let! result = mdRelay.request_products() - return UpdateProducts result.products + let! result = mdRelay.request_products() + return UpdateProducts result.products } let requestExpNames (mdRelay:IMarketDataRelay) = promise { @@ -115,6 +127,10 @@ let clearSelection model = SelectedPositionLimits = [] } +let orderIsEqual (order1:OrdersSummaryUnit) (order2:OrdersSummaryUnit) = + (order1.account = order2.account) + && (order1.user = order2.user) + let ClipLimitIsEqual (limit1:AccountSecurityClipLimitUnit) (limit2:AccountSecurityClipLimitUnit) = (limit1.security.exchange = limit2.security.exchange) && (limit1.security.securityType = limit2.security.securityType) @@ -151,6 +167,19 @@ let update msg (model:Model) : Model * Cmd = | NoAdminData msg -> Global.alert Global.Info msg model, Cmd.Empty + | GetOrdersData -> + model, (Access.getAccountOrdersData model.admin UpdateOrdersData (box>>ErrorOccured)) + | UpdateOrdersData summary -> + {model with ordersData = List.ofArray summary.units}, Cmd.Empty + | CancelOrders orders -> + model, Cmd.ofExistingPromise (cancelOrders model.admin model.scratch.name orders) id (box>>ErrorOccured) + | SelectOrder (chckd, orders) -> + let so = match chckd, orders.Length > 1 with + | true, false -> (orders.[0]::model.SelectedOrders) + | false, false -> List.filter (fun elm -> (orderIsEqual elm orders.[0]) |> not) model.SelectedOrders + | true, true -> orders + | false, true -> [] + {model with SelectedOrders = so}, Cmd.Empty | SelectAccount account -> { (clearSelection model) with previousScratch = model.scratch; scratch = account }, Cmd.Empty | DeleteAccount account -> @@ -170,7 +199,11 @@ let update msg (model:Model) : Model * Cmd = Browser.console.log msg model,Cmd.Empty | SelectTab tab -> - { (clearSelection model) with tab = tab }, Cmd.Empty + match tab with + | OrdersTab -> + { (clearSelection model) with tab = tab }, Cmd.ofMsg GetOrdersData + | _ -> + { (clearSelection model) with tab = tab }, Cmd.Empty | Update field -> { model with scratch = applyUpdate model.scratch field }, Cmd.Empty | ClipLimits (oldLimit, newLimit) -> diff --git a/src/web/ui/src/Accounts/Types.fs b/src/web/ui/src/Accounts/Types.fs index 63f87d71a..402f009e8 100644 --- a/src/web/ui/src/Accounts/Types.fs +++ b/src/web/ui/src/Accounts/Types.fs @@ -12,6 +12,7 @@ type Tab = | CreditLimitsTab | ExchangeSettingsTab | ConnectionsTab + | OrdersTab let venues = Venue.values |> Array.skip 1 let securityTypes = SecurityType.values |> Array.skip 1 @@ -42,6 +43,10 @@ type SIMExchangeField = type Msg = | UpdateAdminData of AdminData + | GetOrdersData + | UpdateOrdersData of OrdersSummary + | SelectOrder of bool * OrdersSummaryUnit list + | CancelOrders of OrdersSummaryUnit list | NoAdminData of string | SelectAccount of BTSAccount | DeleteAccount of BTSAccount @@ -84,6 +89,8 @@ type Model = { tab : Tab admin : IAdminService adminData : AdminData option + ordersData : OrdersSummaryUnit list + SelectedOrders: OrdersSummaryUnit list mdRelay : IMarketDataRelay mdRelayProductData: Product[] mdRelayExpNameData: ExpirationNameList[] diff --git a/src/web/ui/src/Accounts/View.fs b/src/web/ui/src/Accounts/View.fs index 95fa32a32..ed74b8a34 100644 --- a/src/web/ui/src/Accounts/View.fs +++ b/src/web/ui/src/Accounts/View.fs @@ -32,6 +32,18 @@ let saveButton model dispatch = let enabled = if dirty then Button.Enabled else Button.Disabled let flavor = if dirty then "is-success" else "" Button.button buttonText flavor "" enabled (fun _ -> dispatch SaveScratch) + +let accountControls model (dispatch:Msg->unit) = + let revertFun = + match (selectedAccount model) with + | Some acc -> fun _ -> (dispatch <| SelectAccount acc) + | None -> ignore + + div [ClassName "is-main-control-bar"] [ + saveButton model dispatch + Button.button "Revert" "" "" (if isDirty model then Button.Enabled else Button.Disabled) revertFun + ] + // // General // @@ -61,6 +73,8 @@ let generalView model dispatch = Fields.text "Description" account.description [] (Description >> Update >> dispatch) Fields.checkbox "Can Trade Into Account" "Unchecking this stops all users from entering orders into the account" account.isActive [] (IsActive >> Update >> dispatch) (if State.isNewAccount account then modal else span[][]) + + accountControls model dispatch ] let GetSymbolArray (model:Model) = @@ -74,7 +88,50 @@ let GetFilteredSymbolArray (model:Model) (remove:string []) = |> Array.distinct) [ [|model.AllValueDisplay|]; a ] |> Array.concat -let limitFilter (positionLimit:AccountSecurityPositionLimitUnit option) (clipLimit:AccountSecurityClipLimitUnit option) allValueDisplay (array:'a []) = + +let ordersView (model:Model) dispatch = + let ordersList = List.filter (fun (elm:OrdersSummaryUnit) -> elm.account = model.scratch.name) model.ordersData + + let headerRow:TableControl.row = + { + cells = + [ + TableControl.controlCell (Fields.checkbox "" "" + (model.SelectedOrders.Length = ordersList.Length) + [] + (fun chckd -> ((chckd, ordersList) |> SelectOrder |> dispatch))) + TableControl.Cell (str "User") + TableControl.Cell (str "Total Contracts") + TableControl.Cell (str "Total Orders") + ] + className = "" + header = None + } + + let rws:TableControl.row list = + [ for od in ordersList do yield { + cells = + [ + TableControl.controlCell (Fields.checkbox "" "" + (List.exists (fun (elm:OrdersSummaryUnit) -> State.orderIsEqual elm od) model.SelectedOrders) + [] + (fun chckd -> ((chckd, [od]) |> SelectOrder |> dispatch))) + TableControl.Cell (str od.user) + TableControl.Cell (str (sprintf "%i" od.totalContracts)) + TableControl.Cell (str (sprintf "%i" od.totalOrders)) + ] + className = "" + header = None + } + ] + + let cancelButton = Button.button "Cancel Selected Orders" "is-danger" "times" Button.Enabled (fun _ -> model.SelectedOrders |> CancelOrders |> dispatch) + + div [][ + TableControl.createDataTable (Some headerRow) rws cancelButton + ] + +let limitFilter (positionLimit:AccountSecurityPositionLimitUnit option) (clipLimit:AccountSecurityClipLimitUnit option) allValueDisplay (array:ExpirationNameList []) = match positionLimit, clipLimit with | Some limit, None -> Array.filter (fun (x:bts.co2.ExpirationNameList) -> ( @@ -581,6 +638,8 @@ let LimitsView model dispatch = clipLimitsView model dispatch productLimits model dispatch //securityLimitsView model dispatch + + accountControls model dispatch ] let ConnectionsView (model:Accounts.Types.Model) dispatch = @@ -639,6 +698,8 @@ let ConnectionsView (model:Accounts.Types.Model) dispatch = div [ ] [ TableControl.createSelectorTable (Some headerRow) rows + + accountControls model dispatch ] let creditLimitsView model dispatch = div [] [str "lorem ipsum"] @@ -663,19 +724,9 @@ let exchangeSettings model dispatch = Fields.enumSelect "Origin" (toHuman false CustomerOrFirm.values sim.origin) (allToHuman false CustomerOrFirm.values) [] (fromHuman CustomerOrFirm.values>>SIMExchangeField.Origin>>sendSim) Fields.text "Clearing Account" sim.clearingAccount [MaxLength 12.0] (SIMExchangeField.ClearingAccount>>sendSim) + accountControls model dispatch ] -let accountControls model (dispatch:Msg->unit) = - let revertFun = - match (selectedAccount model) with - | Some acc -> fun _ -> (dispatch <| SelectAccount acc) - | None -> ignore - - div [ClassName "is-main-control-bar"] [ - saveButton model dispatch - Button.button "Revert" "" "" (if isDirty model then Button.Enabled else Button.Disabled) revertFun - ] - let tabContent model dispatch = div [Key (string model.scratch.id); ClassName "is-tab-content"] [ (match model.tab with @@ -683,9 +734,8 @@ let tabContent model dispatch = | LimitsTab -> LimitsView model dispatch | ConnectionsTab -> ConnectionsView model dispatch | CreditLimitsTab -> creditLimitsView model dispatch - | ExchangeSettingsTab -> exchangeSettings model dispatch) - - accountControls model dispatch + | ExchangeSettingsTab -> exchangeSettings model dispatch + | OrdersTab -> ordersView model dispatch) ] let isActive account = function @@ -834,6 +884,7 @@ let tabName = function | CreditLimitsTab -> "Credit Limits" | ConnectionsTab -> "Connections" | ExchangeSettingsTab -> "Exchange Settings" + | OrdersTab -> "Orders" | other -> string other let tabHeader tab currentTab dispatch = @@ -866,6 +917,7 @@ let root (model:Model) dispatch = tabHeader ConnectionsTab model.tab dispatch // tabHeader CreditLimits model.tab dispatch tabHeader ExchangeSettingsTab model.tab dispatch + tabHeader OrdersTab model.tab dispatch ] ] tabContent model dispatch diff --git a/src/web/ui/src/Login/Login.fs b/src/web/ui/src/Login/Login.fs index b8a2adeb2..20d9d49e9 100644 --- a/src/web/ui/src/Login/Login.fs +++ b/src/web/ui/src/Login/Login.fs @@ -34,14 +34,14 @@ let loginMsg user pass = (function | U2.Case1 sc -> LoginSuccess sc | U2.Case2 str -> LoginFailure str) (sprintf "Login exception: %A" >> LoginFailure) -let update msg model = +let update msg model = match msg with | Username u -> { model with user = u.ToLower(); error = "" }, Cmd.Empty | Password p -> { model with password = p; error = "" }, Cmd.Empty - | Help -> + | Help -> Global.alert Global.Info "Call support" model, Cmd.Empty - | Submit -> + | Submit -> printfn "Login with %s %s" model.user model.password let isAdmin = ((model.user.ToLower().Contains "bts") || (model.user.ToLower().Contains "admin")) match isAdmin with @@ -58,37 +58,36 @@ let root model dispatch = let sparkImg = "img/BTS-300.png" let handler msg (ev:Fable.Import.React.FormEvent) = !!ev.target?value |> msg |> dispatch - section [ ClassName "hero is-fullheight is-primary" ] [ - div [ ClassName "hero-body" ] [ - div [ ClassName "container has-text-centered" ] [ - div [ ClassName "column is-4 is-offset-4" ] [ - div [ ClassName "box" ] [ + section [ ClassName "hero is-fullheight is-primary" ] [ + div [ ClassName "hero-body" ] [ + div [ ClassName "container has-text-centered" ] [ + div [ ClassName "column is-4 is-offset-4" ] [ + div [ ClassName "box" ] [ p [ Style [Margin "2em 0 3em 0"] ][ - figure [ ClassName "avatar" ] [ - img [ Src sparkImg ] + figure [ ClassName "avatar" ] [ + img [ Src sparkImg ] ] - h1 [ Style [ Float "right"; FontSize "1.2rem"; Margin "1.6rem .5rem 0 -1rem"; Transform "rotate(90deg)"] ] [ str "beta" ] h1 [ ClassName "is-size-1" ] [ span [] [str "Spark Admin"]] ] - form [ OnSubmit (fun e -> - e.preventDefault() - dispatch Submit) ] [ - div [ ClassName "field" ] [ - div [ ClassName "control" ] [ + form [ OnSubmit (fun e -> + e.preventDefault() + dispatch Submit) ] [ + div [ ClassName "field" ] [ + div [ ClassName "control" ] [ input [ ClassName "input login-input" Type "text" OnChange (handler Username) AutoFocus true Placeholder "Username" ] ] ] - div [ ClassName "field" ] [ - div [ ClassName "control" ] [ + div [ ClassName "field" ] [ + div [ ClassName "control" ] [ input [ ClassName "input login-input" Type "password" OnChange (handler Password) Placeholder "Password" ] ] ] div [ Style [Color "red"] ] [str model.error] br [] - button [ClassName "button is-block is-primary is-fullwidth" ] [ + button [ClassName "button is-block is-primary is-fullwidth" ] [ str "Login" ] ] ] ] ] ] ] diff --git a/src/web/ui/src/Users/State.fs b/src/web/ui/src/Users/State.fs index dc61a1346..f2d8dd611 100644 --- a/src/web/ui/src/Users/State.fs +++ b/src/web/ui/src/Users/State.fs @@ -62,6 +62,7 @@ let defaultScratchUser = { smpInstruction = "" maxOrderRatePerConnection = "" location = "" + mdPermissions = [||] } simData = { operatorId = "" diff --git a/testing/manual/admin/user_account_permissions.txt b/testing/manual/admin/user_account_permissions.txt new file mode 100644 index 000000000..c490f52fb --- /dev/null +++ b/testing/manual/admin/user_account_permissions.txt @@ -0,0 +1,14 @@ +This test verifies that users can only see trades that are in accounts for which they're permissioned + +1. Start a client and the admin gui + +2. For your user, add permissions to see all accounts in the system in the admin gui + +3. Enter some orders in the client for those accounts (you may have to add limits, etc), make some orders fill, cancel some orders + +4. Now close the client, and in the admin UI pick another user and remove trading access to the account you just traded into. Start the client again with the user you just de-permissioned. You should not be able to see any of the trades you just made + +5. Next re-permission your current user for the accounts with trades in them and the should show up in the UI. + +6. Start another client with -multiple with the original user and make sure you can still see all trades. + diff --git a/testing/manual/forms/implied_markets_ui.txt b/testing/manual/forms/implied_markets_ui.txt new file mode 100644 index 000000000..8970b116b --- /dev/null +++ b/testing/manual/forms/implied_markets_ui.txt @@ -0,0 +1,23 @@ +0. This test should run in sim market with merged turned on + +ExchangeSim {value {merge_with_md:true}} + +1. Bring up a market view for SI futures. There are 3 quantity columns +a. Implied : This is the quantity coming purely from implieds (ex for the spreads, this is +'implied' from the legs) +b. Direct : This comes purely from markets in the security itself +c. Qty (or Merged Qty) == Implied + Direct. + +2. Select a future with only implied markets (SI should have lots of them). Verify that the ladder +agrees with the market view (the ladder shows merged by default). + + +3. Enter buy and sell orders away from the market and make sure they show up as direct market in the ladder. + +4. In the ladder settings -> Market Data, select Direct Only. Make sure the ladder does the right thing. + +5. Go back to Merged and make sure the Ladder does the right thing. + +6. Make you direct bid better than the implied bid (by moving your buy order) and make sure MView does the right thing. + +7. Repeat 6 for sell/ask. \ No newline at end of file diff --git a/tools/cpp_helper/Generate-Cpps-reqs.ps1 b/tools/cpp_helper/Generate-Cpps-reqs.ps1 index 07d324706..d0f3c03ec 100644 --- a/tools/cpp_helper/Generate-Cpps-reqs.ps1 +++ b/tools/cpp_helper/Generate-Cpps-reqs.ps1 @@ -1,3 +1,15 @@ +# This should mirror the one found in Security.proto +$cme_mics = @{ + "CME" = 1 + "NYMEX" = 2 + "CBT" = 3 + "COMEX" = 4 + "MGE" = 5 + "DME" = 6 + "NYMSW" = 7 + "FXS" = 8 +} + $known_good = @{ "IC" = 50 "IH" = 50 diff --git a/tools/cpp_helper/Generate-Cpps.ps1 b/tools/cpp_helper/Generate-Cpps.ps1 index a7a71a1fe..1d0726610 100644 --- a/tools/cpp_helper/Generate-Cpps.ps1 +++ b/tools/cpp_helper/Generate-Cpps.ps1 @@ -3,7 +3,7 @@ $cme_file_url = "ftp://ftp.cmegroup.com/pub/fprf/csv/cmeg.fut.prf.csv" $csv_save_path = ".\cmeg.fut.prf.csv" -$output_file = ".\cpp_config.conf" +$output_file = ".\cme_reference_data.conf" # turn off console update progress while downloading $CurrentProgressPref = $ProgressPreference; @@ -13,11 +13,18 @@ $ProgressPreference = $CurrentProgressPref $a = Import-Csv $csv_save_path $b = $a | select Sym, Mult | Group Sym | Sort Name +$mic_grouping = $a | select Sym, Exch | Group Sym | Sort Name +# Automated header disclaimer @" -# Generated by automated script. DO NOT EDIT! +# +# Entire file generated by automated script. DO NOT EDIT! # source: tools/cpp_helper/generate-cpps.ps1 +# + "@ | out-file -Encoding ascii $output_file + +# cpps "CppConfig`n{`n`tvalue`n`t{" | out-file -Encoding ascii $output_file -Append $b | % { if ($known_good.ContainsKey($_.Group[0].Sym)) { @@ -27,9 +34,18 @@ $b | % { } "`t`tcpp_entry {{ key: `"{0}`" value: {1} }}" -f $_.Group[0].Sym, $cpp } ` | out-file -Encoding ascii $output_file -Append +"`t}`n}`n" | out-file -Encoding ascii $output_file -Append + +# mics +"CmeSymbolMics`n{`n`tvalue`n`t{" | out-file -Encoding ascii $output_file -Append +$mic_grouping | % { + "`t`tcme_mic {{ key: `"{0}`" value: {1} }}" -f $_.Group[0].Sym, $cme_mics[$_.Group[0].Exch] } ` + | out-file -Encoding ascii $output_file -Append "`t}`n}" | out-file -Encoding ascii $output_file -Append + + Remove-Item $csv_save_path -Force # You can now copy the output file to defaults assuming all looks good -# Copy-Item .\cpp_config.conf ..\..\src\lib\config\defaults\ -Force -# Remove-Item cpp_config.conf \ No newline at end of file +# Copy-Item .\cme_reference_data.conf ..\..\src\lib\config\defaults\ -Force +# Remove-Item cme_reference_data.conf \ No newline at end of file diff --git a/tools/runsparkd.cmd b/tools/runsparkd.cmd index 9a3168b01..00fedf7f3 100644 --- a/tools/runsparkd.cmd +++ b/tools/runsparkd.cmd @@ -1,2 +1,3 @@ @if "%SPARKVM_PORT%"=="" set SPARKVM_PORT=22 -@ssh bts@sparkvm -p %SPARKVM_PORT% "cd /home/bts/spark/build/ninja_debug/server/ && ./sparkd %*" +@if "%SPARKVM_RUNMODE%"=="" set SPARKVM_RUNMODE=debug +@ssh bts@sparkvm -p %SPARKVM_PORT% "cd /home/bts/spark/build/ninja_%SPARKVM_RUNMODE%/server/ && ./sparkd %*" diff --git a/tools/runsparktest.cmd b/tools/runsparktest.cmd new file mode 100644 index 000000000..903d77d38 --- /dev/null +++ b/tools/runsparktest.cmd @@ -0,0 +1,4 @@ +@echo off +pushd %~dp0\..\src\client\sparkcmd +dotnet watch run test %1 -t +popd diff --git a/tools/tool.fsx b/tools/tool.fsx new file mode 100644 index 000000000..781fc03c2 --- /dev/null +++ b/tools/tool.fsx @@ -0,0 +1,45 @@ +#r "paket: +nuget Fake.IO.FileSystem +nuget Fake.Core.Target" +#load "./.fake/tool.fsx/intellisense.fsx" + +open Fake.Core +open Fake.IO +open Fake.IO.Globbing.Operators +open Fake + +open System.Diagnostics + +let replicate _ = + // unison.exe doesn't work without very specific parameters + printfn "Detected changes..." + let psi = ProcessStartInfo() + psi.FileName <- "python" + psi.Arguments <- __SOURCE_DIRECTORY__ + "/replicator/replicate_to_linux_vm.py" + psi.UseShellExecute <- false + psi.CreateNoWindow <- true + let p = Process.Start(psi) + p.WaitForExit() + printfn "Ok" + +let build _ = + replicate () + Process.directExec (fun config -> + { config with + FileName = "ssh" + Arguments = "bts@sparkvm -o StrictHostKeyChecking=no -t \"cd /home/bts/spark/build/ninja_debug/ && touch ../../CMakeLists.txt && time ninja\" " + }) |> ignore + + +Target.create "AutoBuild" (fun _ -> + use watcher = !!"src/**/*.cpp" ++ "src/**/*.h" ++ "src/**/*.proto" ++ "tools/*.sh" |> ChangeWatcher.run build + System.Console.ReadLine() |> ignore +) + +Target.create "Replicate" (fun _ -> + use watcher = !!"src/**/*.cpp" ++ "src/**/*.h" ++ "src/**/*.proto" |> ChangeWatcher.run replicate + System.Console.ReadLine() |> ignore +) + +Target.runOrDefault "Replicate" + diff --git a/tools/watch_tests.sh b/tools/watch_tests.sh new file mode 100755 index 000000000..6a5b9a871 --- /dev/null +++ b/tools/watch_tests.sh @@ -0,0 +1,14 @@ +#!/bin/bash +DIR=$(dirname ${BASH_SOURCE[0]}) +if [ "$1" = "-b" ] + then + cd $DIR/../build/ninja_debug/ + ninja + cd server + echo "Starting tests..." + ./sparkd test $2 +else + cd $DIR/../src + find $DIR/../src/ -name \*.h -o -name \*.cpp | entr /home/bts/spark/tools/watch_tests.sh -b $1 +fi +