Compare commits
34
Commits
2.4.0
...
SP-87-Splitter
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc0a123166 | ||
|
|
7f2988ec57 | ||
|
|
08c2762500 | ||
|
|
509a33dc53 | ||
|
|
4e6e97d90b | ||
|
|
cdabf3b023 | ||
|
|
8248bd7db8 | ||
|
|
d3ae9bc891 | ||
|
|
ac90a21314 | ||
|
|
1104f4db1d | ||
|
|
0dcdbd540e | ||
|
|
226fcc279b | ||
|
|
a6ad70fc05 | ||
|
|
e2b0da8103 | ||
|
|
aea233d537 | ||
|
|
5756c090f3 | ||
|
|
5ab0acb0ef | ||
|
|
3950e5b634 | ||
|
|
b5ea399e7e | ||
|
|
62baa6aa31 | ||
|
|
87433aa7c3 | ||
|
|
3252cab4bb | ||
|
|
fbe9b570a3 | ||
|
|
1bcb209961 | ||
|
|
b0726ad3ea | ||
|
|
e0ba27a0a9 | ||
|
|
345e45c5bf | ||
|
|
490add84d9 | ||
|
|
94fc2c4248 | ||
|
|
8a7e2db4ac | ||
|
|
71c576bbc0 | ||
|
|
2e4dff4d91 | ||
|
|
73336df814 | ||
|
|
633edb43eb |
No files matched your search
@@ -32,3 +32,4 @@ trace_data/
|
||||
/src/lib/orderman/perf_analysis/last_pretty_log.csv
|
||||
/src/lib/orderman/perf_analysis/log.csv
|
||||
/doc/user/public/*
|
||||
.ionide
|
||||
@@ -4,6 +4,11 @@ weight = 3
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
## 2.5.0
|
||||
### User Visible Settings
|
||||
- Made algos, synthetics and user names private so that multiple users can share a single system privately.
|
||||
- Allowed the mini order book defaults for column widths to be changed from the mini order book
|
||||
- Allowed the mini order book to shrink to the minimum size that will be needed to display the selected columns
|
||||
## 2.4.0
|
||||
### User Visible Settings
|
||||
- In the ladder, changed the last trade column to accumulate successive trades at the same price
|
||||
|
||||
@@ -7,12 +7,12 @@ using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using bts.api.core;
|
||||
using mbts.ui.shell;
|
||||
|
||||
namespace mbts.spark {
|
||||
[Export]
|
||||
class AdminModel : IDisposable {
|
||||
readonly string _user_name;
|
||||
public string user_name => _user_name;
|
||||
public readonly UserTuple user;
|
||||
|
||||
readonly BehaviorSubject<IReadOnlyDictionary<uint, string>>
|
||||
_accounts_subject = new BehaviorSubject<IReadOnlyDictionary<uint, string>>(new Dictionary<uint, string>()),
|
||||
@@ -24,14 +24,14 @@ namespace mbts.spark {
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
AdminModel(AdminService service, [Import("username")]string user_name) {
|
||||
_user_name = user_name;
|
||||
AdminModel(AdminService service, [Import]UserTuple user) {
|
||||
this.user = user;
|
||||
_dispose = service.subscribei_admin_data().Select(update => {
|
||||
_wrapper_subject.OnNext(new AdminModelWrapper(update.all.TryFirst(), _user_name));
|
||||
_wrapper_subject.OnNext(new AdminModelWrapper(update.all.TryFirst(), user.name));
|
||||
IEnumerable<BTSUserImmutable> active_users =
|
||||
from admin_data in update.all
|
||||
from bts_user in admin_data.Users
|
||||
where bts_user.Name == user_name && bts_user.HasUserData && bts_user.UserData.HasAccountPermissions
|
||||
where bts_user.Name == user.name && bts_user.HasUserData && bts_user.UserData.HasAccountPermissions
|
||||
select bts_user;
|
||||
|
||||
IEnumerable<BTSAccountImmutable> active_accounts =
|
||||
@@ -62,14 +62,6 @@ namespace mbts.spark {
|
||||
public IObservable<IReadOnlyList<AccountTuple>> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToList());
|
||||
public IObservable<AdminModelWrapper> on_admin_data => _wrapper_subject.AsObservable();
|
||||
|
||||
public UserTuple? get_current_user() {
|
||||
foreach (var kv in _user_map) {
|
||||
// just for good measure, there should never be > 1 element in _user_map, and it should always match _user_name
|
||||
if (kv.Value == _user_name)
|
||||
return new UserTuple(kv.Key, kv.Value);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public AccountTuple? get_default_account() {
|
||||
foreach (var kv in _account_map) {
|
||||
return new AccountTuple(kv.Key, kv.Value);
|
||||
|
||||
@@ -3,6 +3,7 @@ using mbts;
|
||||
using mbts.config;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
@@ -22,7 +23,7 @@ namespace bts.ui {
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
public AboutDaemon(IBus bus, [Import("username")]string username) {
|
||||
public AboutDaemon(IBus bus, [Import(LoginContainer.Exports.UserName)]string username) {
|
||||
_user = username;
|
||||
_bus = bus;
|
||||
_dispose = bus.subscribe(SparkTags.About).OnDispatcher().Subscribe(_ => show_about());
|
||||
|
||||
@@ -10,6 +10,7 @@ using System.Reactive.Linq;
|
||||
using System.Windows.Threading;
|
||||
using System.Windows;
|
||||
using System.Threading.Tasks;
|
||||
using mbts.ui.shell;
|
||||
|
||||
namespace bts.spark
|
||||
{
|
||||
@@ -20,7 +21,7 @@ namespace bts.spark
|
||||
IDisposable _dispose;
|
||||
|
||||
[ImportingConstructor]
|
||||
public ErrorReporter(ErrorReportService svc, IBus bus, [Import("username")] string username) {
|
||||
public ErrorReporter(ErrorReportService svc, IBus bus, [Import(LoginContainer.Exports.UserName)] string username) {
|
||||
_svc = svc;
|
||||
_username = username;
|
||||
_dispose = bus.subscribe(SparkTags.Support).OnDispatcher().Subscribe(msg => show_support_window(msg, "Report a problem"));
|
||||
@@ -82,7 +83,6 @@ namespace bts.spark
|
||||
}
|
||||
}
|
||||
|
||||
//public static async Task<Result<T>>
|
||||
public static async Task<(T value, bool success)> safe_execute<T>(this ErrorReporter error_reporter, Func<Task<Result<T>>> execute, Func<T, bool> get_success, Func<T, string> get_error_msg, string desc) {
|
||||
try {
|
||||
var result = await execute();
|
||||
@@ -101,5 +101,31 @@ namespace bts.spark
|
||||
void _report_error(string msg) =>
|
||||
error_reporter.report_error($"Error {desc}: {msg}", AlgoStudioWindow.NAME, MessageBoxImage.Warning);
|
||||
}
|
||||
|
||||
public static async Task<TResult> safe_execute<T, TResult>(
|
||||
this ErrorReporter error_reporter,
|
||||
Func<Task<Result<T>>> execute,
|
||||
Func<T, bool> get_success,
|
||||
Func<T, string> get_error_msg,
|
||||
Func<T, bool, TResult> result_selector,
|
||||
string desc) {
|
||||
//
|
||||
try {
|
||||
var result = await execute();
|
||||
if (!result.success) {
|
||||
_report_error(result.error_message);
|
||||
} else if (!get_success(result.value)) {
|
||||
_report_error(get_error_msg(result.value));
|
||||
} else {
|
||||
return result_selector(result.value, true);
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
_report_error(ex.ToString());
|
||||
}
|
||||
return result_selector(default, false);
|
||||
//
|
||||
void _report_error(string msg) =>
|
||||
error_reporter.report_error($"Error {desc}: {msg}", AlgoStudioWindow.NAME, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using bts.api.user;
|
||||
using bts.md;
|
||||
using mbts.ui.shell;
|
||||
|
||||
namespace bts {
|
||||
[Export(typeof(IInitializable))]
|
||||
@@ -27,8 +28,8 @@ namespace bts {
|
||||
static uint get_naggle() => mbts.config.Configuration.get<md.RelayOptions>().naggle_ms;
|
||||
|
||||
[ImportingConstructor]
|
||||
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss) :
|
||||
base(get_naggle(), MDSourceOptions.DeferInit) {
|
||||
public MDDaemon(Lazy<IBus> bus, Lazy<AuthTokenImmutable> token, Lazy<SyntheticSecurityService> ss, [Import(LoginContainer.Exports.UserID)]uint user_id) :
|
||||
base(get_naggle(), MDSourceOptions.DeferInit, user_id) {
|
||||
_bus = bus;
|
||||
_token = token;
|
||||
_ss = ss;
|
||||
|
||||
@@ -3,6 +3,7 @@ using bts.metrics;
|
||||
using mbts;
|
||||
using mbts.config;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
@@ -23,7 +24,7 @@ namespace bts.perf {
|
||||
Ping _pinger = new Ping();
|
||||
|
||||
[ImportingConstructor]
|
||||
public ShellLatencyDaemon(ShellLatencyService service, [Import("username")] string user) {
|
||||
public ShellLatencyDaemon(ShellLatencyService service, [Import(LoginContainer.Exports.UserName)] string user) {
|
||||
_service = service;
|
||||
_version = Configuration.get<bts.config.System>().version;
|
||||
_username = user;
|
||||
|
||||
@@ -5,6 +5,7 @@ using mbts.co2;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.IO;
|
||||
@@ -18,7 +19,7 @@ namespace bts.shell
|
||||
class MarketShellModule : IShellModule
|
||||
{
|
||||
[ImportingConstructor]
|
||||
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model, [Import("username")]string user_name) {
|
||||
public MarketShellModule(IBus bus, MDConsumer consumer, UserSettingsHub settings_hub, OrdersModel orders_model, [Import(LoginContainer.Exports.UserName)]string user_name) {
|
||||
_tsf = new TimeAndSalesWindowFactory(this, bus, consumer, settings_hub);
|
||||
_mvf = new MarketViewWindowFactory(this, bus, settings_hub, orders_model);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using bts.api;
|
||||
using bts.api.synthetic;
|
||||
using bts.spark;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.Composition;
|
||||
@@ -14,12 +15,14 @@ namespace mbts.spark {
|
||||
readonly IBus _bus;
|
||||
readonly SyntheticSecurityService _service;
|
||||
readonly ErrorReporter _error;
|
||||
readonly uint _user_id;
|
||||
|
||||
[ImportingConstructor]
|
||||
public SpreaderModel(IBus bus, SyntheticSecurityService service, ErrorReporter error) {
|
||||
public SpreaderModel(IBus bus, SyntheticSecurityService service, ErrorReporter error, [Import(LoginContainer.Exports.UserID)]uint user_id) {
|
||||
_bus = bus;
|
||||
_service = service;
|
||||
_error = error;
|
||||
_user_id = user_id;
|
||||
}
|
||||
|
||||
public IObservable<IReadOnlyDictionary<uint, ExecutionStrategyImmutable>> strategy => _bus.subscribe(SparkTags.SyntheticstrategiesTag);
|
||||
@@ -39,29 +42,40 @@ namespace mbts.spark {
|
||||
return (result, success);
|
||||
});
|
||||
|
||||
public async Task<ExecutionStrategyImmutable?> lookup_strategy_async(SecurityImmutable security) =>
|
||||
await lookup_strategy_async(security.Id);
|
||||
public Task<ExecutionStrategyImmutable?> lookup_strategy_async(SecurityImmutable security) =>
|
||||
lookup_strategy_async(security.Id);
|
||||
|
||||
public async Task<ExecutionStrategyImmutable?> lookup_strategy_async(uint id) {
|
||||
var result = await _service.query_security_metadata_async(new MetadataQuery { SecurityKey = { id } });
|
||||
if (result.success) {
|
||||
var metadata = result.value.Data.FirstOrDefault(i => i.SecurityKey == id);
|
||||
if (metadata != null) {
|
||||
return metadata.Strategy;
|
||||
}
|
||||
public Task<ExecutionStrategyImmutable?> lookup_strategy_async(uint id) =>
|
||||
_error.safe_execute(
|
||||
() => _service.query_security_metadata_async(new MetadataQuery { SecurityKey = { id } }),
|
||||
mdc => mdc.Data.Count == 1 && mdc.Data[0].SecurityKey == id,
|
||||
mdc => $"did not find strategy for security id '{id}.'",
|
||||
(mdc, success) => success ? mdc.Data[0].Strategy : (ExecutionStrategyImmutable?)null,
|
||||
"lookup strategy");
|
||||
|
||||
public Task<uint?> create_synthetic(SecurityImmutable synthetic) {
|
||||
if (synthetic.ContractCase != Security.ContractOneofCase.Synthetic)
|
||||
return null;
|
||||
if (synthetic.Synthetic.UserId != _user_id) {
|
||||
var msynth = synthetic.CloneToMutable();
|
||||
msynth.Synthetic.UserId = _user_id;
|
||||
synthetic = new SecurityImmutable(msynth);
|
||||
}
|
||||
return null;
|
||||
return _error.safe_execute(
|
||||
() => _service.create_synthetic_async(synthetic),
|
||||
r => r.Error == CreateResult.Types.Error.Ok,
|
||||
r => r.Error.ToString(),
|
||||
(r, success) => success ? r.Key : (uint?)null,
|
||||
"creating synthetic");
|
||||
}
|
||||
|
||||
public async Task<uint?> create_synthetic(SecurityImmutable? synthetic) {
|
||||
var result = await _service.create_synthetic_async(synthetic.Value);
|
||||
return _error.handle_error(result, "Error creating synthetic: {0}", r => r.Error, e => e == CreateResult.Types.Error.Ok, r => new uint?(r.Key), () => null).result;
|
||||
}
|
||||
|
||||
public async Task<bool> save_strategy(uint synthetic, ExecutionStrategyImmutable strategy) {
|
||||
var result = await _service.update_security_metadata_async(new Metadata { SecurityKey = synthetic, Strategy = strategy.CloneToMutable() });
|
||||
return _error.handle_error(result, "Error saving execution strategy: {0}", r => r.Error, e => e == UpdateResult.Types.Error.Ok);
|
||||
}
|
||||
public Task<bool> save_strategy(uint synthetic, ExecutionStrategyImmutable strategy) =>
|
||||
_error.safe_execute(
|
||||
() => _service.update_security_metadata_async(new Metadata { SecurityKey = synthetic, Strategy = strategy.CloneToMutable() }),
|
||||
r => r.Error == UpdateResult.Types.Error.Ok,
|
||||
r => r.Error.ToString(),
|
||||
(r, success) => success,
|
||||
"saving strategy");
|
||||
|
||||
public async Task<bool> verify_name_is_unique(string name) {
|
||||
var (result, success) = await strategy_tuple.FirstAsyncTask(SparkUtils.DefaultTimeOut);
|
||||
|
||||
@@ -134,7 +134,7 @@ namespace mbts.spark {
|
||||
_synthetic.ContractSpec.PriceBreak = double.MaxValue;
|
||||
_synthetic.ContractSpec.MainFraction = (uint)MathUtils.factor_out(_tick_factors.tick_base, 10);
|
||||
_synthetic.ContractSpec.PriceDisplayFormat = (uint)_tick_factors.display_digits();
|
||||
uint? id = await model.create_synthetic(_synthetic);
|
||||
uint? id = await model.create_synthetic(new SecurityImmutable(_synthetic));
|
||||
if (id.HasValue && id != 0) {
|
||||
if (await model.save_strategy(id.Value, get_strategy())) {
|
||||
var (smd, success) = await SparkUtils.get_security_md_async(bus, id.Value);
|
||||
|
||||
+12
-38
@@ -1,5 +1,4 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using bts.persist.algo;
|
||||
@@ -110,38 +109,24 @@ namespace mbts.spark {
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
|
||||
async Task<AlgoData?> _create_definition(CompositeAlgoDefinitionImmutable definition) {
|
||||
if (_try_get_user(out uint user_id)) {
|
||||
return await _create_definition(definition, user_id);
|
||||
} else {
|
||||
_report_no_user_id(ActionMetadata.CreateDefinition);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
async Task<AlgoData?> _create_definition(CompositeAlgoDefinitionImmutable definition) =>
|
||||
await _create_definition(definition, _admin_model.user.id);
|
||||
|
||||
async Task<AlgoTemplateData?> _create_template(AlgoTemplateImmutable template) {
|
||||
if (_try_get_user(out uint user_id)) {
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.create_template_async(
|
||||
new TemplateCreateRequest { UserId = user_id, NewTemplate = template.CloneToMutable() }),
|
||||
r => r.Success, r => r.ErrMsg, ActionMetadata.CreateTemplate);
|
||||
if (success)
|
||||
return _Utils.template_data(value.Record);
|
||||
} else {
|
||||
_report_no_user_id(ActionMetadata.CreateTemplate);
|
||||
}
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.create_template_async(
|
||||
new TemplateCreateRequest { UserId = _admin_model.user.id, NewTemplate = template.CloneToMutable() }),
|
||||
r => r.Success, r => r.ErrMsg, ActionMetadata.CreateTemplate);
|
||||
if (success)
|
||||
return _Utils.template_data(value.Record);
|
||||
return null;
|
||||
}
|
||||
|
||||
async Task<AlgoTemplateData?> _update_template(AlgoTemplateRecordImmutable template) {
|
||||
if (_try_get_user(out uint user_id)) {
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.update_template_async(
|
||||
new TemplateUpdateRequest { Key = template.Key, UpdatedTemplate = template.AlgoTemplate.CloneToMutable() }),
|
||||
r => r.Success, r => r.ErrMsg.ToString(), ActionMetadata.UpdateTemplate);
|
||||
if (success)
|
||||
return _Utils.template_data(template);
|
||||
} else {
|
||||
_report_no_user_id(ActionMetadata.UpdateTemplate);
|
||||
}
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.update_template_async(
|
||||
new TemplateUpdateRequest { Key = template.Key, UpdatedTemplate = template.AlgoTemplate.CloneToMutable() }),
|
||||
r => r.Success, r => r.ErrMsg.ToString(), ActionMetadata.UpdateTemplate);
|
||||
if (success)
|
||||
return _Utils.template_data(template);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -175,17 +160,6 @@ namespace mbts.spark {
|
||||
void _report_no_user_id(string desc) =>
|
||||
_error_reporter.report_error($"{desc} failed: did not find current user id");
|
||||
|
||||
bool _try_get_user(out uint id) {
|
||||
var user = _admin_model.get_current_user();
|
||||
if (user.HasValue) {
|
||||
id = user.Value.id;
|
||||
return true;
|
||||
} else {
|
||||
id = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// used for error handling/reporting
|
||||
interface IActionMetadata {
|
||||
string desc { get; }
|
||||
|
||||
+11
-24
@@ -1,14 +1,11 @@
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.core;
|
||||
using bts.api.orderman.algo;
|
||||
using bts.persist.algo;
|
||||
using bts.spark;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
@@ -27,7 +24,12 @@ namespace mbts.spark {
|
||||
_admin_model = admin_model;
|
||||
_error_reporter = error_reporter;
|
||||
this.ui_configuration = ui_configuration;
|
||||
definitions = service.subscribei_algo_definitions().Filter(r => r.Metadata.IsActive).SelectKeyValue(_definition_data, d => d.key).Table().Cache();
|
||||
definitions =
|
||||
service.subscribei_algo_definitions()
|
||||
.Filter(r => r.Metadata.IsActive && r.Metadata.CreatorUserId == _admin_model.user.id)
|
||||
.SelectKeyValue(_definition_data, d => d.key)
|
||||
.Table()
|
||||
.Cache();
|
||||
}
|
||||
|
||||
public IObservable<TableUpdate<AlgoData, AlgoKey>> definitions { get; }
|
||||
@@ -39,15 +41,11 @@ namespace mbts.spark {
|
||||
Observable.Return(TableUpdate<AlgoTemplateData, AlgoKey>.mk_empty(t => t.key));
|
||||
|
||||
public async Task<AlgoData?> save_definition(AlgoData definition_data) {
|
||||
if (_try_get_user(out uint user_id)) {
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.save_algo_definition_async(
|
||||
_with_key(new AlgoDefinitionUpdateRequest { UserId = user_id, Algo = _template(definition_data) }, definition_data.key)),
|
||||
r => r.Success, r => r.ErrorMessage, "saving template");
|
||||
if (success)
|
||||
return _definition_data(new AlgoDefinitionRecordImmutable(value.Record));
|
||||
} else {
|
||||
_report_no_user_id("save template");
|
||||
}
|
||||
var (value, success) = await _error_reporter.safe_execute(() => _service.save_algo_definition_async(
|
||||
_with_key(new AlgoDefinitionUpdateRequest { UserId = _admin_model.user.id, Algo = _template(definition_data) }, definition_data.key)),
|
||||
r => r.Success, r => r.ErrorMessage, "saving template");
|
||||
if (success)
|
||||
return _definition_data(new AlgoDefinitionRecordImmutable(value.Record));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -105,16 +103,5 @@ namespace mbts.spark {
|
||||
|
||||
void _report_no_user_id(string desc) =>
|
||||
_error_reporter.report_error($"{desc} failed: did not find current user id");
|
||||
|
||||
bool _try_get_user(out uint id) {
|
||||
var user = _admin_model.get_current_user();
|
||||
if (user.HasValue) {
|
||||
id = user.Value.id;
|
||||
return true;
|
||||
} else {
|
||||
id = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -96,14 +96,28 @@ namespace bts.shell {
|
||||
lp => new SettingsProfile<LadderSettings>(lp.id, lp.name, LadderSettings.sanitize(lp.value), lp.security_criteria))),
|
||||
(us, lps) => us.with(ladder_profiles: lps.Values.Select(lp => new LadderSettingsProfile(lp.id, lp.name, lp.value, lp.security_criteria)).ToArray()));
|
||||
state_settings?.sanitize();
|
||||
/// When settings are adjusted outside of the settings dialog, the vm should raise the <see cref="LadderVM.settings_invalidated"/> event.
|
||||
/// In response to that event, 'needs_resync' is set to true. This causes 'last_value' to be refreshed when the next caller requests the latest settings
|
||||
/// (i.e., an IObserver subscribing to the settings_source, or the settings-dialog viewmodel).
|
||||
bool initialized = false, needs_resync = false;
|
||||
LadderSettings last_value = fit_to_sec(state_settings);
|
||||
var settings_subject = new BehaviorSubject<LadderSettings>(last_value);
|
||||
LadderVM vm = null;
|
||||
var settings_subject = new Subject<LadderSettings>();
|
||||
var settings_source = Observable.Create<LadderSettings>(o => {
|
||||
ensure_synced();
|
||||
if (last_value != null)
|
||||
o.OnNext(last_value);
|
||||
return settings_subject.Subscribe(s => {
|
||||
if (s != null)
|
||||
o.OnNext(s);
|
||||
});
|
||||
});
|
||||
// vm
|
||||
var vm = LadderVM.create(_bus, model, last_value ?? _settings_hub.current.ladder, settings_subject.Where(s => s != null));
|
||||
vm = LadderVM.create(_bus, model, last_value ?? _settings_hub.current.ladder, settings_source);
|
||||
vm.settings_invalidated += on_settings_invalidated;
|
||||
vm.order_properties_vm.open_algo_popup_on_select = _ladder_conf.open_algo_popup_on_select;
|
||||
vm.order_properties_vm.use_algo_param_grid = _ladder_conf.use_algo_param_grid;
|
||||
vm.request_settings_dialog += open_settings_dialog;
|
||||
bool initialized = false;
|
||||
var settings_subscription = _settings_hub.subscribe_default_settings().Subscribe(on_user_settings);
|
||||
vm.OnDisposed += disposing => {
|
||||
Disposable.dispose(ref settings_subscription);
|
||||
@@ -119,8 +133,25 @@ namespace bts.shell {
|
||||
LadderSettings fit_to_sec(LadderSettings s) =>
|
||||
_fit_to_sec(s, md.security.ContractCase);
|
||||
|
||||
void on_settings_invalidated() =>
|
||||
needs_resync = true;
|
||||
|
||||
void ensure_synced() {
|
||||
if (!needs_resync)
|
||||
return;
|
||||
var curr = vm.get_save_state();
|
||||
if (settings_id == 0) {
|
||||
last_value = curr;
|
||||
} else if (!curr.Equals(last_value)) {
|
||||
settings_id = 0;
|
||||
last_value = curr;
|
||||
}
|
||||
needs_resync = false;
|
||||
}
|
||||
|
||||
void on_user_settings(UserSettings us) {
|
||||
if (initialized) {
|
||||
ensure_synced();
|
||||
if (settings_id == 0) {
|
||||
// not on a profile, don't respond to default settings changes
|
||||
return;
|
||||
@@ -290,4 +321,4 @@ namespace bts.shell {
|
||||
[ProtoMember(3)]
|
||||
public uint settings_id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -355,8 +355,17 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public LadderSettings get_result() {
|
||||
var clone = SparkUtils.clone_proto(_current);
|
||||
clone.mini_ob = new LadderMiniOrderBookSettings(mini_ob.create());
|
||||
LadderSettings clone;
|
||||
if (_current == null) {
|
||||
clone = new LadderSettings {
|
||||
mini_ob = new LadderMiniOrderBookSettings(mini_ob.create())
|
||||
};
|
||||
} else {
|
||||
clone = SparkUtils.clone_proto(_current);
|
||||
#pragma warning disable CS0612 // Type or member is obsolete
|
||||
clone.mini_ob = new LadderMiniOrderBookSettings(mini_ob.create().with(sorting_criterion: _current.mini_ob.value.sorting_criterion));
|
||||
#pragma warning restore CS0612 // Type or member is obsolete
|
||||
}
|
||||
clone.badge_style = badge_style;
|
||||
clone.alternate_order_size = alternate_order_size;
|
||||
clone.favorite_algos = favorite_algos_vm.create();
|
||||
|
||||
@@ -241,7 +241,7 @@ namespace mbts.spark {
|
||||
|
||||
public IBus bus => _bus;
|
||||
|
||||
public event Action on_tick, request_settings_dialog;
|
||||
public event Action on_tick, request_settings_dialog, settings_invalidated;
|
||||
|
||||
public MDConsumerHandle md => _model.md;
|
||||
// we return only entries who have piqs and whose piqs are not uint.Max
|
||||
@@ -394,6 +394,12 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
public void on_mini_orderbook_closed(bts.shell.orderbook.OrderBookStartupData ob_state) {
|
||||
current_settings = SparkUtils.clone_proto(current_settings);
|
||||
current_settings.mini_ob = new LadderMiniOrderBookSettings(ob_state.orderbook_grid.general);
|
||||
settings_invalidated?.Invoke();
|
||||
}
|
||||
|
||||
public void send_order((double price, Side? side, MouseButton button) args) {
|
||||
if (order_properties_vm == null || args.button == MouseButton.Middle)
|
||||
return;
|
||||
|
||||
@@ -9,9 +9,7 @@ using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Interactivity;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.spark {
|
||||
@@ -156,7 +154,10 @@ namespace mbts.spark {
|
||||
popup.SetBinding(System.Windows.Controls.Primitives.Popup.IsOpenProperty,
|
||||
nameof(OrderBookVM.is_row_collection_populated)
|
||||
);
|
||||
popup.Closed += (sender, event_args) => (popup.DataContext as IDisposable)?.Dispose();
|
||||
popup.Closed += (sender, event_args) => {
|
||||
(_parent.DataContext as LadderVM)?.on_mini_orderbook_closed(obvm.create());
|
||||
(popup.DataContext as IDisposable)?.Dispose();
|
||||
};
|
||||
popup.MouseUp += (sender, event_args) => {
|
||||
if (event_args.ChangedButton == MouseButton.Middle) {
|
||||
popup.IsOpen = false;
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
xmlns:order="clr-namespace:bts.api.orderman;assembly=spark_api"
|
||||
mc:Ignorable="d"
|
||||
MaxHeight="240"
|
||||
MinWidth="875"
|
||||
d:DesignHeight="600" d:DesignWidth="1080"
|
||||
FocusManager.IsFocusScope="True"
|
||||
Background="{StaticResource CommonBackground}">
|
||||
|
||||
@@ -1177,7 +1177,6 @@ namespace mbts.spark {
|
||||
public class CommitColumn : FrameworkElementColumnBase<Columns> {
|
||||
public CommitColumn() : base(Columns.Commit) {
|
||||
id = key.ToString();
|
||||
Header = "";
|
||||
Width = 20;
|
||||
}
|
||||
|
||||
|
||||
@@ -331,8 +331,8 @@ namespace mbts.spark {
|
||||
|
||||
IDisposable connect_order_mod_vm() =>
|
||||
Disposable.Create(
|
||||
_selected_params_mon.subscribe_algos().Subscribe(order_mod_vm.algo_vm.load_many),
|
||||
order_mod_vm.subscribe_is_dirty().Subscribe(_ => {
|
||||
_selected_params_mon.subscribe_algos().OnDispatcher().Subscribe(order_mod_vm.algo_vm.load_many),
|
||||
order_mod_vm.subscribe_is_dirty().OnDispatcher().Subscribe(_ => {
|
||||
modify_selected_orders_async_cmd.RaiseCanExecuteChanged();
|
||||
revert_selected_orders_cmd.RaiseCanExecuteChanged();
|
||||
}),
|
||||
@@ -340,10 +340,10 @@ namespace mbts.spark {
|
||||
|
||||
IDisposable connect_common_selected_params(OrderParamsVM vm) =>
|
||||
Disposable.Create(
|
||||
_selected_params_mon.subscribe_security().to_security_md(bus).OnDispatcher().Subscribe(newval => vm.security_md = newval),
|
||||
_selected_params_mon.subscribe_price().Subscribe(vm.on_common_price),
|
||||
_selected_params_mon.subscribe_size().Subscribe(vm.on_common_size),
|
||||
_selected_params_mon.subscribe_account().Subscribe(newval => vm.selected_account = newval));
|
||||
_selected_params_mon.subscribe_security().OnDispatcher().to_security_md(bus).OnDispatcher().Subscribe(newval => vm.security_md = newval),
|
||||
_selected_params_mon.subscribe_price().OnDispatcher().Subscribe(vm.on_common_price),
|
||||
_selected_params_mon.subscribe_size().OnDispatcher().Subscribe(vm.on_common_size),
|
||||
_selected_params_mon.subscribe_account().OnDispatcher().Subscribe(newval => vm.selected_account = newval));
|
||||
}
|
||||
|
||||
private void _row_collection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) {
|
||||
|
||||
@@ -168,17 +168,14 @@ namespace mbts.spark.utilities {
|
||||
// this will be the tag 50 on CME orders
|
||||
// current user might be okay, but we might also want to get it
|
||||
// from the store that serves the risk admin gui
|
||||
ManualOrderIndicator = ManualOrderIndicator.Yes
|
||||
ManualOrderIndicator = ManualOrderIndicator.Yes,
|
||||
User = _admin_model.user.name,
|
||||
UserId = _admin_model.user.id
|
||||
};
|
||||
if (account.HasValue) {
|
||||
rv.Account = account.Value.name;
|
||||
rv.AccountId = account.Value.id;
|
||||
}
|
||||
var user = _admin_model.get_current_user();
|
||||
if (user.HasValue) {
|
||||
rv.User = user.Value.name;
|
||||
rv.UserId = user.Value.id;
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,16 +28,27 @@ namespace mbts.ui.shell {
|
||||
model.Dispose();
|
||||
}
|
||||
|
||||
[Export("username")]
|
||||
[Export(Exports.UserName)]
|
||||
public string UserName { get; private set; }
|
||||
|
||||
[Export("userid")]
|
||||
[Export(Exports.UserID)]
|
||||
public uint UserId { get; private set; }
|
||||
|
||||
[Export]
|
||||
public UserTuple UserTuple => new UserTuple(UserId, UserName);
|
||||
|
||||
[Export]
|
||||
public AuthTokenImmutable Token { get; private set; }
|
||||
|
||||
[Export("workspace")]
|
||||
public string Workspace { get; private set; }
|
||||
|
||||
public static class Exports {
|
||||
public const string
|
||||
UserName = "username",
|
||||
UserID = "userid",
|
||||
UserTuple = "usertuple";
|
||||
}
|
||||
}
|
||||
|
||||
public struct User {
|
||||
@@ -49,8 +60,7 @@ namespace mbts.ui.shell {
|
||||
public readonly UInt32 id;
|
||||
}
|
||||
|
||||
public class LoginModel : IDisposable
|
||||
{
|
||||
public class LoginModel : IDisposable {
|
||||
// Singleton to help bootstrap the system
|
||||
private static LoginModel _instance = new LoginModel();
|
||||
public static LoginModel instance { get { return _instance; } }
|
||||
@@ -70,6 +80,11 @@ namespace mbts.ui.shell {
|
||||
public bool HasFatalError { get; set; }
|
||||
public User[] Users { get; private set; }
|
||||
|
||||
public bool try_get_user(string name, out User user) =>
|
||||
Users.TryGetFirst(u => u.name == name, out user);
|
||||
|
||||
public bool user_exists(string name) =>
|
||||
Users.Any(u => u.name == name);
|
||||
|
||||
// The final username
|
||||
public (string, uint, AuthTokenImmutable) get_user() {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
xmlns:ui="clr-namespace:mbts.ui"
|
||||
xmlns:uicore="clr-namespace:mbts.ui"
|
||||
xmlns:converters="clr-namespace:mbts.ui.converters"
|
||||
xmlns:autocomplete="clr-namespace:WPFTextBoxAutoComplete;assembly=WPFTextBoxAutoComplete"
|
||||
mc:Ignorable="d" MouseDown="Window_MouseDown"
|
||||
Width="500" Height="309" WindowStyle="None" ResizeMode="NoResize" WindowStartupLocation="CenterScreen"
|
||||
ui:StateManager.VisualState="{Binding VisualState}">
|
||||
@@ -82,7 +83,13 @@
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ComboBox ItemsSource="{Binding UserNames}" Grid.ColumnSpan="2" Margin="10" SelectedItem="{Binding Name, Mode=TwoWay}" Width="160" />
|
||||
<TextBox
|
||||
Grid.ColumnSpan="2"
|
||||
autocomplete:AutoCompleteBehavior.AutoCompleteItemsSource="{Binding KnownUsers}"
|
||||
Margin="10"
|
||||
Width="160"
|
||||
Text="{Binding Name, UpdateSourceTrigger=PropertyChanged}"
|
||||
/>
|
||||
<ComboBox ItemsSource="{Binding Workspaces}" Grid.Row="1" Grid.ColumnSpan="2" Margin="10" SelectedItem="{Binding SelectedWorkspace, Mode=TwoWay}" Width="160" MaxWidth="160" />
|
||||
|
||||
<PasswordBox Grid.Row="2" Margin="10" MinWidth="120" Grid.ColumnSpan="2" x:Name="_password" PasswordChanged="_passwordChanged" Style="{DynamicResource PasswordBoxStyle1}" Width="160">
|
||||
|
||||
@@ -18,7 +18,7 @@ namespace mbts.ui.shell
|
||||
_dispatcher = disp;
|
||||
_model = model;
|
||||
Workspaces = new ObservableCollection<string>();
|
||||
LoginCommand = new DelegateCommand(OnLogin, _ => UserNames.Count > 0 && SelectedWorkspace != null);
|
||||
LoginCommand = new DelegateCommand(OnLogin, _ => !string.IsNullOrEmpty(_name) && SelectedWorkspace != null);
|
||||
CancelCommand = new DelegateCommand(OnCancel);
|
||||
EmailCommand = new DelegateCommand(OnSendMail);
|
||||
if (_model.HasFatalError) {
|
||||
@@ -42,9 +42,10 @@ namespace mbts.ui.shell
|
||||
void _model_Ready() {
|
||||
_dispatcher.BeginInvoke(() => {
|
||||
if (!_model.HasFatalError) {
|
||||
this.UserNames.AddRange(_model.Users.Select(u => u.name));
|
||||
this.VisualState = "LoginState";
|
||||
load_username();
|
||||
if (!string.IsNullOrEmpty(_name))
|
||||
KnownUsers.Add(_name);
|
||||
LoginCommand.RaiseCanExecuteChanged();
|
||||
}
|
||||
else {
|
||||
@@ -57,8 +58,8 @@ namespace mbts.ui.shell
|
||||
private string _vstate = "Normal";
|
||||
public string VisualState { get { return _vstate; } set { _vstate = value; RaisePropertyChanged("VisualState"); } }
|
||||
|
||||
private ObservableCollection<string> _user_names = new ObservableCollection<string>();
|
||||
public ObservableCollection<string> UserNames { get { return _user_names; } }
|
||||
/// <see cref="KnownUsers"/> should only contain the user-name of the last login (if none exists, it should be empty).
|
||||
public ObservableCollection<string> KnownUsers { get; } = new ObservableCollection<string>();
|
||||
private string _name;
|
||||
public string Name {
|
||||
get { return _name; }
|
||||
@@ -82,12 +83,13 @@ namespace mbts.ui.shell
|
||||
private async void update_workspaces(string name) {
|
||||
Workspaces.Clear();
|
||||
Workspaces.Add("New workspace...");
|
||||
var user = _model.Users.First(u => u.name == name);
|
||||
var to_add = await _model.workspaces_for(user);
|
||||
this.SelectedWorkspace = Workspaces.First();
|
||||
if (to_add != null && name == _name && Workspaces.Count == 1) {
|
||||
Workspaces.AddRange(to_add);
|
||||
load_last_workspace(name);
|
||||
SelectedWorkspace = Workspaces.First();
|
||||
if (_model.try_get_user(name, out var user)) {
|
||||
var to_add = await _model.workspaces_for(user);
|
||||
if (to_add != null && name == _name && Workspaces.Count == 1) {
|
||||
Workspaces.AddRange(to_add);
|
||||
load_last_workspace(name);
|
||||
}
|
||||
}
|
||||
RaisePropertyChanged("SelectedWorkspace");
|
||||
LoginCommand.RaiseCanExecuteChanged();
|
||||
@@ -111,7 +113,7 @@ namespace mbts.ui.shell
|
||||
var value = key.GetValue("login");
|
||||
if (value != null) {
|
||||
var n = value.ToString();
|
||||
if (UserNames.Contains(n)) {
|
||||
if (_model.user_exists(n)) {
|
||||
Name = n;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
using System;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.spark;
|
||||
using mbts.ui.native;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using mbts.ui.native;
|
||||
using mbts.ui.utility;
|
||||
using bts.api;
|
||||
using bts.spark;
|
||||
using bts;
|
||||
|
||||
namespace mbts.ui.shell {
|
||||
[Export(typeof(ILauncher))]
|
||||
@@ -23,7 +23,7 @@ namespace mbts.ui.shell {
|
||||
[ImportingConstructor]
|
||||
public LauncherVM(
|
||||
ObservableCollection<IShellModule> modules,
|
||||
[Import("username")]string username,
|
||||
[Import(LoginContainer.Exports.UserName)]string username,
|
||||
ErrorReporter errors, IBus bus) {
|
||||
_user = username;
|
||||
_modules = new ObservableAutoWrapper<ShellModuleVM, IShellModule>(modules, m => new ShellModuleVM(m, bus));
|
||||
|
||||
@@ -45,12 +45,7 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<TextBox Margin="4" Text="{Binding SaveName, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Stretch" />
|
||||
<CheckBox Margin="4,0,4,0" Grid.Row="1" IsChecked="{Binding IsPublicWorkspace}" HorizontalAlignment="Left">Shared</CheckBox>
|
||||
<Button Grid.Column="1" HorizontalAlignment="Right" Margin="4" Command="{Binding SaveWorkspace}" IsDefault="True" MinWidth="60">Save</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
@@ -1,36 +1,33 @@
|
||||
using bts.api;
|
||||
using bts.api.synthetic;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Linq;
|
||||
using SecurityKey = System.UInt32;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using bts.api.user;
|
||||
using System.Collections.Generic;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using SecurityKey = System.UInt32;
|
||||
|
||||
namespace bts.md
|
||||
{
|
||||
namespace bts.md {
|
||||
[Flags]
|
||||
enum MDSourceOptions {
|
||||
DeferInit = 1,
|
||||
}
|
||||
class MDSource : IObserver<List<SecurityAndHandle>>, IDisposable
|
||||
{
|
||||
{
|
||||
const uint _no_user_id = 0;
|
||||
protected readonly MDConsumer _consumer = new MDConsumer();
|
||||
protected readonly Co2RelayControl _relay;
|
||||
protected DateTime _version = DateTime.MinValue;
|
||||
protected readonly ConcurrentDictionary<SecurityKey, SecurityMD> _exchange_sec_map = new ConcurrentDictionary<SecurityKey, SecurityMD>();
|
||||
protected List<IDisposable> _dispose = new List<IDisposable>();
|
||||
protected readonly MDSourceOptions _options;
|
||||
readonly uint _user_id;
|
||||
|
||||
public MDSource(uint naggle_ms, MDSourceOptions options = 0)
|
||||
public MDSource(uint naggle_ms, MDSourceOptions options = 0, uint user_id = _no_user_id)
|
||||
{
|
||||
_options = options;
|
||||
_user_id = user_id;
|
||||
_relay = new Co2RelayControl(_consumer, naggle_ms);
|
||||
_relay.preload_securities((co2.MDRequest.Types.Flags)0xff).ignore();
|
||||
_dispose.Add(_consumer.on_security.Subscribe(this));
|
||||
@@ -54,12 +51,15 @@ namespace bts.md
|
||||
|
||||
public void OnCompleted() { }
|
||||
public void OnError(Exception error) => Log.Md.error(error.ToString());
|
||||
public void OnNext(List<SecurityAndHandle> values)
|
||||
{
|
||||
if (values.Count > 0)
|
||||
{
|
||||
foreach (var v in values)
|
||||
{
|
||||
public void OnNext(List<SecurityAndHandle> values) {
|
||||
if (values.Count > 0) {
|
||||
foreach (var v in values) {
|
||||
if (_user_id != _no_user_id &&
|
||||
v.security.ContractCase == Security.ContractOneofCase.Synthetic &&
|
||||
v.security.Synthetic.UserId != _user_id &&
|
||||
v.security.Synthetic.UserId != _no_user_id) {
|
||||
continue;
|
||||
}
|
||||
var smd = new SecurityMD(new SecurityImmutable(v.security), v.handle);
|
||||
_exchange_sec_map.AddOrUpdate(smd.security.Id, smd, (_, __) => smd);
|
||||
}
|
||||
@@ -93,4 +93,4 @@ namespace bts.md
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +143,7 @@ namespace bts.tests.utils {
|
||||
products = tmpp.value;
|
||||
break;
|
||||
}
|
||||
await Task.Delay(10000);
|
||||
}
|
||||
|
||||
bts.test.Assert.IsTrue(products != null, "No products found");
|
||||
@@ -173,7 +174,7 @@ namespace bts.tests.utils {
|
||||
Result<Response> oret = await OrderUtilities.send_order(order_service, odef, 0,1);
|
||||
|
||||
var sub = order_service.subscribei_orders().Subscribe(async update => {
|
||||
if(service_value == null) {
|
||||
if(service_value == null && oret.value.OrderKey > 0) {
|
||||
await OrderUtilities.cancel_orders(order_service, null, oret.value.OrderKey);
|
||||
service_value = update;
|
||||
}
|
||||
|
||||
@@ -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/service_helpers/AlgoServiceUtils.cpp;api/services/AdminService.proto;api/services/AlgoDefinitionService.proto;api/services/AlgoService.proto;api/services/ApiUnitTestService.proto;api/services/AuthService.proto;api/services/ErrorReportService.proto;api/services/HelloService.proto;api/services/MBOService.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/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/MarketData.conf;config/defaults/Ports.conf;config/defaults/Symbols.conf;config/defaults/cme_reference_data.conf;config/defaults/edge.conf;config/defaults/jetfix.conf;config/defaults/keycode.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/jetfix.proto;config/protos/keycode.proto;config/protos/logging.proto;config/protos/mercury_cfg.proto;config/protos/orderman_cfg.proto;config/protos/orders_fills_cleanup.proto;config/protos/perflog_cfg.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/MDPeriodicDumper.cpp;md/Diag/ShmDumper.cpp;md/MBOProcessor.cpp;md/MBOServiceServer.cpp;md/MDClient.cpp;md/MDConsumer.cpp;md/MDConsumerHandle.cpp;md/MDConsumerInterop.cpp;md/MDProducer.cpp;md/MDProducerHandle.cpp;md/MemorySegmentUtils.cpp;md/PlatformLoader.cpp;md/PriceSize.cpp;md/SecurityCache.cpp;md/SecurityDB.cpp;md/SecurityIDRecorder.cpp;md/SecurityOracle.cpp;md/SettlementRecorder.cpp;md/Shm/FixedSecurity.cpp;md/SyntheticSecurityService.cpp;md/cme/MDP3Utils.cpp;md/common.cpp;md/ice/ICEUtils.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/mbo_tests.cpp;md/test/md2_tests.cpp;md/test/md_test_utils.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/OrderServices.cpp;orderman/actors/order_manLine truncated
|
||||
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/service_helpers/AlgoServiceUtils.cpp;api/services/AdminService.proto;api/services/AlgoDefinitionService.proto;api/services/AlgoService.proto;api/services/ApiUnitTestService.proto;api/services/AuthService.proto;api/services/ErrorReportService.proto;api/services/HelloService.proto;api/services/MBOService.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/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/MarketData.conf;config/defaults/Ports.conf;config/defaults/Symbols.conf;config/defaults/cme_reference_data.conf;config/defaults/edge.conf;config/defaults/jetfix.conf;config/defaults/keycode.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/jetfix.proto;config/protos/keycode.proto;config/protos/logging.proto;config/protos/mercury_cfg.proto;config/protos/orderman_cfg.proto;config/protos/orders_fills_cleanup.proto;config/protos/perflog_cfg.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/MDPeriodicDumper.cpp;md/Diag/ShmDumper.cpp;md/MBOProcessor.cpp;md/MBOServiceServer.cpp;md/MDClient.cpp;md/MDConsumer.cpp;md/MDConsumerHandle.cpp;md/MDConsumerInterop.cpp;md/MDProducer.cpp;md/MDProducerHandle.cpp;md/MemorySegmentUtils.cpp;md/PlatformLoader.cpp;md/PriceSize.cpp;md/SecurityCache.cpp;md/SecurityDB.cpp;md/SecurityIDRecorder.cpp;md/SecurityOracle.cpp;md/SettlementRecorder.cpp;md/Shm/FixedSecurity.cpp;md/SyntheticSecurityService.cpp;md/cme/MDP3Utils.cpp;md/common.cpp;md/ice/ICEUtils.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/mbo_tests.cpp;md/test/md2_tests.cpp;md/test/md_test_utils.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/OrderServices.cpp;orderman/actors/order_manLine truncated
|
||||
@@ -101,6 +101,7 @@ message CustomOrderDefinition
|
||||
OCOOrderDefinition oco = 25;
|
||||
PricerOrderDefinition pricer = 26;
|
||||
CompositeOrderDefinition composite = 27;
|
||||
SplitterOrderDefinition splitter = 29;
|
||||
|
||||
DynamicOrderDefinition dynamic = 1000;
|
||||
}
|
||||
@@ -204,6 +205,12 @@ message CompositeOrderDefinition {
|
||||
uint32 algo_template_key = 1;
|
||||
CustomOrderDefinition definition = 2;
|
||||
}
|
||||
message SplitterOrderDefinition {
|
||||
uint32 max_order_qty = 1;
|
||||
uint32 exposed_qty = 2;
|
||||
// uint32 target_qty = 3;
|
||||
CustomOrderDefinition child = 10;
|
||||
}
|
||||
message DynamicOrderDefinition {
|
||||
string dynamic_order_type = 1;
|
||||
map<string, DynamicOrderParam> order_params = 2;
|
||||
@@ -253,9 +260,10 @@ message OrderState {
|
||||
TriggerOrderState trigger = 21;
|
||||
CoverOrderState cover = 22;
|
||||
OffsetOrderState offset = 24;
|
||||
QueueHolderState queue_holder = 30;
|
||||
OCOOrderState oco = 25;
|
||||
PricerOrderState pricer = 26;
|
||||
QueueHolderState queue_holder = 30;
|
||||
SplitterOrderState splitter = 31;
|
||||
|
||||
DynamicOrderState dynamic = 255; // do not use anything bigger than 255, or things will break
|
||||
}
|
||||
@@ -334,6 +342,7 @@ message OCOOrderState {}
|
||||
message PricerOrderState {}
|
||||
message DynamicOrderState {}
|
||||
message QueueHolderState {}
|
||||
message SplitterOrderState {}
|
||||
|
||||
enum OrderStatus {
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ message SpreadContract {
|
||||
|
||||
message SyntheticContract {
|
||||
string name = 1;
|
||||
uint32 user_id = 2;
|
||||
repeated ContractLeg legs = 20;
|
||||
PriceCalc price_calc = 30;
|
||||
enum PriceCalc {
|
||||
|
||||
@@ -34,9 +34,9 @@ class IntPrice {
|
||||
#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) {}
|
||||
constexpr IntPrice() : _mantissa(0), _exponent(127) {/*nan*/}
|
||||
constexpr IntPrice(int64_t mantissa, int8_t exponent) : _mantissa(mantissa), _exponent(exponent) {}
|
||||
constexpr IntPrice(const IntPrice& other) : _mantissa(other._mantissa), _exponent(other._exponent) {}
|
||||
|
||||
bool operator==(const IntPrice& other) const {
|
||||
if(_exponent == other._exponent) {
|
||||
|
||||
@@ -77,7 +77,7 @@ public:
|
||||
extern PriceSize NO_MARKET;
|
||||
static_assert(sizeof(PriceSize) == 16, "PriceSize should be small");
|
||||
|
||||
template <uint8_t PRECISION>
|
||||
template <uint8_t PRECISION=11>
|
||||
PriceSize PriceSize_from_double(double price, Size size) {
|
||||
if (size == 0 || os::is_nan(price)) return NO_MARKET;
|
||||
return { IntPrice_from_double<PRECISION>(price), size };
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "lib/md/protos/co2.pb.h"
|
||||
#include "lib/md/SecurityIDRecorder.h"
|
||||
#include "lib/config/protos/system.pb.h"
|
||||
#include "lib/config/protos/MarketData.pb.h"
|
||||
#include "lib/utils/bts_chrono.h"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
@@ -173,7 +174,7 @@ void replay_file(const std::string& filename, IRelayWriter& writer, int32_t nagg
|
||||
} while (loop);
|
||||
}
|
||||
|
||||
void replay_url(const std::string& url, IRelayWriter& writer, int32_t naggle_ms) {
|
||||
bool replay_url(const std::string& url, IRelayWriter& writer, int32_t naggle_ms, bool loop) {
|
||||
// split up url parts
|
||||
size_t http_len = BTS_ARRAYSIZE("http://") - 1;
|
||||
auto after_host = url.find('/', http_len);
|
||||
@@ -184,21 +185,53 @@ void replay_url(const std::string& url, IRelayWriter& writer, int32_t naggle_ms)
|
||||
filename = os::sparkdata_path("data/replay/") + filename;
|
||||
utils::download_file_if_newer(host, path, filename);
|
||||
if (boost::filesystem::exists(filename)) {
|
||||
replay_file(filename, writer, naggle_ms, true);
|
||||
replay_file(filename, writer, naggle_ms, loop);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void replay_url_with_fallback(const std::string& url, IRelayWriter& writer, int32_t naggle_ms, bool loop) {
|
||||
if (!replay_url(url, writer, naggle_ms, loop)) {
|
||||
const auto fallback = config::get<ReplayOptions>().fallback();
|
||||
if (boost::filesystem::exists(fallback)) {
|
||||
replay_file(fallback, writer, naggle_ms, loop);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void replay_url_with_fallback(const std::string& url, IRelayWriter& writer, int32_t naggle_ms) {
|
||||
// loops forever
|
||||
replay_url(url, writer, naggle_ms);
|
||||
// If it returns it means it failed to download or play cached
|
||||
const auto fallback = config::get<ReplayOptions>().fallback();
|
||||
if (boost::filesystem::exists(fallback)) {
|
||||
replay_file(fallback, writer, naggle_ms);
|
||||
struct SecurityExtractorDone : public std::bad_exception {};
|
||||
struct SecurityExtractor : public IRelayWriter {
|
||||
size_t _count;
|
||||
std::function<void(const api::Security&)> _cb;
|
||||
std::set<uint32_t> _keys;
|
||||
SecurityExtractor(std::function<void(const api::Security&)>& cb, size_t count)
|
||||
: _count(count), _cb(cb), _keys() { }
|
||||
~SecurityExtractor() override {};
|
||||
void write(const co2::MDReply& data, bool*) override {
|
||||
for (auto const &ch : data.channels()) {
|
||||
for (auto const &cr : ch.updates()) {
|
||||
if (cr.has_security() && _keys.find(cr.security().id()) == _keys.end()) {
|
||||
_keys.insert(cr.security().id());
|
||||
_cb(cr.security());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (_keys.size() >= _count) {
|
||||
throw SecurityExtractorDone();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
void get_securities_from_replay(std::function<void(const api::Security&)> cb) {
|
||||
SecurityExtractor extractor(cb, 100);
|
||||
auto url = config::get<ReplayOptions>().url();
|
||||
try {
|
||||
replay_url_with_fallback(url, extractor, 0, false);
|
||||
} catch(const SecurityExtractorDone&) { }
|
||||
}
|
||||
|
||||
|
||||
shared_ptr<IRelayWriter> mkrelayproducer(bool shared);
|
||||
|
||||
/*this is the public one*/
|
||||
|
||||
@@ -29,9 +29,10 @@ void print_recording(const std::string&);
|
||||
void unprint_recording(const std::string& src, const std::string& dest);
|
||||
void trace_recording(const std::string& file, security_key key);
|
||||
void replay_recording(const std::string&, int32_t delay_ms, const std::string fallback="");
|
||||
void replay_url(const std::string& url, IRelayWriter& writer, int32_t naggle_ms);
|
||||
void replay_url_with_fallback(const std::string& url, IRelayWriter& writer, int32_t naggle_ms);
|
||||
bool replay_url(const std::string& url, IRelayWriter& writer, int32_t naggle_ms, bool loop=true);
|
||||
void replay_url_with_fallback(const std::string& url, IRelayWriter& writer, int32_t naggle_ms, bool loop=true);
|
||||
void replay_file(const std::string& filename, IRelayWriter& writer, int32_t naggle_ms, bool loop=false);
|
||||
void get_securities_from_replay(std::function<void(const api::Security&)>);
|
||||
|
||||
}}//namespace
|
||||
|
||||
@@ -153,6 +153,9 @@ class OrderStateUpdateEvent : public mercury::Event<OrderStateUpdateEvent> {
|
||||
case api::orderman::OrderState::kDynamic:
|
||||
result.mutable_dynamic();
|
||||
break;
|
||||
case api::orderman::OrderState::kSplitter:
|
||||
result.mutable_splitter();
|
||||
break;
|
||||
case api::orderman::OrderState::CUSTOM_NOT_SET:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/TopAlgo.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/IcebergAlgo.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/LeanAlgo.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/SplitterAlgo.h"
|
||||
#include "lib/orderman/orders/book_algos/OCOOrder.h"
|
||||
#include "lib/orderman/orders/book_algos/OffsetOrder.h"
|
||||
#include "lib/orderman/orders/book_algos/PricerOrder.h"
|
||||
@@ -208,6 +209,10 @@ Order &OrderFactory::allocate(OrderKey k, OrderKey pk,
|
||||
case CustomOrderDefinition::kComposite:
|
||||
order_ptr.reset(new orderman::CompositeOrder());
|
||||
break;
|
||||
case CustomOrderDefinition::kSplitter:
|
||||
order_ptr.reset(
|
||||
new orderman::AlgoWrapperOrder(std::make_unique<splitter::SplitterAlgo>()));
|
||||
break;
|
||||
case CustomOrderDefinition::CUSTOM_NOT_SET:
|
||||
bts_assert(false);
|
||||
break;
|
||||
|
||||
@@ -49,5 +49,68 @@ bool should_forward_attachment(
|
||||
}
|
||||
}
|
||||
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(int32_t v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.set_as_int32(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(uint32_t v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.set_as_uint32(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(double v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.set_as_double(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(std::string v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.set_as_string(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(bool v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.set_as_bool(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(const api::orderman::CustomOrderDefinition& v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.mutable_as_order_def()->CopyFrom(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(const api::orderman::DynamicOrderArray& v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.mutable_as_array()->CopyFrom(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(const api::orderman::DynamicOrderStruct& v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.mutable_as_struct()->CopyFrom(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(const api::orderman::DynamicOrderEnumerated& v) {
|
||||
api::orderman::DynamicOrderParam res;
|
||||
res.mutable_as_enumerated()->CopyFrom(v);
|
||||
return res;
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_array(const std::vector<api::orderman::DynamicOrderParam>& v) {
|
||||
api::orderman::DynamicOrderArray a;
|
||||
for (const auto& item : v) {
|
||||
a.add_element()->CopyFrom(item);
|
||||
}
|
||||
return dynamic_parameter(a);
|
||||
}
|
||||
api::orderman::DynamicOrderParam dynamic_map(
|
||||
const std::vector<std::tuple<std::string,api::orderman::DynamicOrderParam>>& v) {
|
||||
DynamicOrderStruct res;
|
||||
auto& map = *res.mutable_field();
|
||||
for (const auto& tup : v) {
|
||||
map[std::get<std::string>(tup)] = std::get<1>(tup);
|
||||
}
|
||||
return dynamic_parameter(res);
|
||||
}
|
||||
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
@@ -30,5 +32,64 @@ inline bool is_parent_order(const bts::api::orderman::OrderRecord &ord) {
|
||||
bool should_forward_attachment(const api::orderman::CustomOrderDefinition &def);
|
||||
bool can_consume_attachment(api::orderman::CustomOrderDefinition::CustomCase) ;
|
||||
|
||||
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(int32_t);
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(uint32_t);
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(double);
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(std::string);
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(bool);
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(const api::orderman::CustomOrderDefinition&);
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(const api::orderman::DynamicOrderArray&);
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(const api::orderman::DynamicOrderStruct&);
|
||||
api::orderman::DynamicOrderParam dynamic_parameter(const api::orderman::DynamicOrderEnumerated&);
|
||||
api::orderman::DynamicOrderParam dynamic_array(const std::vector<api::orderman::DynamicOrderParam>&);
|
||||
api::orderman::DynamicOrderParam dynamic_map( const std::vector<std::tuple<std::string,api::orderman::DynamicOrderParam>>&);
|
||||
|
||||
template <class T>
|
||||
std::optional<T> get_if(const api::orderman::DynamicOrderParam&) { return nullptr; }
|
||||
|
||||
template <>
|
||||
inline std::optional<int32_t> get_if(const api::orderman::DynamicOrderParam& p) {
|
||||
return p.value_case() == api::orderman::DynamicOrderParam::kAsInt32 ? std::optional<int32_t>{p.as_int32()} : std::nullopt;
|
||||
}
|
||||
template <>
|
||||
inline std::optional<uint32_t> get_if(const api::orderman::DynamicOrderParam& p) {
|
||||
return p.value_case() == api::orderman::DynamicOrderParam::kAsUint32 ? std::optional<uint32_t>{p.as_uint32()} : std::nullopt;
|
||||
}
|
||||
template <>
|
||||
inline std::optional<double> get_if(const api::orderman::DynamicOrderParam& p) {
|
||||
return p.value_case() == api::orderman::DynamicOrderParam::kAsDouble ? std::optional<double>{p.as_double()} : std::nullopt;
|
||||
}
|
||||
template <>
|
||||
inline std::optional<bool> get_if(const api::orderman::DynamicOrderParam& p) {
|
||||
return p.value_case() == api::orderman::DynamicOrderParam::kAsBool ? std::optional<bool>{p.as_bool()} : std::nullopt;
|
||||
}
|
||||
template <>
|
||||
inline std::optional<api::orderman::CustomOrderDefinition> get_if(const api::orderman::DynamicOrderParam& p) {
|
||||
return p.value_case() == api::orderman::DynamicOrderParam::kAsOrderDef ? std::optional<api::orderman::CustomOrderDefinition>{p.as_order_def()} : std::nullopt;
|
||||
}
|
||||
template <>
|
||||
inline std::optional<api::orderman::DynamicOrderArray> get_if(const api::orderman::DynamicOrderParam& p) {
|
||||
return p.value_case() == api::orderman::DynamicOrderParam::kAsArray ? std::optional<api::orderman::DynamicOrderArray>{p.as_array()} : std::nullopt;
|
||||
}
|
||||
template <>
|
||||
inline std::optional<api::orderman::DynamicOrderStruct> get_if(const api::orderman::DynamicOrderParam& p) {
|
||||
return p.value_case() == api::orderman::DynamicOrderParam::kAsStruct ? std::optional<api::orderman::DynamicOrderStruct>{p.as_struct()} : std::nullopt;
|
||||
}
|
||||
template <>
|
||||
inline std::optional<api::orderman::DynamicOrderEnumerated> get_if(const api::orderman::DynamicOrderParam& p) {
|
||||
return p.value_case() == api::orderman::DynamicOrderParam::kAsEnumerated ? std::optional<api::orderman::DynamicOrderEnumerated>{p.as_enumerated()} : std::nullopt;
|
||||
}
|
||||
|
||||
|
||||
template <class T>
|
||||
std::optional<T> get_if(const google::protobuf::Map<std::string,api::orderman::DynamicOrderParam>& map, const std::string& key) {
|
||||
auto c = map.find(key);
|
||||
if (c != map.end()) {
|
||||
return get_if<T>(c->second);
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -1 +1,8 @@
|
||||
#include "lib/orderman/orders/algo/IBeforeLaunchAlgoServices.h"
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
TimeStamp IBeforeLaunchAlgoServices::get_time() const { return Timer::now(); }
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/domain_model/SecurityKey.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
#include "lib/utils/bts_chrono.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
@@ -42,6 +43,8 @@ class IBeforeLaunchAlgoServices {
|
||||
virtual ChildTemplate &
|
||||
make_child_template(api::orderman::OrderDefinition const &child_def,
|
||||
size_t reserve = 1) = 0;
|
||||
|
||||
virtual TimeStamp get_time() const;
|
||||
};
|
||||
|
||||
} // namespace orderman
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
#include "SplitterAlgo.h"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoServices.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <tuple>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace splitter {
|
||||
|
||||
using namespace api::orderman;
|
||||
|
||||
struct ChildOrderState {
|
||||
OrderKey key;
|
||||
ChildState state;
|
||||
uint32_t min_qty() const { return state.total_fills.value(); }
|
||||
};
|
||||
|
||||
struct SplitterAlgo::Impl {
|
||||
IAlgoServices* algo = nullptr;
|
||||
ChildTemplate* child_template = nullptr;
|
||||
|
||||
uint32_t clip = 0;
|
||||
uint32_t exposed_qty = 0;
|
||||
uint32_t target_qty = 0;
|
||||
|
||||
bool is_terminated = false;
|
||||
Quantity total_fills = {};
|
||||
Price last_fill_price = {};
|
||||
|
||||
std::vector<ChildOrderState> orders;
|
||||
};
|
||||
|
||||
SplitterAlgo::SplitterAlgo() : me(new Impl()) { }
|
||||
SplitterAlgo::~SplitterAlgo() {}
|
||||
|
||||
OrderOpResult SplitterAlgo::do_before_launch(IBeforeLaunchAlgoServices& ibl) {
|
||||
// Validate and store parameters
|
||||
|
||||
auto& def = ibl.definition();
|
||||
bts_assert(def.has_custom_def());
|
||||
bts_assert(def.custom_def().has_splitter());
|
||||
auto& splitter = def.custom_def().splitter();
|
||||
|
||||
me->clip = splitter.max_order_qty();
|
||||
me->exposed_qty = splitter.exposed_qty();
|
||||
if (me->clip <= 0 || me->exposed_qty <= 0) {
|
||||
return OrderOpResult{ORDER_ERROR_INVALID_ORDER_DEFINITON,
|
||||
"max order quantity and exposed quantity must be greater than 0"};
|
||||
}
|
||||
|
||||
if (me->exposed_qty < me->target_qty) {
|
||||
return OrderOpResult{ORDER_ERROR_INVALID_ORDER_DEFINITON,
|
||||
"Exposed quantity must be greater than or equal to target quantity"};
|
||||
}
|
||||
|
||||
update_child_template(ibl);
|
||||
|
||||
me->orders.reserve(me->exposed_qty / me->clip + 1);
|
||||
|
||||
if (ibl.is_attaching()) {
|
||||
return OrderOpResult{ORDER_ERROR_OP_NOT_SUPPORTED,
|
||||
"Splitter does not support attach."};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void SplitterAlgo::do_launch(IAlgoServices& ias) {
|
||||
me->algo = &ias;
|
||||
me->target_qty = ias.quantity().value();
|
||||
// launches initial child orders
|
||||
sync_orders();
|
||||
}
|
||||
void SplitterAlgo::do_launch_attached(IAlgoServices &, OrderKey, ChildState) {
|
||||
// rejected in before_launch
|
||||
bts_assert(false);
|
||||
}
|
||||
void SplitterAlgo::do_update(const api::orderman::Update& update) {
|
||||
const CustomOrderDefinition* new_child_def = nullptr;
|
||||
if (update.has_custom() && update.custom().has_splitter()) {
|
||||
auto& splitter = update.custom().splitter();
|
||||
if (splitter.max_order_qty() > 0) {
|
||||
me->clip = splitter.max_order_qty();
|
||||
}
|
||||
me->exposed_qty = splitter.exposed_qty();
|
||||
|
||||
if (splitter.has_child()) {
|
||||
update_child_template(algo());
|
||||
new_child_def = &splitter.child();
|
||||
}
|
||||
}
|
||||
me->target_qty = update.quantity();
|
||||
|
||||
if (me->exposed_qty < me->target_qty) {
|
||||
algo().cancel("Exposed quantity must be greater than or equal to target quantity");
|
||||
return;
|
||||
}
|
||||
|
||||
sync_orders(new_child_def);
|
||||
}
|
||||
void SplitterAlgo::on_child_change(OrderKey key, ChildState const & state) {
|
||||
for (auto& child : me->orders) {
|
||||
if (child.key == key) {
|
||||
if (state.total_fills < child.state.total_fills) {
|
||||
algo().cancel("Unexpected fill reduction in fill quantityt");
|
||||
return;
|
||||
}
|
||||
auto delta = state.total_fills.value() - child.state.total_fills.value();
|
||||
if (delta != 0) {
|
||||
me->total_fills = Quantity { me->total_fills.value() + delta };
|
||||
me->last_fill_price = state.last_price;
|
||||
algo().update_fill_state(me->total_fills, me->last_fill_price);
|
||||
}
|
||||
child.state = state;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sync_orders();
|
||||
}
|
||||
|
||||
// TEST case:
|
||||
// total = 300, clip = 100
|
||||
// constraints: 50,50,50,50
|
||||
void compute_desired_sizes(uint32_t total, uint32_t clip, const std::vector<MinMax>& constraints, std::vector<Quantity>* results) {
|
||||
bts_assert(results);
|
||||
results->clear();
|
||||
|
||||
uint32_t irreducible = 0;
|
||||
for (const auto& mm : constraints) irreducible += mm.min;
|
||||
|
||||
// total > overfills ?
|
||||
|
||||
auto citer = constraints.begin();
|
||||
while(total > 0) {
|
||||
auto current = std::min(total, clip);
|
||||
// fit to constraint
|
||||
if (citer != constraints.end()) {
|
||||
irreducible -= citer->min;
|
||||
if (current > citer->max) current = citer->max;
|
||||
if (current < citer->min) current = citer->min;
|
||||
|
||||
// I can't be higher than (total - irreducible)
|
||||
if (current > (total - irreducible)) current = (total-irreducible);
|
||||
citer++;
|
||||
}
|
||||
results->push_back(Quantity{current});
|
||||
total -= current;
|
||||
}
|
||||
}
|
||||
|
||||
void get_constraints(const std::vector<ChildOrderState>& orders, std::vector<MinMax>* constraints) {
|
||||
bts_assert(constraints);
|
||||
for (const auto& order : orders) {
|
||||
//Note: to maintain queue position set max to current quantity. This will cause more orders to be added to the end if size increases
|
||||
constraints->push_back(MinMax {
|
||||
order.state.total_fills.value(),
|
||||
order.state.status == ChildStatus::WORKING ?
|
||||
std::numeric_limits<uint32_t>::max() : order.state.total_fills.value()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
bool SplitterAlgo::is_complete() const {
|
||||
uint32_t fills = 0;
|
||||
for (const auto& o : me->orders) { fills += o.state.total_fills.value(); }
|
||||
return fills >= me->target_qty;
|
||||
}
|
||||
|
||||
void SplitterAlgo::terminate() const {
|
||||
algo().complete();
|
||||
//?? Do we need to clean up children?
|
||||
for (const auto& o : me->orders) {
|
||||
if (o.state.status == ChildStatus::WORKING) {
|
||||
algo().cancel_child(o.key);
|
||||
}
|
||||
}
|
||||
me->is_terminated = true;
|
||||
}
|
||||
|
||||
void SplitterAlgo::update_child_template(IBeforeLaunchAlgoServices& ibl) {
|
||||
// Create child template
|
||||
auto child_def = ibl.definition();
|
||||
bts_assert(child_def.has_custom_def());
|
||||
bts_assert(child_def.custom_def().has_splitter());
|
||||
auto& splitter = child_def.custom_def().splitter();
|
||||
*child_def.mutable_custom_def() = splitter.child();
|
||||
me->child_template = &ibl.make_child_template(child_def);
|
||||
}
|
||||
|
||||
void SplitterAlgo::sync_orders(const api::orderman::CustomOrderDefinition* new_child_def) {
|
||||
if (me->is_terminated) return;
|
||||
if (is_complete()) {
|
||||
terminate();
|
||||
return;
|
||||
}
|
||||
|
||||
std::sort(me->orders.begin(), me->orders.end(), [](auto& a, auto b) { return b.min_qty() < a.min_qty(); });
|
||||
std::vector<MinMax> constraints;
|
||||
get_constraints(me->orders, &constraints);
|
||||
|
||||
std::vector<Quantity> desired;
|
||||
compute_desired_sizes(me->exposed_qty, me->clip, constraints, &desired);
|
||||
|
||||
auto oiter = me->orders.begin();
|
||||
// iterate to the current number of orders, don't consider orders that we're adding
|
||||
auto order_count = me->orders.size();
|
||||
auto price = algo().price();
|
||||
for (const auto& qty : desired) {
|
||||
if (order_count == 0) {
|
||||
// Ran out of existing orders, time to add more
|
||||
auto key = algo().launch_child(*me->child_template, price, qty);
|
||||
// Assume the order begins successfully. Hopefully we get back some sort of NACK otherwise
|
||||
me->orders.push_back({ key, {} });
|
||||
me->orders.back().state.price = price;
|
||||
me->orders.back().state.quantity = Quantity { qty };
|
||||
me->orders.back().state.status = ChildStatus::WORKING;
|
||||
}
|
||||
else {
|
||||
// Check the current order's quantity and modify it to match desired qty
|
||||
if (oiter->state.status == ChildStatus::WORKING) {
|
||||
if (oiter->state.price != price ||
|
||||
oiter->state.quantity != qty ||
|
||||
new_child_def != nullptr) {
|
||||
Update update;
|
||||
update.set_quantity(qty.value());
|
||||
update.set_price(price.value());
|
||||
if (new_child_def != nullptr) {
|
||||
*update.mutable_custom() = *new_child_def;
|
||||
}
|
||||
algo().update_child(oiter->key, update);
|
||||
}
|
||||
}
|
||||
oiter++;
|
||||
order_count -= 1;
|
||||
}
|
||||
}
|
||||
// cancel any orders past desired
|
||||
if (desired.size() < me->orders.size()) {
|
||||
for (size_t x = desired.size(); x < me->orders.size(); ++x) {
|
||||
auto& order= me->orders[x];
|
||||
if (order.state.status == ChildStatus::WORKING) {
|
||||
order.state.status = ChildStatus::CANCELED;
|
||||
algo().cancel_child(order.key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
IAlgoServices& SplitterAlgo::algo() const { bts_assert(me->algo); return *me->algo; }
|
||||
|
||||
}}} //namespace
|
||||
@@ -0,0 +1,60 @@
|
||||
#pragma once
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace splitter {
|
||||
|
||||
/*
|
||||
SplitterAlgo
|
||||
|
||||
Parameters:
|
||||
max_order_qty
|
||||
-- Clip size. No single order can exceed this quantity.
|
||||
exposed_qty
|
||||
-- The total quantity we want visible on the exchange.
|
||||
-- This will typically be higher than the quantity which helps capture fills in pro-rata algorithms
|
||||
price, quantity
|
||||
-- Common to all orders.
|
||||
-- Splitter is filled when it reaches (or exceeds) quantity not exposed_qty
|
||||
|
||||
SplitterAlgo acts as a single order to its parent but actually divides the total exposure
|
||||
by the clip size and working several orders at once.
|
||||
- It completes when the quantity is matched or exceeded.
|
||||
- It tries to maximize the size of the orders and it may sacrifice queue position to do that.
|
||||
- It reacts to fills and parameter changes and adjusts it's children appropriately.
|
||||
- See splitter_tests.cpp for examples
|
||||
- Future work: By constraining maximum size we can force this algorithm to maintain queue position.
|
||||
|
||||
*/
|
||||
class SplitterAlgo : public IAlgoOrder
|
||||
{
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> me;
|
||||
|
||||
public:
|
||||
SplitterAlgo();
|
||||
~SplitterAlgo();
|
||||
|
||||
OrderOpResult do_before_launch(IBeforeLaunchAlgoServices &) override;
|
||||
void do_launch(IAlgoServices &) override;
|
||||
void do_launch_attached(IAlgoServices &, OrderKey, ChildState) override;
|
||||
void do_update(api::orderman::Update const &) override;
|
||||
void on_child_change(OrderKey, ChildState const &) override;
|
||||
|
||||
private:
|
||||
IAlgoServices& algo() const;
|
||||
|
||||
void sync_orders(const api::orderman::CustomOrderDefinition* new_child_def = nullptr);
|
||||
bool is_complete() const;
|
||||
void terminate() const;
|
||||
|
||||
void update_child_template(IBeforeLaunchAlgoServices&);
|
||||
};
|
||||
|
||||
// exposed for unit testing purposes.
|
||||
struct MinMax {uint32_t min, max;};
|
||||
void compute_desired_sizes(uint32_t , uint32_t ,
|
||||
const std::vector<MinMax>& , std::vector<Quantity>* );
|
||||
|
||||
}}} //namespace
|
||||
@@ -0,0 +1,378 @@
|
||||
#include "AlgoTestDriver.h"
|
||||
#include "lib/md/relay/RelayRecorder.h"
|
||||
#include "catch/catch.hpp"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/logging/logging.h"
|
||||
|
||||
#include <optional>
|
||||
#include <queue>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
using namespace api::orderman;
|
||||
|
||||
constexpr bool has_flag(ATDFlags prop, ATDFlags flag) {
|
||||
return ((uint32_t)prop & (uint32_t)flag) == (uint32_t)flag;
|
||||
}
|
||||
|
||||
const std::map<SecurityKey, api::Security>& get_some_securities() {
|
||||
static std::map<SecurityKey, api::Security> results;
|
||||
if (results.empty()) {
|
||||
md::get_securities_from_replay([&](const api::Security& s) {
|
||||
results.insert(std::make_pair(SecurityKey(s.id()), s));
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
|
||||
struct RecursionCounter {
|
||||
size_t depth = 0;
|
||||
virtual void flush() = 0;
|
||||
virtual ~RecursionCounter() {}
|
||||
};
|
||||
struct RecursionGuard {
|
||||
RecursionGuard(RecursionCounter& c) : counter(c) {
|
||||
counter.depth += 1;
|
||||
REQUIRE(counter.depth < 100);
|
||||
}
|
||||
~RecursionGuard() {
|
||||
counter.depth -= 1;
|
||||
if (counter.depth == 0) { counter.flush(); }
|
||||
}
|
||||
private:
|
||||
RecursionCounter& counter;
|
||||
};
|
||||
|
||||
struct AlgoTestDriverBase::Impl : public RecursionCounter {
|
||||
IAlgoOrder& algo;
|
||||
api::orderman::OrderDefinition definition;
|
||||
Price price;
|
||||
Quantity size;
|
||||
ATDFlags flags;
|
||||
TimeStamp now;
|
||||
api::Security security;
|
||||
OrderKey order_key, parent_key;
|
||||
std::tuple<Quantity, Price> fill_state;
|
||||
|
||||
AlgoState state;
|
||||
std::vector<ChildOrder> orders;
|
||||
|
||||
std::map<SecurityKey, Book> books;
|
||||
std::map<SecurityKey, bool> book_subs;
|
||||
bool sub_timer = false;
|
||||
|
||||
// Callbacks
|
||||
std::vector<std::function<void(OrderKey)>> on_child_launching;
|
||||
std::vector<std::function<void(const ChildOrder&)>> on_child_changing;
|
||||
std::vector<std::function<void(const ChildOrder&, const ChildOrder&)>> on_child_changed;
|
||||
|
||||
Impl(IAlgoOrder& o) : algo(o) {}
|
||||
|
||||
OrderKey next_order() {
|
||||
_next_order.value() += 1;
|
||||
return _next_order;
|
||||
}
|
||||
RecursionGuard recusion_guard() { return {*this}; }
|
||||
|
||||
void flush() override;
|
||||
bool flushing = false;
|
||||
// child changes must be asyncronous
|
||||
std::vector<std::tuple<OrderKey, ChildState>> deferred;
|
||||
void defer_child_change(OrderKey k, ChildState cs) { deferred.push_back(std::make_tuple(k,cs)); }
|
||||
|
||||
private:
|
||||
OrderKey _next_order = OrderKey(99);
|
||||
};
|
||||
|
||||
void AlgoTestDriverBase::Impl::flush() {
|
||||
if(flushing) return;
|
||||
flushing = true;
|
||||
for (const auto& update : deferred) {
|
||||
algo.on_child_change(std::get<OrderKey>(update), std::get<ChildState>(update));
|
||||
}
|
||||
deferred.clear();
|
||||
flushing = false;
|
||||
}
|
||||
|
||||
AlgoTestDriverBase::AlgoTestDriverBase(
|
||||
IAlgoOrder& algo,
|
||||
const api::orderman::OrderDefinition& def,
|
||||
Price price, Quantity size,
|
||||
ATDFlags flags)
|
||||
: me(new Impl(algo)) {
|
||||
|
||||
me->definition = def;
|
||||
me->price = price;
|
||||
me->size = size;
|
||||
me->flags = flags;
|
||||
me->now = Timer::now();
|
||||
me->state = AlgoState::CREATED;
|
||||
|
||||
auto securities = get_some_securities();
|
||||
REQUIRE(securities.size() > 0);
|
||||
me->security = securities.begin()->second;
|
||||
|
||||
me->order_key = me->next_order();
|
||||
if (has_flag(flags, ATDFlags::HAS_PARENT)) {
|
||||
me->parent_key = me->next_order();
|
||||
}
|
||||
}
|
||||
|
||||
AlgoTestDriverBase::~AlgoTestDriverBase() {}
|
||||
|
||||
OrderOpResult AlgoTestDriverBase::before_launch() {
|
||||
REQUIRE(me->state == AlgoState::CREATED);
|
||||
me->state = AlgoState::READY;
|
||||
return me->algo.do_before_launch(*this);
|
||||
}
|
||||
|
||||
void AlgoTestDriverBase::launch() {
|
||||
REQUIRE(me->state == AlgoState::READY);
|
||||
me->algo.do_launch(*this);
|
||||
me->state = AlgoState::RUNNING;
|
||||
}
|
||||
|
||||
void AlgoTestDriverBase::launch_attached(OrderKey key, ChildState state) {
|
||||
REQUIRE(me->state == AlgoState::READY);
|
||||
me->algo.do_launch_attached(*this, key, state);
|
||||
me->state = AlgoState::RUNNING;
|
||||
}
|
||||
void AlgoTestDriverBase::do_update(const api::orderman::Update& update) {
|
||||
auto guard = me->recusion_guard();
|
||||
REQUIRE(me->state == AlgoState::RUNNING);
|
||||
me->algo.do_update(update);
|
||||
}
|
||||
|
||||
void AlgoTestDriverBase::update_book(const Book& book) {
|
||||
auto key = book.security_key();
|
||||
me->books[key] = book;
|
||||
auto iter = me->book_subs.find(key);
|
||||
if (iter != me->book_subs.end()) {
|
||||
me->algo.on_book(key, book);
|
||||
}
|
||||
}
|
||||
void AlgoTestDriverBase::advance_time(Duration d) {
|
||||
me->now += d;
|
||||
if (me->sub_timer) {
|
||||
me->algo.on_timer();
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<ChildOrder>& AlgoTestDriverBase::child_orders() const {
|
||||
return me->orders;
|
||||
}
|
||||
|
||||
AlgoState AlgoTestDriverBase::state() const {
|
||||
return me->state;
|
||||
}
|
||||
|
||||
void AlgoTestDriverBase::fill(OrderKey k, Price price, Quantity qty) {
|
||||
auto guard = me->recusion_guard();
|
||||
for (auto& child : me->orders) {
|
||||
if (child.key == k) {
|
||||
REQUIRE(child.cstate.quantity.value() - child.cstate.total_fills.value() >= qty.value());
|
||||
for (const auto& cb : me->on_child_changing) cb(child);
|
||||
auto old = child;
|
||||
|
||||
auto total_price = child.cstate.avg_price.value() * child.cstate.total_fills.value();
|
||||
total_price += price.value() * qty.value();
|
||||
|
||||
child.cstate.last_fills = qty;
|
||||
child.cstate.total_fills.value() += qty.value();
|
||||
child.cstate.last_price = price;
|
||||
child.cstate.avg_price = Price { total_price / child.cstate.total_fills.value() };
|
||||
|
||||
if (child.cstate.quantity.value() <= child.cstate.total_fills.value()) {
|
||||
child.cstate.status = ChildStatus::COMPLETED;
|
||||
}
|
||||
|
||||
for (const auto& cb : me->on_child_changed) cb(old, child);
|
||||
// No defer because this is called from outside
|
||||
if (child.subscribed) me->algo.on_child_change(k, child.cstate);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const ChildOrder* largest_order(const std::vector<ChildOrder>& orders) {
|
||||
uint32_t sz = 0;
|
||||
const ChildOrder* result = nullptr;
|
||||
for (const auto& order : orders) {
|
||||
auto available =
|
||||
order.cstate.quantity.value() - order.cstate.total_fills.value();
|
||||
if (order.cstate.status == ChildStatus::WORKING &&
|
||||
available > sz) {
|
||||
sz = available;
|
||||
result = ℴ
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
void AlgoTestDriverBase::fill(Price p, Quantity q) {
|
||||
auto to_fill = q.value();
|
||||
while(to_fill > 0) {
|
||||
auto order = largest_order(this->child_orders());
|
||||
REQUIRE(order);
|
||||
auto available = (order->cstate.quantity.value() - order->cstate.total_fills.value());
|
||||
if (available >= to_fill) {
|
||||
this->fill(order->key, p, Quantity {to_fill});
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this->fill(order->key, p, Quantity {available});
|
||||
to_fill -= available;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AlgoTestDriverBase::on_child_launching(std::function<void(OrderKey)>&& cb) {
|
||||
me->on_child_launching.push_back(cb);
|
||||
}
|
||||
void AlgoTestDriverBase::on_child_changing(std::function<void(const ChildOrder&)>&& cb) {
|
||||
me->on_child_changing.push_back(cb);
|
||||
}
|
||||
void AlgoTestDriverBase::on_child_changed(std::function<void(const ChildOrder&, const ChildOrder&)>&& cb) {
|
||||
me->on_child_changed.push_back(cb);
|
||||
}
|
||||
|
||||
//
|
||||
// Utility
|
||||
|
||||
ChildOrder mkchild(OrderKey key, Price price, Quantity qty, bool sub) {
|
||||
ChildOrder result = {};
|
||||
result.key = key;
|
||||
result.cstate.price = price;
|
||||
result.cstate.quantity = qty;
|
||||
result.subscribed = sub;
|
||||
return result;
|
||||
}
|
||||
|
||||
//
|
||||
// IBeforeLaunch
|
||||
|
||||
const api::orderman::OrderDefinition& AlgoTestDriverBase::definition() const { return me->definition; }
|
||||
const api::Security& AlgoTestDriverBase::security() const { return me->security; }
|
||||
OrderKey AlgoTestDriverBase::order_key() const { return me->order_key; }
|
||||
OrderKey AlgoTestDriverBase::parent_order_key() const { return me->parent_key; }
|
||||
const api::Security* AlgoTestDriverBase::lookup_security(SecurityKey key) const {
|
||||
auto s = get_some_securities();
|
||||
auto iter = s.find(key);
|
||||
if (iter != s.end()) {
|
||||
return &iter->second;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
bool AlgoTestDriverBase::is_attaching() const { return has_flag(me->flags, ATDFlags::IS_ATTACHING); }
|
||||
ChildTemplate& AlgoTestDriverBase::make_child_template(const api::orderman::OrderDefinition&, size_t) {
|
||||
ChildTemplate* t = nullptr;
|
||||
return *t;
|
||||
}
|
||||
TimeStamp AlgoTestDriverBase::get_time() const { return me->now; }
|
||||
|
||||
//
|
||||
// IAlgoServices
|
||||
|
||||
Price AlgoTestDriverBase::price() const { return me->price; }
|
||||
Quantity AlgoTestDriverBase::quantity() const { return me->size; }
|
||||
Quantity AlgoTestDriverBase::total_fills() const {
|
||||
return std::get<Quantity>(me->fill_state);
|
||||
}
|
||||
|
||||
void AlgoTestDriverBase::subscribe_book(SecurityKey key){
|
||||
me->book_subs[key] = true;
|
||||
auto iter = me->books.find(key);
|
||||
if (iter != me->books.end()) {
|
||||
me->algo.on_book(key, iter->second);
|
||||
}
|
||||
}
|
||||
void AlgoTestDriverBase::unsubscribe_book(SecurityKey key) {
|
||||
me->book_subs.erase(key);
|
||||
}
|
||||
void AlgoTestDriverBase::subscribe_child_change(OrderKey k) {
|
||||
for (auto& child : me->orders) {
|
||||
if (child.key == k) {
|
||||
child.subscribed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
void AlgoTestDriverBase::unsubscribe_child_change(OrderKey k) {
|
||||
for (auto& child : me->orders) {
|
||||
if (child.key == k) {
|
||||
child.subscribed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
void AlgoTestDriverBase::subscribe_timer() { me->sub_timer = true; }
|
||||
void AlgoTestDriverBase::unsubscribe_timer() { me->sub_timer = false; }
|
||||
|
||||
OrderKey AlgoTestDriverBase::launch_child(ChildTemplate&, Price price, Quantity qty, bool sub) {
|
||||
OrderKey key = me->next_order();
|
||||
me->orders.push_back(
|
||||
mkchild(key, price, qty, sub));
|
||||
for(const auto& cb : me->on_child_launching) cb(key);
|
||||
return key;
|
||||
}
|
||||
OrderKey AlgoTestDriverBase::launch_child(api::orderman::OrderDefinition const & od, Price price, Quantity qty, bool sub) {
|
||||
return launch_child(make_child_template(od), price, qty, sub);
|
||||
}
|
||||
void AlgoTestDriverBase::update_child(OrderKey key, api::orderman::Update const & update) {
|
||||
auto guard = me->recusion_guard();
|
||||
for (auto& child : me->orders) {
|
||||
if (child.key == key) {
|
||||
for (const auto& cb : me->on_child_changing) cb(child);
|
||||
auto old = child;
|
||||
if (update.price() != 0) {
|
||||
child.cstate.price = Price(update.price());
|
||||
}
|
||||
if (update.quantity() != 0) {
|
||||
child.cstate.quantity = Quantity(update.quantity());
|
||||
}
|
||||
if (child.cstate.quantity.value() <= child.cstate.total_fills.value()) {
|
||||
child.cstate.status = ChildStatus::COMPLETED;
|
||||
}
|
||||
for (const auto& cb : me->on_child_changed) cb(old, child);
|
||||
|
||||
if (child.subscribed) {
|
||||
me->defer_child_change(key, child.cstate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
void AlgoTestDriverBase::cancel_child(OrderKey key) {
|
||||
auto guard = me->recusion_guard();
|
||||
for (auto& child : me->orders) {
|
||||
if (child.key == key) {
|
||||
REQUIRE(child.cstate.status != ChildStatus::CANCELED);
|
||||
for (const auto& cb : me->on_child_changing) cb(child);
|
||||
auto before = child;
|
||||
child.cstate.status = ChildStatus::CANCELED;
|
||||
for (const auto& cb : me->on_child_changed) cb(before, child);
|
||||
if(child.subscribed) {
|
||||
me->defer_child_change(key, child.cstate);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
void AlgoTestDriverBase::detach_child(OrderKey key) {
|
||||
std::remove_if(me->orders.begin(), me->orders.end(), [key](const auto& order) { return order.key == key; });
|
||||
}
|
||||
void AlgoTestDriverBase::detach_all_children() {
|
||||
me->orders.clear();
|
||||
}
|
||||
void AlgoTestDriverBase::cancel(std::string const &) {
|
||||
me->state = AlgoState::CANCELED;
|
||||
}
|
||||
void AlgoTestDriverBase::terminate_error(std::string const &) {
|
||||
me->state = AlgoState::TERMINATED;
|
||||
}
|
||||
void AlgoTestDriverBase::complete() {
|
||||
me->state = AlgoState::COMPLETED;
|
||||
}
|
||||
void AlgoTestDriverBase::set_ladder_visibility(bool) { /* whatever, ok */ }
|
||||
void AlgoTestDriverBase::update_fill_state(Quantity q, Price p) { me->fill_state = { q, p }; }
|
||||
|
||||
}}}//namespace
|
||||
@@ -0,0 +1,140 @@
|
||||
#pragma once
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoServices.h"
|
||||
#include "lib/md/IntPrice.h"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
|
||||
const std::map<SecurityKey, api::Security>& get_some_securities();
|
||||
|
||||
enum class ATDFlags {
|
||||
NONE = 0,
|
||||
IS_ATTACHING = 1,
|
||||
HAS_PARENT = 2,
|
||||
};
|
||||
|
||||
enum class AlgoState {
|
||||
CREATED,
|
||||
// before_lauh() ->
|
||||
READY,
|
||||
// launch() ->
|
||||
RUNNING,
|
||||
CANCELED,
|
||||
TERMINATED,
|
||||
COMPLETED
|
||||
};
|
||||
|
||||
struct ChildOrder {
|
||||
OrderKey key;
|
||||
bool subscribed;
|
||||
ChildState cstate;
|
||||
uint32_t active_qty() const {
|
||||
return cstate.status == ChildStatus::WORKING ?
|
||||
cstate.quantity.value() - cstate.total_fills.value() : 0;
|
||||
}
|
||||
};
|
||||
|
||||
class AlgoTestDriverBase : private IAlgoServices {
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> me;
|
||||
|
||||
public:
|
||||
AlgoTestDriverBase(
|
||||
IAlgoOrder& algo,
|
||||
const api::orderman::OrderDefinition& def,
|
||||
Price price, Quantity size,
|
||||
ATDFlags flags=ATDFlags::NONE);
|
||||
~AlgoTestDriverBase();
|
||||
|
||||
//
|
||||
// Expose some things from IAlgoServices
|
||||
const api::Security& security() const override;
|
||||
Price price() const override;
|
||||
Quantity quantity() const override;
|
||||
Quantity total_fills() const override;
|
||||
|
||||
//
|
||||
// Change state
|
||||
|
||||
OrderOpResult before_launch();
|
||||
void launch();
|
||||
void launch_attached(OrderKey, ChildState);
|
||||
void do_update(const api::orderman::Update&);
|
||||
|
||||
void update_book(const Book&);
|
||||
void advance_time(Duration);
|
||||
|
||||
//
|
||||
// Inspection methods
|
||||
const std::vector<ChildOrder>& child_orders() const;
|
||||
AlgoState state() const;
|
||||
|
||||
// Fills for this specific order
|
||||
void fill(OrderKey, Price, Quantity);
|
||||
// Fill this quantity, start with the largest child order
|
||||
void fill(Price, Quantity);
|
||||
|
||||
void on_child_launching(std::function<void(OrderKey)>&&);
|
||||
void on_child_changing(std::function<void(const ChildOrder&)>&&);
|
||||
void on_child_changed(std::function<void(const ChildOrder&, const ChildOrder&)>&&);
|
||||
|
||||
|
||||
|
||||
private:
|
||||
|
||||
//
|
||||
// IBeforeLaunchAlgoServices
|
||||
const api::orderman::OrderDefinition& definition() const override;
|
||||
// const api::Security& security() const override;
|
||||
OrderKey order_key() const override;
|
||||
OrderKey parent_order_key() const override;
|
||||
const api::Security* lookup_security(SecurityKey) const override;
|
||||
bool is_attaching() const override;
|
||||
ChildTemplate& make_child_template(const api::orderman::OrderDefinition& child_def, size_t reserve = 1) override;
|
||||
TimeStamp get_time() const override;
|
||||
|
||||
//
|
||||
// IAlgoServices
|
||||
void subscribe_book(SecurityKey)override;
|
||||
void unsubscribe_book(SecurityKey) override;
|
||||
void subscribe_child_change(OrderKey) override;
|
||||
void unsubscribe_child_change(OrderKey) override;
|
||||
void subscribe_timer() override;
|
||||
void unsubscribe_timer() override;
|
||||
|
||||
OrderKey launch_child(ChildTemplate &, Price, Quantity, bool) override;
|
||||
OrderKey launch_child(api::orderman::OrderDefinition const &, Price,
|
||||
Quantity, bool) override;
|
||||
void update_child(OrderKey , api::orderman::Update const &) override;
|
||||
void cancel_child(OrderKey) override;
|
||||
void detach_child(OrderKey) override;
|
||||
void detach_all_children() override;
|
||||
|
||||
void cancel(std::string const &) override;
|
||||
void terminate_error(std::string const &) override;
|
||||
void complete() override;
|
||||
void set_ladder_visibility(bool) override;
|
||||
void update_fill_state(Quantity , Price) override;
|
||||
|
||||
};
|
||||
|
||||
template <class Algo>
|
||||
struct AlgoTestDriver : public AlgoTestDriverBase
|
||||
{
|
||||
// Avoids mutual ownership problem
|
||||
Algo _algo;
|
||||
AlgoTestDriver(
|
||||
const api::orderman::OrderDefinition& def,
|
||||
const md::IntPrice& price, Quantity size,
|
||||
ATDFlags flags=ATDFlags::NONE)
|
||||
: AlgoTestDriverBase(_algo, def, Price {price.price()}, size, flags), _algo()
|
||||
{ }
|
||||
const Algo& algo() const { return _algo; }
|
||||
};
|
||||
|
||||
}}}//namespace
|
||||
@@ -0,0 +1,600 @@
|
||||
|
||||
#include "../SplitterAlgo.h"
|
||||
#include "AlgoTestDriver.h"
|
||||
#include "catch/catch.hpp"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include <ctime>
|
||||
#include <optional>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
#include "lib/utils/bts_chrono.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace bts::orderman::splitter;
|
||||
using namespace bts::api::orderman;
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
|
||||
/*
|
||||
|
||||
- max/exposed, assert correct # of orders on the market
|
||||
- completes on target_qty reached
|
||||
- completes on target_qty exceeded
|
||||
- send fills, check order state
|
||||
- send fills, then change target_qty
|
||||
|
||||
*/
|
||||
|
||||
constexpr auto some_price = md::IntPrice{128000, -3};
|
||||
|
||||
using ExpectedCount = utils::ValueWrapper<struct ExpectedCountTag, uint32_t>;
|
||||
struct SimpleCase {
|
||||
ExpectedCount expected_orders;
|
||||
Quantity target;
|
||||
SplitterOrderDefinition sod;
|
||||
};
|
||||
|
||||
SimpleCase mkSimple(ExpectedCount expected, uint32_t max_size, uint32_t exposed,
|
||||
uint32_t target) {
|
||||
SimpleCase result;
|
||||
result.expected_orders = expected;
|
||||
result.target = Quantity{target};
|
||||
result.sod.set_max_order_qty(max_size);
|
||||
result.sod.set_exposed_qty(exposed);
|
||||
return result;
|
||||
}
|
||||
|
||||
TEST_CASE("compute_desired_sizes fuzzing", "[algo][splitter]") {
|
||||
srand((unsigned)time(0));
|
||||
|
||||
auto start = Timer::now();
|
||||
|
||||
while (sec_diff(Timer::now(),start) < 5) {
|
||||
uint32_t total = rand() % 100 + 1;
|
||||
uint32_t clip = rand() % 4 + 1;
|
||||
std::vector<MinMax> constraints(rand() % 10);
|
||||
std::vector<Quantity> result;
|
||||
|
||||
uint32_t all_fills = 0;
|
||||
for (auto &mm : constraints) {
|
||||
uint32_t order_working = rand() % 2;
|
||||
uint32_t fills = rand() % 2 * clip;
|
||||
all_fills += fills;
|
||||
mm = {fills, order_working ? fills + rand() % clip + 1 : fills};
|
||||
}
|
||||
if (all_fills < total) {
|
||||
compute_desired_sizes(total, clip, constraints, &result);
|
||||
uint32_t exposed_qty = 0;
|
||||
for (auto q : result)
|
||||
exposed_qty += q.value();
|
||||
|
||||
bool clip_size_honored = true;
|
||||
for (auto q : result)
|
||||
clip_size_honored = clip_size_honored && (q.value() <= clip);
|
||||
|
||||
if (exposed_qty != total || !clip_size_honored) {
|
||||
std::cout << "total: " << total << "\n"
|
||||
<< "clip: " << clip << "\n{ ";
|
||||
|
||||
for (auto mm : constraints)
|
||||
std::cout << "{" << mm.min << "," << mm.max << "},";
|
||||
std::cout << " }\n";
|
||||
}
|
||||
REQUIRE(clip_size_honored);
|
||||
REQUIRE(exposed_qty == total);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("initial orders", "[algo][splitter]") {
|
||||
std::vector<SimpleCase> cases;
|
||||
cases.push_back(mkSimple(ExpectedCount{10}, 10, 100, 50));
|
||||
cases.push_back(mkSimple(ExpectedCount{11}, 10, 105, 50));
|
||||
cases.push_back(mkSimple(ExpectedCount{5}, 10, 50, 50));
|
||||
cases.push_back(mkSimple(ExpectedCount{5}, 10, 50, 150));
|
||||
|
||||
for (const auto &tcase : cases) {
|
||||
auto expected = tcase.expected_orders.value();
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
*def.mutable_custom_def()->mutable_splitter() = tcase.sod;
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, tcase.target);
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
REQUIRE(state.child_orders().size() == expected);
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("completes on target", "[algo][splitter]") {
|
||||
std::vector<SimpleCase> cases;
|
||||
cases.push_back(mkSimple(ExpectedCount{3}, 30, 90, 50));
|
||||
cases.push_back(mkSimple(ExpectedCount{10}, 10, 100, 50));
|
||||
cases.push_back(mkSimple(ExpectedCount{11}, 10, 105, 50));
|
||||
cases.push_back(mkSimple(ExpectedCount{5}, 10, 50, 50));
|
||||
cases.push_back(mkSimple(ExpectedCount{5}, 10, 50, 150));
|
||||
cases.push_back(mkSimple(ExpectedCount{4}, 13, 50, 150));
|
||||
cases.push_back(mkSimple(ExpectedCount{12}, 13, 150, 50));
|
||||
|
||||
for (const auto &tcase : cases) {
|
||||
auto expected = tcase.expected_orders.value();
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
*def.mutable_custom_def()->mutable_splitter() = tcase.sod;
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, tcase.target);
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
REQUIRE(state.child_orders().size() == expected);
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
|
||||
SECTION("Fill completely") {
|
||||
auto orders = state.child_orders();
|
||||
// Fill the order
|
||||
size_t filled = 0;
|
||||
size_t iterations = 0;
|
||||
while (state.state() == AlgoState::RUNNING) {
|
||||
// loop to allow more orders to be added
|
||||
++iterations;
|
||||
REQUIRE(iterations < 999);
|
||||
for (const auto &order : orders) {
|
||||
state.fill(order.key, Price(some_price.price()),
|
||||
order.cstate.quantity);
|
||||
filled += order.cstate.quantity.value();
|
||||
REQUIRE(filled == state.total_fills().value());
|
||||
if (filled >= tcase.target.value()) {
|
||||
REQUIRE(state.state() == AlgoState::COMPLETED);
|
||||
break;
|
||||
} else {
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SECTION("fill one by one") {
|
||||
const auto &orders = state.child_orders();
|
||||
// Fill 1 per iteration.
|
||||
// Algo should add more orders if not enough are visible initially
|
||||
size_t filled = 0;
|
||||
size_t iterations = 0;
|
||||
while (state.state() == AlgoState::RUNNING) {
|
||||
++iterations;
|
||||
REQUIRE(iterations < 999); // not making progress
|
||||
for (const auto &order : orders) {
|
||||
if (order.cstate.status == ChildStatus::WORKING) {
|
||||
auto price = Price{some_price.price()};
|
||||
state.fill(order.key, price, Quantity{1});
|
||||
filled += 1;
|
||||
REQUIRE(filled == state.total_fills().value());
|
||||
// Fills should never cause new orders to get added
|
||||
REQUIRE(state.child_orders().size() == expected);
|
||||
if (filled >= tcase.target.value()) {
|
||||
REQUIRE(state.state() == AlgoState::COMPLETED);
|
||||
break;
|
||||
} else {
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("updates", "[algo][splitter]") {
|
||||
// Start in state 0, update to state 1
|
||||
std::vector<std::tuple<SimpleCase, std::vector<uint32_t>, SimpleCase>> cases;
|
||||
|
||||
// reduce max order size, increase # orders
|
||||
cases.push_back({mkSimple(ExpectedCount{10} /*order count*/, 10 /*max*/,
|
||||
100 /*exposed*/, 50 /*target*/),
|
||||
{},
|
||||
mkSimple(ExpectedCount{20}, 5, 100, 50)});
|
||||
/// reduce max order size, keep # orders constant
|
||||
cases.push_back({mkSimple(ExpectedCount{10}, 10, 100, 50),
|
||||
{},
|
||||
mkSimple(ExpectedCount{10}, 5, 50, 50)});
|
||||
cases.push_back({mkSimple(ExpectedCount{10}, 10, 100, 50),
|
||||
{5, 5},
|
||||
mkSimple(ExpectedCount{8}, 5, 50, 40)});
|
||||
cases.push_back({mkSimple(ExpectedCount{10}, 10, 100, 50),
|
||||
{5, 5},
|
||||
mkSimple(ExpectedCount{18}, 5, 100, 50)});
|
||||
|
||||
for (const auto &tcase : cases) {
|
||||
auto case_init = std::get<0>(tcase);
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
*def.mutable_custom_def()->mutable_splitter() = case_init.sod;
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, case_init.target);
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == case_init.expected_orders.value());
|
||||
for (const auto &o : state.child_orders()) {
|
||||
if (o.cstate.status == ChildStatus::WORKING) {
|
||||
REQUIRE(o.cstate.quantity.value() <= case_init.sod.exposed_qty());
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t fills = 0;
|
||||
for (auto q : std::get<1>(tcase)) {
|
||||
state.fill({}, Quantity{q});
|
||||
fills += q;
|
||||
}
|
||||
|
||||
REQUIRE(state.total_fills().value() == fills);
|
||||
|
||||
Update update;
|
||||
auto case_next = std::get<2>(tcase);
|
||||
update.set_price(some_price.price());
|
||||
update.set_quantity(case_next.target.value());
|
||||
*def.mutable_custom_def()->mutable_splitter() = case_next.sod;
|
||||
*update.mutable_custom() = def.custom_def();
|
||||
|
||||
state.do_update(update);
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
size_t working = 0;
|
||||
for (const auto &c : state.child_orders())
|
||||
if (c.cstate.status == ChildStatus::WORKING)
|
||||
working += 1;
|
||||
REQUIRE(working == case_next.expected_orders.value());
|
||||
for (const auto &o : state.child_orders()) {
|
||||
if (o.cstate.status == ChildStatus::WORKING) {
|
||||
REQUIRE(o.cstate.quantity.value() <= case_next.sod.exposed_qty());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_CASE("Reduction with fills", "[algo][splitter]") {
|
||||
// Fills represent a floor on how much a trade can be reduced
|
||||
// IF we have expose:400, clip: 100 we get
|
||||
// 100,100,100,100
|
||||
// Receive fills
|
||||
// 100/50, 100/50, 100/50, 100/50
|
||||
// Change expose to 300
|
||||
// naive: 100, 100, 100 but we *can't* reduce the last to 0
|
||||
// expected: 100, 100, 50, 50
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
auto *splitter = def.mutable_custom_def()->mutable_splitter();
|
||||
splitter->set_max_order_qty(100);
|
||||
splitter->set_exposed_qty(400);
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, Quantity{300});
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
// check initial state
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
for (const auto &c : state.child_orders()) {
|
||||
REQUIRE(c.cstate.quantity.value() == 100);
|
||||
}
|
||||
|
||||
// Add fills
|
||||
for (const auto &order : state.child_orders()) {
|
||||
state.fill(order.key, {}, Quantity{50});
|
||||
}
|
||||
|
||||
// Reduce exposed quantity
|
||||
Update update;
|
||||
update.set_quantity(300);
|
||||
update.mutable_custom()->mutable_splitter()->set_exposed_qty(300);
|
||||
state.do_update(update);
|
||||
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
size_t total = 0;
|
||||
for (const auto &order : state.child_orders()) {
|
||||
total += order.cstate.quantity.value();
|
||||
}
|
||||
REQUIRE(total == 300);
|
||||
|
||||
// need this to be 100,100,50,50 not 100,100,100
|
||||
std::set<uint32_t> actual, expected{100, 100, 50, 50};
|
||||
for (const auto &order : state.child_orders()) {
|
||||
actual.insert(order.cstate.quantity.value());
|
||||
}
|
||||
REQUIRE(actual == expected);
|
||||
|
||||
REQUIRE(state.total_fills().value() == 200);
|
||||
}
|
||||
|
||||
TEST_CASE("Reduction with fills 2", "[algo][splitter]") {
|
||||
// Fills represent a floor on how much a trade can be reduced
|
||||
// IF we have expose:400, clip: 100 we get
|
||||
// 100,100,100,100
|
||||
// Receive fills
|
||||
// 100/35, 100/45, 100/55, 100/65
|
||||
// Change expose to 300
|
||||
// naive: 100, 100, 100 but we *can't* reduce the last to 0
|
||||
// expected: 100, 100, 65, 35
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
auto *splitter = def.mutable_custom_def()->mutable_splitter();
|
||||
splitter->set_max_order_qty(100);
|
||||
splitter->set_exposed_qty(400);
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, Quantity{300});
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
// check initial state
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
for (const auto &c : state.child_orders()) {
|
||||
REQUIRE(c.cstate.quantity.value() == 100);
|
||||
}
|
||||
|
||||
// Add fills
|
||||
state.fill({}, Quantity{35});
|
||||
state.fill({}, Quantity{45});
|
||||
state.fill({}, Quantity{55});
|
||||
state.fill({}, Quantity{65});
|
||||
|
||||
// Reduce exposed quantity
|
||||
Update update;
|
||||
update.set_quantity(300);
|
||||
update.mutable_custom()->mutable_splitter()->set_exposed_qty(300);
|
||||
state.do_update(update);
|
||||
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
size_t total = 0;
|
||||
for (const auto &order : state.child_orders()) {
|
||||
total += order.cstate.quantity.value();
|
||||
}
|
||||
REQUIRE(total == 300);
|
||||
|
||||
// need this to be 100,100,50,50 not 100,100,100
|
||||
std::set<uint32_t> actual, expected{100, 100, 65, 35};
|
||||
for (const auto &order : state.child_orders()) {
|
||||
actual.insert(order.cstate.quantity.value());
|
||||
}
|
||||
REQUIRE(actual == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("Reduction with fills 3", "[algo][splitter]") {
|
||||
// Fills represent a floor on how much a trade can be reduced
|
||||
// IF we have expose:400, clip: 100 we get
|
||||
// 100,100,100,100
|
||||
// Receive fills
|
||||
// 100/35, 100/45, 100/55, 100/65
|
||||
// Change expose to 201
|
||||
// naive: 100, 100, 1 but we *can't* reduce the last to 0
|
||||
// expected: 66, 55, 45, 35
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
auto *splitter = def.mutable_custom_def()->mutable_splitter();
|
||||
splitter->set_max_order_qty(100);
|
||||
splitter->set_exposed_qty(400);
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, Quantity{300});
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
// check initial state
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
for (const auto &c : state.child_orders()) {
|
||||
REQUIRE(c.cstate.quantity.value() == 100);
|
||||
}
|
||||
|
||||
// Add fills
|
||||
state.fill({}, Quantity{35});
|
||||
state.fill({}, Quantity{45});
|
||||
state.fill({}, Quantity{55});
|
||||
state.fill({}, Quantity{65});
|
||||
|
||||
// Reduce exposed quantity
|
||||
Update update;
|
||||
update.set_quantity(201);
|
||||
update.mutable_custom()->mutable_splitter()->set_exposed_qty(201);
|
||||
state.do_update(update);
|
||||
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
size_t total = 0;
|
||||
for (const auto &order : state.child_orders()) {
|
||||
total += order.cstate.quantity.value();
|
||||
}
|
||||
REQUIRE(total == 201);
|
||||
|
||||
std::set<uint32_t> actual, expected{66, 55, 45, 35};
|
||||
for (const auto &order : state.child_orders()) {
|
||||
actual.insert(order.cstate.quantity.value());
|
||||
}
|
||||
REQUIRE(actual == expected);
|
||||
}
|
||||
|
||||
TEST_CASE("reduce to completion", "[algo][splitter]") {
|
||||
// Fills represent a floor on how much a trade can be reduced
|
||||
// IF we have expose:400, clip: 100 we get
|
||||
// 100,100,100,100
|
||||
// Receive fills
|
||||
// 100/100, 100/100, 100/0, 100/00
|
||||
// Change expose to 200
|
||||
// expected: algo transitions to copmleted state canceling 2 orders
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
auto *splitter = def.mutable_custom_def()->mutable_splitter();
|
||||
splitter->set_max_order_qty(100);
|
||||
splitter->set_exposed_qty(400);
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, Quantity{300});
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
// check initial state
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
for (const auto &c : state.child_orders()) {
|
||||
REQUIRE(c.cstate.quantity.value() == 100);
|
||||
}
|
||||
|
||||
// Add fills
|
||||
state.fill({}, Quantity{100});
|
||||
state.fill({}, Quantity{100});
|
||||
|
||||
// Reduce exposed quantity
|
||||
Update update;
|
||||
update.set_quantity(200);
|
||||
def.mutable_custom_def()->mutable_splitter()->set_exposed_qty(200);
|
||||
state.do_update(update);
|
||||
|
||||
REQUIRE(state.state() == AlgoState::COMPLETED);
|
||||
size_t total = 0;
|
||||
for (const auto &order : state.child_orders()) {
|
||||
if (order.cstate.status == ChildStatus::WORKING) {
|
||||
total += order.cstate.quantity.value();
|
||||
} else {
|
||||
total += order.cstate.total_fills.value();
|
||||
}
|
||||
}
|
||||
REQUIRE(total == 200);
|
||||
}
|
||||
|
||||
TEST_CASE("reduce to completion 2", "[algo][splitter]") {
|
||||
// Fills represent a floor on how much a trade can be reduced
|
||||
// IF we have expose:400, clip: 100 we get
|
||||
// 100,100,100,100
|
||||
// Receive fills
|
||||
// 100/100, 100/100, 100/0, 100/00
|
||||
// Change expose to 200
|
||||
// expected: algo transitions to copmleted state canceling 2 orders
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
auto *splitter = def.mutable_custom_def()->mutable_splitter();
|
||||
splitter->set_max_order_qty(100);
|
||||
splitter->set_exposed_qty(400);
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, Quantity{300});
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
// check initial state
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
for (const auto &c : state.child_orders()) {
|
||||
REQUIRE(c.cstate.quantity.value() == 100);
|
||||
}
|
||||
|
||||
// Add fills
|
||||
state.fill({}, Quantity{100});
|
||||
state.fill({}, Quantity{50});
|
||||
state.fill({}, Quantity{50});
|
||||
|
||||
// Reduce exposed quantity
|
||||
Update update;
|
||||
update.set_quantity(200);
|
||||
state.do_update(update);
|
||||
|
||||
REQUIRE(state.state() == AlgoState::COMPLETED);
|
||||
}
|
||||
|
||||
TEST_CASE("Async Fills", "[algo][splitter]") {
|
||||
// OK, NOW it gets tricky because we're going to test the asyncronous stuff
|
||||
|
||||
// Start with 4 orders size 20 outstanding (80 total exposed)
|
||||
// Change exposure to 64 so one of the four will be reduced to 5
|
||||
// Before the change is confirmed fill the one that is reducing by 7
|
||||
// which would constitute an overfill
|
||||
// Algo must reduce a second order to compensate
|
||||
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
auto *splitter = def.mutable_custom_def()->mutable_splitter();
|
||||
splitter->set_max_order_qty(20);
|
||||
splitter->set_exposed_qty(80);
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, Quantity{80});
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
// check initial state
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 4);
|
||||
for (const auto &c : state.child_orders()) {
|
||||
REQUIRE(c.cstate.quantity.value() == 20);
|
||||
}
|
||||
|
||||
// Set up some instant fills
|
||||
bool applied_fills = false;
|
||||
state.on_child_changing([&](const auto &) {
|
||||
if (applied_fills)
|
||||
return;
|
||||
applied_fills = true;
|
||||
// 7 fills on the last order which is has a size reduction in flight
|
||||
state.fill(state.child_orders().rbegin()->key, {}, Quantity{7});
|
||||
});
|
||||
|
||||
Update update;
|
||||
update.set_price(state.price().value());
|
||||
update.set_quantity(65);
|
||||
update.mutable_custom()->mutable_splitter()->set_exposed_qty(65);
|
||||
// This should trigger 1 order to be reduced to size 5 from 20
|
||||
// in the meantime we'll trigger fills of size 7
|
||||
// This results in an overfill for the 5 lot order
|
||||
// And the
|
||||
state.do_update(update);
|
||||
|
||||
// expected result:
|
||||
// 7 fills, exposed_qty 65 = 58 active
|
||||
uint32_t active = 0;
|
||||
for (const auto &o : state.child_orders())
|
||||
active += o.active_qty();
|
||||
REQUIRE(active == 58);
|
||||
}
|
||||
|
||||
TEST_CASE("Increase then decrease", "[algo][splitter]") {
|
||||
// Start with 3 orders of size 100
|
||||
// Increase the size to 400 so we launch a 4th order
|
||||
// Then instantly send another update to 350
|
||||
|
||||
// NOTE: this test is not very good due to limitations in the test driver
|
||||
// I'm leaving it in because it passes but if it starts failing in the future
|
||||
// it's probably OK to disable it
|
||||
|
||||
api::orderman::OrderDefinition def;
|
||||
def.set_side(api::SIDE_BUY);
|
||||
auto *splitter = def.mutable_custom_def()->mutable_splitter();
|
||||
splitter->set_max_order_qty(100);
|
||||
splitter->set_exposed_qty(300);
|
||||
AlgoTestDriver<SplitterAlgo> state(def, some_price, Quantity{300});
|
||||
|
||||
REQUIRE(state.before_launch().ok());
|
||||
state.launch();
|
||||
// check initial state
|
||||
REQUIRE(state.state() == AlgoState::RUNNING);
|
||||
REQUIRE(state.child_orders().size() == 3);
|
||||
for (const auto &c : state.child_orders()) {
|
||||
REQUIRE(c.cstate.quantity.value() == 100);
|
||||
}
|
||||
|
||||
Update update;
|
||||
update.set_price(state.price().value());
|
||||
update.set_quantity(state.quantity().value());
|
||||
update.mutable_custom()->mutable_splitter()->set_exposed_qty(400);
|
||||
|
||||
// Set up some instant fills
|
||||
bool applied_second_update = false;
|
||||
state.on_child_launching([&, update](const auto &) mutable {
|
||||
if (!applied_second_update) {
|
||||
applied_second_update = true;
|
||||
update.mutable_custom()->mutable_splitter()->set_exposed_qty(350);
|
||||
state.do_update(update);
|
||||
}
|
||||
});
|
||||
|
||||
state.do_update(update);
|
||||
|
||||
// expected result:
|
||||
// 350 exposed, NOT 450
|
||||
uint32_t quantity = 0;
|
||||
for (const auto &o : state.child_orders())
|
||||
quantity += o.active_qty();
|
||||
REQUIRE(quantity == 350);
|
||||
}
|
||||
|
||||
} // namespace test
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -24,6 +24,7 @@ public:
|
||||
bool operator==(ValueWrapper const&v) const {return _v == v._v;}
|
||||
bool operator!=(ValueWrapper const&v) const {return _v != v._v;}
|
||||
bool operator<(ValueWrapper const&v) const {return _v < v._v;}
|
||||
bool operator>(ValueWrapper const&v) const {return _v > v._v;}
|
||||
ValueT const& value() const {return _v;}
|
||||
ValueT & value() {return _v;}
|
||||
private:
|
||||
|
||||
Reference in new issue
Block a user