Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6139d53e49 | ||
|
|
7f3c2e75a3 | ||
|
|
a1faf685eb | ||
|
|
9e2b01a723 | ||
|
|
f199cb4157 | ||
|
|
5a3bfd7f81 | ||
|
|
bf6461c126 | ||
|
|
8892505d11 | ||
|
|
5d0a29432a | ||
|
|
cd01c7ae5a | ||
|
|
bf19ec0fda | ||
|
|
1a0158817e | ||
|
|
53853591fe | ||
|
|
80d4899f2c | ||
|
|
75f15c50d8 | ||
|
|
f197f213f3 | ||
|
|
a7e1b71883 | ||
|
|
f9f22e1e36 | ||
|
|
cda50f562d | ||
|
|
4010298a46 | ||
|
|
48e701ba00 | ||
|
|
cb07abbf42 | ||
|
|
3c8e2c636f | ||
|
|
6a3b4ebe75 | ||
|
|
80256fbe12 | ||
|
|
a0a5d82139 | ||
|
|
ee6b00b9f2 | ||
|
|
9a494ce24f | ||
|
|
ae78ebac9a | ||
|
|
5c11eecbd1 | ||
|
|
78a47c33c7 | ||
|
|
9dcd80e3db | ||
|
|
f7563413bd | ||
|
|
29c418db42 | ||
|
|
f177efde70 | ||
|
|
3b83d105a0 | ||
|
|
34aafd79cd | ||
|
|
d3de164c53 | ||
|
|
1d1c0d6b85 | ||
|
|
2f127ecc18 | ||
|
|
3fc080d02c | ||
|
|
d86543261c | ||
|
|
1a7fcb2027 |
No files matched your search
@@ -1,6 +1,8 @@
|
||||
cmake_minimum_required(VERSION 3.5)
|
||||
project(spark)
|
||||
|
||||
set (USE_ONIX "TRUE")
|
||||
|
||||
if (WIN32)
|
||||
set (platform "windows")
|
||||
else ()
|
||||
@@ -40,6 +42,11 @@ else ()
|
||||
-fPIC -Wall -pedantic -Wextra -Werror \
|
||||
-Wno-missing-braces -Wno-ignored-qualifiers \
|
||||
--std=c++17 -fdiagnostics-color")
|
||||
|
||||
if (${USE_ONIX})
|
||||
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -DUSE_ONIX")
|
||||
endif()
|
||||
|
||||
set (CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_DEBUG} -fno-omit-frame-pointer -DNDEBUG -O3")
|
||||
set (CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wno-undef")
|
||||
add_subdirectory(src/server/sparkd)
|
||||
|
||||
@@ -1 +1 @@
|
||||
set(sources /home/eric/spark/doc/user/build.sh;content/_index.md;content/bts_spark/Glossary/_index.md;content/bts_spark/Help/_index.md;content/bts_spark/Help/about_spark.png;content/bts_spark/Help/image39.png;content/bts_spark/Help/image58.png;content/bts_spark/Launcher/_index.md;content/bts_spark/Launcher/image38.png;content/bts_spark/Launcher/image68.png;content/bts_spark/Logging-In-to-the-System/_index.md;content/bts_spark/Logging-In-to-the-System/image3.png;content/bts_spark/Logging-In-to-the-System/image64.png;content/bts_spark/Market/_index.md;content/bts_spark/Market/change_securities.png;content/bts_spark/Market/image11.png;content/bts_spark/Market/image14.png;content/bts_spark/Market/image20.png;content/bts_spark/Market/image21.png;content/bts_spark/Market/image23.png;content/bts_spark/Market/image24.png;content/bts_spark/Market/image34.png;content/bts_spark/Market/image49.png;content/bts_spark/Market/image53.png;content/bts_spark/Market/image66.png;content/bts_spark/Market/image69.png;content/bts_spark/Market/market_view.png;content/bts_spark/Market/settings_icon.png;content/bts_spark/Market/time_and_sales.png;content/bts_spark/My-Windows/_index.md;content/bts_spark/My-Windows/image15.png;content/bts_spark/My-Windows/image16.png;content/bts_spark/My-Windows/image37.png;content/bts_spark/Settings/_index.md;content/bts_spark/Settings/image51.png;content/bts_spark/Settings/image9.png;content/bts_spark/Spreader/_index.md;content/bts_spark/Spreader/image10.png;content/bts_spark/Spreader/image13.png;content/bts_spark/Spreader/image18.png;content/bts_spark/Spreader/image19.png;content/bts_spark/Spreader/image28.png;content/bts_spark/Spreader/image29.png;content/bts_spark/Spreader/image31.png;content/bts_spark/Spreader/image35.png;content/bts_spark/Spreader/image40.png;content/bts_spark/Spreader/image43.png;content/bts_spark/Spreader/image54.png;content/bts_spark/Spreader/image55.png;content/bts_spark/Spreader/image56.png;content/bts_spark/Spreader/image6.png;content/bts_spark/Spreader/image7.png;content/bts_spark/Spreader/image70.png;content/bts_spark/Spreader/image72.png;content/bts_spark/Spreader/image8.png;content/bts_spark/Trading/OrderTypes/index.md;content/bts_spark/Trading/_index.md;content/bts_spark/Trading/algo_studio.png;content/bts_spark/Trading/collapse_all_icon.png;content/bts_spark/Trading/cover_algo_control.png;content/bts_spark/Trading/expand_all_icon.png;content/bts_spark/Trading/image1.png;content/bts_spark/Trading/image17.png;content/bts_spark/Trading/image2.png;content/bts_spark/Trading/image25.png;content/bts_spark/Trading/image27.png;content/bts_spark/Trading/image33.png;content/bts_spark/Trading/image42.png;content/bts_spark/Trading/image44.png;content/bts_spark/Trading/image47.png;content/bts_spark/Trading/image48.png;content/bts_spark/Trading/image5.png;content/bts_spark/Trading/image50.png;content/bts_spark/Trading/image52.png;content/bts_spark/Trading/image57.png;content/bts_spark/Trading/image59.png;content/bts_spark/Trading/image60.png;content/bts_spark/Trading/image61.png;content/bts_spark/Trading/image62.png;content/bts_spark/Trading/image73.png;content/bts_spark/Trading/image74.png;content/bts_spark/Trading/join_algo_control.png;content/bts_spark/Trading/ladder.png;content/bts_spark/Trading/ladder_badges.png;content/bts_spark/Trading/ladder_main_part.png;content/bts_spark/Trading/lean_algo_control.png;content/bts_spark/Trading/mini_orderbook.png;content/bts_spark/Trading/oco_algo_control.png;content/bts_spark/Trading/offset_algo_control.png;content/bts_spark/Trading/pnl_calculation.png;content/bts_spark/Trading/pnl_window.png;content/bts_spark/Trading/recreate_icon.png;content/bts_spark/Trading/settings_icon.png;content/bts_spark/Trading/spark_menu_order_book.png;content/bts_spark/Trading/top_algo_control.png;content/bts_spark/Trading/trigger_algo_control.png;content/bts_spark/_index.md;content/release_notes/_index.md;content/system_requirements/_index.md;static/book.js;static/favicon.ico;static/image.js;templates/index.html;templates/page.html;templates/section.html;templates/shortcodes/back_button.html)
|
||||
set(sources /home/ppinto/Dropbox/bts/spark/doc/user/build.sh;content/_index.md;content/bts_spark/Glossary/_index.md;content/bts_spark/Help/_index.md;content/bts_spark/Help/about_spark.png;content/bts_spark/Help/image39.png;content/bts_spark/Help/image58.png;content/bts_spark/Launcher/_index.md;content/bts_spark/Launcher/image38.png;content/bts_spark/Launcher/image68.png;content/bts_spark/Logging-In-to-the-System/_index.md;content/bts_spark/Logging-In-to-the-System/image3.png;content/bts_spark/Logging-In-to-the-System/image64.png;content/bts_spark/Market/_index.md;content/bts_spark/Market/change_securities.png;content/bts_spark/Market/image11.png;content/bts_spark/Market/image14.png;content/bts_spark/Market/image20.png;content/bts_spark/Market/image21.png;content/bts_spark/Market/image23.png;content/bts_spark/Market/image24.png;content/bts_spark/Market/image34.png;content/bts_spark/Market/image49.png;content/bts_spark/Market/image53.png;content/bts_spark/Market/image66.png;content/bts_spark/Market/image69.png;content/bts_spark/Market/market_view.png;content/bts_spark/Market/settings_icon.png;content/bts_spark/Market/time_and_sales.png;content/bts_spark/My-Windows/_index.md;content/bts_spark/My-Windows/image15.png;content/bts_spark/My-Windows/image16.png;content/bts_spark/My-Windows/image37.png;content/bts_spark/Settings/_index.md;content/bts_spark/Settings/image51.png;content/bts_spark/Settings/image9.png;content/bts_spark/Spreader/_index.md;content/bts_spark/Spreader/image10.png;content/bts_spark/Spreader/image13.png;content/bts_spark/Spreader/image18.png;content/bts_spark/Spreader/image19.png;content/bts_spark/Spreader/image28.png;content/bts_spark/Spreader/image29.png;content/bts_spark/Spreader/image31.png;content/bts_spark/Spreader/image35.png;content/bts_spark/Spreader/image40.png;content/bts_spark/Spreader/image43.png;content/bts_spark/Spreader/image54.png;content/bts_spark/Spreader/image55.png;content/bts_spark/Spreader/image56.png;content/bts_spark/Spreader/image6.png;content/bts_spark/Spreader/image7.png;content/bts_spark/Spreader/image70.png;content/bts_spark/Spreader/image72.png;content/bts_spark/Spreader/image8.png;content/bts_spark/Trading/OrderTypes/index.md;content/bts_spark/Trading/_index.md;content/bts_spark/Trading/algo_studio.png;content/bts_spark/Trading/collapse_all_icon.png;content/bts_spark/Trading/cover_algo_control.png;content/bts_spark/Trading/expand_all_icon.png;content/bts_spark/Trading/image1.png;content/bts_spark/Trading/image17.png;content/bts_spark/Trading/image2.png;content/bts_spark/Trading/image25.png;content/bts_spark/Trading/image27.png;content/bts_spark/Trading/image33.png;content/bts_spark/Trading/image42.png;content/bts_spark/Trading/image44.png;content/bts_spark/Trading/image47.png;content/bts_spark/Trading/image48.png;content/bts_spark/Trading/image5.png;content/bts_spark/Trading/image50.png;content/bts_spark/Trading/image52.png;content/bts_spark/Trading/image57.png;content/bts_spark/Trading/image59.png;content/bts_spark/Trading/image60.png;content/bts_spark/Trading/image61.png;content/bts_spark/Trading/image62.png;content/bts_spark/Trading/image73.png;content/bts_spark/Trading/image74.png;content/bts_spark/Trading/join_algo_control.png;content/bts_spark/Trading/ladder.png;content/bts_spark/Trading/ladder_badges.png;content/bts_spark/Trading/ladder_main_part.png;content/bts_spark/Trading/lean_algo_control.png;content/bts_spark/Trading/mini_orderbook.png;content/bts_spark/Trading/oco_algo_control.png;content/bts_spark/Trading/offset_algo_control.png;content/bts_spark/Trading/pnl_calculation.png;content/bts_spark/Trading/pnl_window.png;content/bts_spark/Trading/recreate_icon.png;content/bts_spark/Trading/settings_icon.png;content/bts_spark/Trading/spark_menu_order_book.png;content/bts_spark/Trading/top_algo_control.png;content/bts_spark/Trading/trigger_algo_control.png;content/bts_spark/_index.md;content/release_notes/_index.md;content/system_requirements/_index.md;static/book.js;static/favicon.ico;static/image.js;templates/index.html;templates/page.html;templates/section.html;templates/shortcodes/back_button.html)
|
||||
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
@@ -0,0 +1,51 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Documents;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using System.Windows.Navigation;
|
||||
using System.Windows.Shapes;
|
||||
|
||||
namespace RiskUI {
|
||||
/// <summary>
|
||||
/// Follow steps 1a or 1b and then 2 to use this custom control in a XAML file.
|
||||
///
|
||||
/// Step 1a) Using this custom control in a XAML file that exists in the current project.
|
||||
/// Add this XmlNamespace attribute to the root element of the markup file where it is
|
||||
/// to be used:
|
||||
///
|
||||
/// xmlns:MyNamespace="clr-namespace:RiskUI"
|
||||
///
|
||||
///
|
||||
/// Step 1b) Using this custom control in a XAML file that exists in a different project.
|
||||
/// Add this XmlNamespace attribute to the root element of the markup file where it is
|
||||
/// to be used:
|
||||
///
|
||||
/// xmlns:MyNamespace="clr-namespace:RiskUI;assembly=RiskUI"
|
||||
///
|
||||
/// You will also need to add a project reference from the project where the XAML file lives
|
||||
/// to this project and Rebuild to avoid compilation errors:
|
||||
///
|
||||
/// Right click on the target project in the Solution Explorer and
|
||||
/// "Add Reference"->"Projects"->[Select this project]
|
||||
///
|
||||
///
|
||||
/// Step 2)
|
||||
/// Go ahead and use your control in the XAML file.
|
||||
///
|
||||
/// <MyNamespace:CustomControl1/>
|
||||
///
|
||||
/// </summary>
|
||||
public class CustomControl1 : Control {
|
||||
static CustomControl1() {
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(CustomControl1), new FrameworkPropertyMetadata(typeof(CustomControl1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net471</TargetFramework>
|
||||
<RootNamespace>bts</RootNamespace>
|
||||
<LangVersion>7.2</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<WarningsAsErrors />
|
||||
<Version>$(major).$(minor).$(patch)</Version>
|
||||
<Version Condition="'$(version)'=='..'">1.0.0</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="PresentationCore" />
|
||||
<Reference Include="PresentationFramework" />
|
||||
<Reference Include="System.Xaml" />
|
||||
<Reference Include="WindowsBase" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../spark_api/spark_api.csproj" />
|
||||
<ProjectReference Include="../Spark/Spark.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,17 @@
|
||||
<ResourceDictionary
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:RiskUI">
|
||||
<Style TargetType="{x:Type local:CustomControl1}">
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="{x:Type local:CustomControl1}">
|
||||
<Border Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}">
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
</ResourceDictionary>
|
||||
@@ -1,24 +1,33 @@
|
||||
using bts;
|
||||
using bts.admin.risk;
|
||||
using bts.api;
|
||||
using bts.api.admin;
|
||||
using bts.api.core;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.ComponentModel.Composition;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace mbts.spark {
|
||||
[Export]
|
||||
class AdminModel : IDisposable {
|
||||
public readonly UserTuple user;
|
||||
|
||||
readonly AdminService _service;
|
||||
readonly ErrorReporter _error_reporter;
|
||||
IDisposable _dispose;
|
||||
IReadOnlyDictionary<uint, string> _account_map = DictionaryHelper.empty<uint, string>();
|
||||
|
||||
[ImportingConstructor]
|
||||
AdminModel(AdminService service, UserTuple user) {
|
||||
AdminModel(AdminService service, UserTuple user, SecurityStore security_store, ErrorReporter error_reporter) {
|
||||
this.user = user;
|
||||
_service = service;
|
||||
_error_reporter = error_reporter;
|
||||
// accounts
|
||||
on_accounts = service.subscribei_admin_data()
|
||||
.Select(up => up.all.TryGetFirst(out var ad) && ad.Users.Where(u => u.Name == user.name).TryGetSingle(out var usr) && usr.HasUserData && usr.UserData.HasAccountPermissions ?
|
||||
@@ -31,6 +40,34 @@ namespace mbts.spark {
|
||||
new Dictionary<uint, string>(0))
|
||||
.Replay(1)
|
||||
.RefCount();
|
||||
// account risk limits
|
||||
account_security_risk_limits = service.subscribei_admin_data().Where(ad => ad.all.Any())
|
||||
.Select(upd => {
|
||||
AdminDataImmutable admin_data = upd.all.First();
|
||||
return admin_data.Accounts.SelectMany(acc =>
|
||||
(acc.HasProprietaryRiskLimits ? acc.ProprietaryRiskLimits.SecurityLimits : Enumerable.Empty<SecurityRiskLimitUnitImmutable>())
|
||||
.Select(api_limit => AccountSecurityRiskLimitsDataHelper.to_client_data(acc.Id, api_limit)))
|
||||
.ToDictionary(AccountSecurityRiskLimitsDataHelper.key_fn);
|
||||
})
|
||||
.Table(AccountSecurityRiskLimitsDataHelper.key_fn);
|
||||
// this isn't the most efficient join, but the SecurityStore join is more complex than a simple
|
||||
// left join operation, because missing securities are retrieved asynchronously, then merged back into the output sequence.
|
||||
// Until it is known that this inefficiency is problematic, best to leave the battle-tested SecurityStore code alone.
|
||||
account_security_risk_limits = security_store.join<
|
||||
TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>,
|
||||
AccountSecurityRiskLimits,
|
||||
AccountSecurityKey,
|
||||
TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>>(account_security_risk_limits,
|
||||
cli_lim => cli_lim.security_key,
|
||||
table => (table.state.Values, table.added, table.changes, table.removed, AccountSecurityRiskLimitsDataHelper.key_fn),
|
||||
(all, added, changes, removed, key_fn) =>
|
||||
new TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>(
|
||||
ImmutableDictionary.CreateRange<AccountSecurityKey, AccountSecurityRiskLimits>(
|
||||
all.Select(tup => new KeyValuePair<AccountSecurityKey, AccountSecurityRiskLimits>(tup.val.key, risk_lim_with_sec(tup)))),
|
||||
added.Select(risk_lim_with_sec),
|
||||
changes.Select(ch => new Change<AccountSecurityRiskLimits>(risk_lim_with_sec(ch.Old), risk_lim_with_sec(ch.New))),
|
||||
removed.Select(risk_lim_with_sec),
|
||||
AccountSecurityRiskLimitsDataHelper.key_fn));
|
||||
// users
|
||||
on_users = Observable.Return(DictionaryHelper.wrap_value(user.id, user.name));
|
||||
all_user_tuples = user.permissions == UserTuple.Permissions.Admin ?
|
||||
@@ -45,12 +82,16 @@ namespace mbts.spark {
|
||||
.Cache() :
|
||||
Observable.Return(TableUpdate<UserTuple, uint>.mk_empty(UserTuple.key_fn).with_added(new[] { user }));
|
||||
_dispose = on_accounts.Subscribe(map => _account_map = map);
|
||||
// local methods
|
||||
AccountSecurityRiskLimits risk_lim_with_sec((SecurityImmutable? sec, AccountSecurityRiskLimits val) tup) =>
|
||||
tup.val.with(security: tup.sec);
|
||||
}
|
||||
|
||||
public IObservable<IReadOnlyDictionary<uint, string>> on_users { get; }
|
||||
public IObservable<IReadOnlyDictionary<uint, string>> on_accounts { get; }
|
||||
public IObservable<TableUpdate<UserTuple, uint>> all_user_tuples { get; }
|
||||
public IObservable<IReadOnlyList<AccountTuple>> on_account_tuples => on_accounts.Select(d => d.Select(kv => new AccountTuple(kv.Key, kv.Value)).ToListSorted(a => a.name));
|
||||
public IObservable<TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>> account_security_risk_limits { get; }
|
||||
|
||||
public IObservable<Update<T, TKey>> permission_filter<T, TKey>(IObservable<Update<T, TKey>> positions, Func<T, uint> get_account_id) where TKey : IEquatable<TKey> =>
|
||||
user.permissions == UserTuple.Permissions.Admin ?
|
||||
@@ -68,7 +109,69 @@ namespace mbts.spark {
|
||||
public AccountTuple? get_default_account() =>
|
||||
_account_map.Select(kv => new AccountTuple(kv.Key, kv.Value)).TryFirst();
|
||||
|
||||
public Task<(bool success, string error_msg)> add_or_update_risk_limits(uint account_key, IEnumerable<(uint security_key, RiskLimits limits)> new_limits) {
|
||||
return _modify_risk_limits("Updating Risk Limits", account_key, add_limits);
|
||||
//
|
||||
ProprietaryRiskLimits add_limits(ProprietaryRiskLimits prlimits) {
|
||||
foreach (var (security_key, limits) in new_limits) {
|
||||
var api_limits = AccountSecurityRiskLimitsDataHelper.from_client_data(security_key, limits);
|
||||
if (prlimits.SecurityLimits.TryGetFirstIndex(l => l.SecurityKey == security_key, out int index)) {
|
||||
prlimits.SecurityLimits[index] = api_limits;
|
||||
} else {
|
||||
prlimits.SecurityLimits.Add(api_limits);
|
||||
}
|
||||
}
|
||||
return prlimits;
|
||||
}
|
||||
}
|
||||
|
||||
public Task<(bool success, string error_msg)> delete_risk_limits(uint account_key, IEnumerable<uint> security_keys) {
|
||||
return _modify_risk_limits("Deleting Risk Limits", account_key, remove_secs);
|
||||
//
|
||||
ProprietaryRiskLimits remove_secs(ProprietaryRiskLimits prlimits) {
|
||||
var sec_key_set = new HashSet<uint>(security_keys);
|
||||
for (int i = 0; i < prlimits.SecurityLimits.Count; i++) {
|
||||
if (sec_key_set.Contains(prlimits.SecurityLimits[i].SecurityKey))
|
||||
prlimits.SecurityLimits.RemoveAt(i);
|
||||
}
|
||||
return prlimits;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
Disposable.dispose(ref _dispose);
|
||||
|
||||
async Task<(bool success, string error_msg)> _modify_risk_limits(string op_desc, uint account_key, Func<ProprietaryRiskLimits, ProprietaryRiskLimits> modify) {
|
||||
var api_limits = new ProprietaryRiskLimits();
|
||||
var snapshot = await _get_current_admin_data_async();
|
||||
if (!snapshot.HasValue)
|
||||
return (false, "Timed out waiting for data from server.");
|
||||
var mutable_snap = snapshot.Value.CloneToMutable();
|
||||
if (!snapshot.Value.Accounts.TryGetFirst(acc => acc.Id == account_key, out var target_acci)) {
|
||||
return (false, $"Account id '{account_key}' not found.");
|
||||
}
|
||||
var prlimits = target_acci.HasProprietaryRiskLimits ? target_acci.ProprietaryRiskLimits.CloneToMutable() : new ProprietaryRiskLimits();
|
||||
prlimits = modify(prlimits);
|
||||
if (prlimits is null) {
|
||||
return (false, "Client-side modification function returned null.");
|
||||
}
|
||||
var req = new ProprietaryRiskLimitsAPIArgument {
|
||||
AccountId = account_key,
|
||||
ProprietaryRiskLimits = prlimits
|
||||
};
|
||||
var reply = await _error_reporter.safe_execute<AdminServiceOperationResult>(
|
||||
() => _service.update_proprietary_risk_limits_async(req),
|
||||
r => r.IsSuccess,
|
||||
r => r.ErrorText,
|
||||
op_desc);
|
||||
return (reply.success, reply.value.ErrorText);
|
||||
}
|
||||
|
||||
async Task<AdminDataImmutable?> _get_current_admin_data_async() =>
|
||||
await _service.subscribei_admin_data()
|
||||
.Where(upd => upd.all.Any())
|
||||
.Select(upd => (AdminDataImmutable?)upd.all.First())
|
||||
.FirstOrDefaultAsync(TimeSpan.FromSeconds(5));
|
||||
//async Task<AdminDataImmutable?>
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
using bts.api;
|
||||
using bts.api.admin;
|
||||
using bts.utils;
|
||||
using System;
|
||||
|
||||
namespace bts.admin.risk {
|
||||
readonly struct AccountSecurityKey : IEquatable<AccountSecurityKey> {
|
||||
public AccountSecurityKey(uint account, uint security) {
|
||||
this.account = account;
|
||||
this.security = security;
|
||||
}
|
||||
|
||||
public uint account { get; }
|
||||
public uint security { get; }
|
||||
|
||||
public bool Equals(AccountSecurityKey other) =>
|
||||
security == other.security &&
|
||||
account == other.account;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is AccountSecurityKey other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase(security.GetHashCode(), account.GetHashCode());
|
||||
}
|
||||
|
||||
readonly struct AccountSecurityRiskLimits : IEquatable<AccountSecurityRiskLimits> {
|
||||
readonly AccountTuple? _account;
|
||||
|
||||
public AccountSecurityRiskLimits(AccountSecurityKey key, RiskLimits limits, SecurityImmutable? security = null, AccountTuple? account = null) {
|
||||
this.key = key;
|
||||
this.limits = limits;
|
||||
this.security = security;
|
||||
_account = account;
|
||||
}
|
||||
|
||||
public AccountSecurityKey key { get; }
|
||||
public uint account_key => key.account;
|
||||
public uint security_key => key.security;
|
||||
public string account_name => _account?.name;
|
||||
public SecurityImmutable? security { get; }
|
||||
public RiskLimits limits { get; }
|
||||
|
||||
public bool Equals(AccountSecurityRiskLimits other) =>
|
||||
key.Equals(other.key) &&
|
||||
limits.Equals(other.limits);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is AccountSecurityRiskLimits other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase(key.GetHashCode(), limits.GetHashCode());
|
||||
}
|
||||
|
||||
readonly struct RiskLimits : IEquatable<RiskLimits> {
|
||||
public RiskLimits(
|
||||
bool is_position_limit_enabled,
|
||||
uint max_long_position,
|
||||
uint max_short_position,
|
||||
bool is_clip_limit_enabled,
|
||||
uint clip_limit,
|
||||
bool is_price_limit_enabled,
|
||||
double min_price,
|
||||
double max_price) {
|
||||
//
|
||||
this.is_position_limit_enabled = is_position_limit_enabled;
|
||||
this.max_long_position = max_long_position;
|
||||
this.max_short_position = max_short_position;
|
||||
this.is_clip_limit_enabled = is_clip_limit_enabled;
|
||||
this.clip_limit = clip_limit;
|
||||
this.is_price_limit_enabled = is_price_limit_enabled;
|
||||
this.min_price = min_price;
|
||||
this.max_price = max_price;
|
||||
}
|
||||
|
||||
// position
|
||||
public bool is_position_limit_enabled { get; }
|
||||
public uint max_long_position { get; }
|
||||
public uint max_short_position { get; }
|
||||
// clip
|
||||
public bool is_clip_limit_enabled { get; }
|
||||
public uint clip_limit { get; }
|
||||
// price
|
||||
public bool is_price_limit_enabled { get; }
|
||||
public double min_price { get; }
|
||||
public double max_price { get; }
|
||||
|
||||
public bool Equals(RiskLimits other) =>
|
||||
is_position_limit_enabled == other.is_position_limit_enabled &&
|
||||
max_long_position == other.max_long_position &&
|
||||
max_short_position == other.max_short_position &&
|
||||
is_clip_limit_enabled == other.is_clip_limit_enabled &&
|
||||
clip_limit == other.clip_limit &&
|
||||
is_price_limit_enabled == other.is_price_limit_enabled &&
|
||||
min_price == other.min_price &&
|
||||
max_price == other.max_price;
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is RiskLimits other && Equals(other);
|
||||
|
||||
public override int GetHashCode() {
|
||||
const int non_bin_bits = 28;
|
||||
// Evenly split the non-binary props across the first 28 bits.
|
||||
// The last 3 bits are used to represent each of the 'is_enabled' (binary) props.
|
||||
var hash = EqualityComparer.auto_phase(new[] {
|
||||
max_long_position.GetHashCode(),
|
||||
max_short_position.GetHashCode(),
|
||||
clip_limit.GetHashCode(),
|
||||
min_price.GetHashCode(),
|
||||
max_price.GetHashCode()
|
||||
}, non_bin_bits);
|
||||
if (is_position_limit_enabled)
|
||||
hash |= 1 << (non_bin_bits + 1);
|
||||
if (is_clip_limit_enabled)
|
||||
hash |= 1 << (non_bin_bits + 2);
|
||||
if (is_price_limit_enabled)
|
||||
hash |= 1 << (non_bin_bits + 3);
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
|
||||
static class AccountSecurityRiskLimitsDataHelper {
|
||||
public static readonly Func<AccountSecurityRiskLimits, AccountSecurityKey> key_fn =
|
||||
a => a.key;
|
||||
|
||||
// Don't make this a constructor. Subscribers to this data are discouraged
|
||||
// from using the server side data (client/server coupling should be minimal).
|
||||
public static AccountSecurityRiskLimits to_client_data(uint account_key, SecurityRiskLimitUnitImmutable api_data) =>
|
||||
new AccountSecurityRiskLimits(
|
||||
new AccountSecurityKey(account_key, api_data.SecurityKey),
|
||||
new RiskLimits(
|
||||
api_data.EnforceNetPosLimits,
|
||||
api_data.NetLongPositionLimit,
|
||||
api_data.NetShortPositionLimit,
|
||||
api_data.EnforceClipLimits,
|
||||
api_data.ClipSize,
|
||||
api_data.EnforcePriceBands,
|
||||
api_data.MinSellPrice,
|
||||
api_data.MaxBuyPrice));
|
||||
|
||||
public static AccountSecurityRiskLimits with(this AccountSecurityRiskLimits cli_data, RiskLimits? limits = null, AccountTuple? account = null, SecurityImmutable? security = null) =>
|
||||
new AccountSecurityRiskLimits(
|
||||
cli_data.key,
|
||||
limits ?? cli_data.limits,
|
||||
account: account ?? new AccountTuple(cli_data.account_key, cli_data.account_name),
|
||||
security: security ?? cli_data.security);
|
||||
|
||||
public static RiskLimits with(this RiskLimits limits,
|
||||
bool? is_position_limit_enabled = null,
|
||||
uint? max_long_position = null,
|
||||
uint? max_short_position = null,
|
||||
bool? is_clip_limit_enabled = null,
|
||||
uint? clip_limit = null,
|
||||
bool? is_price_limit_enabled = null,
|
||||
double? min_price = null,
|
||||
double? max_price = null) =>
|
||||
new RiskLimits(
|
||||
is_position_limit_enabled ?? limits.is_position_limit_enabled,
|
||||
max_long_position ?? limits.max_long_position,
|
||||
max_short_position ?? limits.max_short_position,
|
||||
is_clip_limit_enabled ?? limits.is_clip_limit_enabled,
|
||||
clip_limit ?? limits.clip_limit,
|
||||
is_price_limit_enabled ?? limits.is_price_limit_enabled,
|
||||
min_price ?? limits.min_price,
|
||||
max_price ?? limits.max_price);
|
||||
|
||||
public static SecurityRiskLimitUnit from_client_data(uint security_key, RiskLimits limits) =>
|
||||
new SecurityRiskLimitUnit {
|
||||
SecurityKey = security_key,
|
||||
NetLongPositionLimit = limits.max_long_position,
|
||||
NetShortPositionLimit = limits.max_short_position,
|
||||
MaxBuyPrice = limits.max_price,
|
||||
MinSellPrice = limits.min_price,
|
||||
ClipSize = limits.clip_limit,
|
||||
EnforceNetPosLimits = limits.is_position_limit_enabled,
|
||||
EnforceClipLimits = limits.is_clip_limit_enabled,
|
||||
EnforcePriceBands = limits.is_price_limit_enabled
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -276,7 +276,7 @@ namespace mbts.spark {
|
||||
ob_settings.filters = new bts.shell.orderbook.FilterStates {
|
||||
statuses = new[] { OrderStatus.Working },
|
||||
sides = new[] { side },
|
||||
security_criteria = new SecurityCriteria(security)
|
||||
security_criteria = SecurityCriteria.from_securities(security)
|
||||
};
|
||||
_bus.post(SparkTags.ShowOrderBookTag, ob_settings);
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace mbts.spark {
|
||||
if (_sorted_selected_security_criteria_backing != null) {
|
||||
_selected_security_criteria_backing = _sorted_selected_security_criteria_backing;
|
||||
} else if (_vm != null)
|
||||
_selected_security_criteria_backing = new SecurityCriteria(_vm.selected_rows.Select(r => r.security));
|
||||
_selected_security_criteria_backing = SecurityCriteria.from_securities(_vm.selected_rows.Select(r => r.security));
|
||||
}
|
||||
return _selected_security_criteria_backing;
|
||||
}
|
||||
@@ -92,7 +92,7 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
var comp = comp_builder.to_comparer();
|
||||
_sorted_selected_security_criteria_backing = new SecurityCriteria(_vm.selected_rows.Ordered(comp).Select(r => r.security));
|
||||
_sorted_selected_security_criteria_backing = SecurityCriteria.from_securities(_vm.selected_rows.Ordered(comp).Select(r => r.security));
|
||||
}
|
||||
}
|
||||
return _sorted_selected_security_criteria_backing;
|
||||
|
||||
@@ -135,7 +135,7 @@ namespace bts.shell.time_and_sales {
|
||||
.complete(selected_rows_src, always_true, get_fills_startup_data),
|
||||
// mview
|
||||
SparkCommands.ShowMarketView
|
||||
.complete(selected_rows_src, always_true, rows => rows.Count > 0 ? new SecurityCriteria(rows.Select(r => r.security)) : _security_criteria),
|
||||
.complete(selected_rows_src, always_true, rows => rows.Count > 0 ? SecurityCriteria.from_securities(rows.Select(r => r.security)) : _security_criteria),
|
||||
// pause
|
||||
toggle_is_paused_cmd,
|
||||
// change securities
|
||||
@@ -180,7 +180,7 @@ namespace bts.shell.time_and_sales {
|
||||
|
||||
FillsSettings get_fills_startup_data(ImmutableHashSet<TimeAndSalesRowVM> rows) =>
|
||||
new FillsSettings(new FillsFilterStates {
|
||||
security = rows.Count > 0 ? new SecurityCriteria(rows.Select(r => r.security)) : _security_criteria
|
||||
security = rows.Count > 0 ? SecurityCriteria.from_securities(rows.Select(r => r.security)) : _security_criteria
|
||||
});
|
||||
|
||||
string get_is_paused_cmd_title() =>
|
||||
|
||||
@@ -79,7 +79,7 @@ namespace mbts.spark {
|
||||
), SparkCommands.ShowOrders.complete(() => {
|
||||
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
|
||||
ob_settings.filters = new bts.shell.orderbook.FilterStates {
|
||||
security_criteria = new SecurityCriteria(_vm.value.row_vms.Select(r => r.security))
|
||||
security_criteria = SecurityCriteria.from_securities(_vm.value.row_vms.Select(r => r.security))
|
||||
};
|
||||
return ob_settings;
|
||||
}), SparkCommands.window_settings_command(new DialogCommand(() => {
|
||||
|
||||
@@ -169,7 +169,7 @@ namespace mbts.spark {
|
||||
void _show_order_book() {
|
||||
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
|
||||
ob_settings.filters = new bts.shell.orderbook.FilterStates {
|
||||
security_criteria = new SecurityCriteria(security)
|
||||
security_criteria = SecurityCriteria.from_securities(security)
|
||||
};
|
||||
_bus.post(SparkTags.ShowOrderBookTag, ob_settings);
|
||||
}
|
||||
|
||||
@@ -302,7 +302,7 @@ namespace mbts.spark {
|
||||
|
||||
var state = new bts.shell.orderbook.OrderBookStartupData(
|
||||
new bts.shell.orderbook.FilterStates {
|
||||
security_criteria = new SecurityCriteria(_owner.security),
|
||||
security_criteria = SecurityCriteria.from_securities(_owner.security),
|
||||
price = price_filter,
|
||||
statuses = new[] { OrderStatus.Working },
|
||||
order_types = AlgoID.exchange_algo_keys
|
||||
|
||||
@@ -583,7 +583,7 @@ namespace mbts.spark {
|
||||
void _show_book() {
|
||||
var ob_settings = bts.shell.orderbook.OrderBookStartupData.Default;
|
||||
ob_settings.filters = new bts.shell.orderbook.FilterStates {
|
||||
security_criteria = new SecurityCriteria(security)
|
||||
security_criteria = SecurityCriteria.from_securities(security)
|
||||
};
|
||||
_bus.post(SparkTags.ShowOrderBookTag, ob_settings);
|
||||
}
|
||||
|
||||
@@ -633,7 +633,7 @@ namespace bts.shell.pnl {
|
||||
case ColumnKeys.Exchange: filter.exchange = new[] { (Venue)group.Name }; break;
|
||||
case ColumnKeys.Security:
|
||||
if (group.Name is SecurityImmutable sec)
|
||||
filter.security = new SecurityCriteria(sec);
|
||||
filter.security = SecurityCriteria.from_securities(sec);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
@@ -696,7 +696,7 @@ namespace bts.shell.pnl {
|
||||
case ColumnKeys.Exchange: filter.exchanges = new[] { (Venue)g.Name }; break;
|
||||
case ColumnKeys.Security:
|
||||
if (g.Name is SecurityImmutable sec)
|
||||
filter.security_criteria = new SecurityCriteria(sec);
|
||||
filter.security_criteria = SecurityCriteria.from_securities(sec);
|
||||
break;
|
||||
default: throw new NotImplementedException();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
using bts.admin.risk;
|
||||
using bts.api;
|
||||
using bts.spark;
|
||||
using mbts;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
class RiskManagementWindow : SparkWindow<RiskManagementSettings>, ISharedWindow {
|
||||
const string
|
||||
_window_type_id = "RiskManagement",
|
||||
_window_type_display_name = "Risk Management";
|
||||
|
||||
readonly IWindowSettingsManager<GridSettings> _settings_manager;
|
||||
readonly AdminModel _model;
|
||||
FrameworkElement _view;
|
||||
IReadOnlyList<SparkCommand> _commands;
|
||||
IDisposable _settings_sub, _securities_sub;
|
||||
RiskManagementSettings _settings;
|
||||
RiskManagementVM _vm;
|
||||
SimpleGridSettingsManagerVM<RiskManagementControl.Columns> _grid_settings_manager => _vm.grid_settings_manager;
|
||||
SparkMenuDisplayVM _menu_display_vm;
|
||||
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
|
||||
|
||||
public RiskManagementWindow(
|
||||
IBus bus,
|
||||
AdminModel model,
|
||||
IWindowSettingsManager<GridSettings> settings_manager,
|
||||
ISaveWindow<RiskManagementSettings> isw,
|
||||
RiskManagementSettings settings)
|
||||
: base(_window_type_display_name, bus, isw, Icons.Risk) {
|
||||
//
|
||||
_settings_manager = settings_manager;
|
||||
_settings = settings;
|
||||
_model = model;
|
||||
thread_id = mbts.config.Configuration.get<bts.config.MultiThreadedWindows>().risk_management ? "OrderBook" : null;
|
||||
_securities_sub = bus.subscribe(SparkTags.SecuritiesTag).Subscribe(sec_map => _security_map = sec_map);
|
||||
}
|
||||
|
||||
public override IEnumerable<SparkCommand> Commands => _commands;
|
||||
public override string window_type => _window_type_id;
|
||||
|
||||
public override RiskManagementSettings SaveState =>
|
||||
(_vm?.create() ?? RiskManagementSettings.Default).with(menu_display: _menu_display_vm.safe_create());
|
||||
|
||||
public override UIElement Content => _view;
|
||||
|
||||
public override double MinWidth =>
|
||||
RiskManagementSettings.DefaultGrid.columns.Where(c => c.is_visible).Sum(gcs => gcs.width)
|
||||
+ SPARK_MENU_COLLAPSED_WIDTH
|
||||
+ LAUNCHER_BUTTON_BORDER_WIDTH
|
||||
+ EXTRA_FOR_NO_HORIZONTAL_SCROLLBAR_WIDTH;
|
||||
|
||||
public string thread_id { get; }
|
||||
|
||||
public override void Dispose() {
|
||||
_commands.empty_if_null().OfType<IDisposable>().DisposeAll();
|
||||
Disposable.dispose(ref _securities_sub);
|
||||
Disposable.dispose(ref _settings_sub);
|
||||
Disposable.dispose(ref _vm);
|
||||
}
|
||||
|
||||
public void thread_init() {
|
||||
// assert nothing has been initialized yet
|
||||
Log.assert(_menu_display_vm == null, "MenuDisplay viewmodel has already been initialized.");
|
||||
Log.assert(_vm == null, "Viewmodel has already been initialized.");
|
||||
Log.assert(_settings != null, "Settings is null.");
|
||||
Log.assert(_settings_sub == null, "Grid settings subscription has already been initialized.");
|
||||
// initialize everything: menu_display_vm, vm, settings subscription, and view
|
||||
_menu_display_vm = _init_menu_display_vm(_settings.menu_display);
|
||||
_vm = _init_vm(_model, _settings);
|
||||
_settings_sub = _settings_manager.subscribe_current()
|
||||
.OnDispatcher()
|
||||
.Subscribe(gs => _vm.load_from(gs));
|
||||
_commands = new List<SparkCommand>(_mk_commands());
|
||||
_view = new RiskManagementControl { DataContext = _vm };
|
||||
}
|
||||
|
||||
static SparkMenuDisplayVM _init_menu_display_vm(SparkMenuDisplaySettings settings) {
|
||||
var vm = new SparkMenuDisplayVM();
|
||||
vm.load_from(settings);
|
||||
return vm;
|
||||
}
|
||||
|
||||
static RiskManagementVM _init_vm(AdminModel model, RiskManagementSettings settings) {
|
||||
var vm = new RiskManagementVM(model.account_security_risk_limits, model.on_account_tuples);
|
||||
vm.request_update_limit += update_limit;
|
||||
vm.load_from(settings);
|
||||
return vm;
|
||||
void update_limit(AccountSecurityRiskLimits lim) =>
|
||||
model.add_or_update_risk_limits(lim.account_key, new[] { (lim.security_key, lim.limits) }).ignore();
|
||||
}
|
||||
|
||||
IEnumerable<SparkCommand> _mk_commands() {
|
||||
yield return new SparkCommandCommand("Add New Limits", Icons.AddBoxes, new DialogCommand(() => {
|
||||
if (_vm is null)
|
||||
return null;
|
||||
var account_id = _vm.selected_account?.id ?? 0;
|
||||
var init_state = new RiskLimitsModification(
|
||||
is_position_limit_enabled: true,
|
||||
is_clip_limit_enabled: true,
|
||||
is_price_limit_enabled: true);
|
||||
return mk_row_editor_dialog(init_state, commit, true);
|
||||
void commit(RiskLimitsModification result) {
|
||||
impl();
|
||||
async void impl() {
|
||||
var target_secs = await result.security_criteria.securities(_bus, TimeSpan.FromSeconds(5));
|
||||
if (target_secs.Count > 0) {
|
||||
var new_limits = result.apply(new RiskLimits());
|
||||
await _model.add_or_update_risk_limits(account_id, target_secs.Keys.Select(k => (k, new_limits)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
var edit_selected_rows_cmd = new DialogCommand(() => {
|
||||
if (!(_vm?.selected_account).HasValue)
|
||||
return null;
|
||||
var account_id = _vm.selected_account.Value.id;
|
||||
var targets = _vm.selected_rows.Select(r => r.create()).ToList();
|
||||
var init_state = RiskLimitsModification.aggregate_common_props(targets);
|
||||
return mk_row_editor_dialog(init_state, commit, false);
|
||||
void commit(RiskLimitsModification mod) =>
|
||||
_model.add_or_update_risk_limits(account_id, targets.Select(r => (r.security_key, mod.apply(r.limits)))).ignore();
|
||||
}, () => _vm.selected_rows.Count > 0, true, off_x: -300, off_y: -60);
|
||||
_vm.selected_rows_changed += edit_selected_rows_cmd.RaiseCanExecuteChanged;
|
||||
yield return new SparkCommandCommand("Edit Selected Rows", Icons.ModifyOrder, edit_selected_rows_cmd);
|
||||
|
||||
var delete_selected_rows_cmd = new DelegateCommand(() => {
|
||||
if (!(_vm?.selected_account).HasValue)
|
||||
return;
|
||||
var selected_rows = _vm.selected_rows;
|
||||
var account_id = _vm.selected_account.Value.id;
|
||||
if (BTSMessageBox.Confirm($"Are you sure you want to delete the selected limits ({selected_rows.Count})?", "Delete Limits"))
|
||||
_model.delete_risk_limits(account_id, selected_rows.Select(r => r.security_key)).ignore();
|
||||
}, () => _vm.selected_rows.Count > 0);
|
||||
_vm.selected_rows_changed += delete_selected_rows_cmd.RaiseCanExecuteChanged;
|
||||
|
||||
yield return new SparkCommandCommand("Delete", Icons.Trash, delete_selected_rows_cmd);
|
||||
// wrap the grid settings view factory in a dialogCommand here since the vm is destroyed/recreated when the security criteria changes
|
||||
yield return SparkCommands.window_settings_command(new DialogCommand(() => {
|
||||
if (_vm is null)
|
||||
return null;
|
||||
_settings_manager.instance_settings = _vm.create().grid;
|
||||
var settings_vm = new ShellSettingsDialogVM<GridSettings, SimpleGridSettingsVM>(
|
||||
_settings_manager,
|
||||
us => us.risk_management_grid,
|
||||
UserSettings.FileExtensions.GRID,
|
||||
_grid_settings_manager.create_vm
|
||||
);
|
||||
settings_vm.SubscribeDisposed(literal_title_source.OnDispatcher().Subscribe(t => settings_vm.title = t));
|
||||
var dialog = new ShellSettingsDialog { DataContext = settings_vm };
|
||||
settings_vm.RequestClose += (s, e) => WindowUtilities.force_close(dialog);
|
||||
return dialog;
|
||||
}));
|
||||
yield return _menu_display_vm.spark_cmd;
|
||||
|
||||
// although only the api only requires security keys,
|
||||
// the actual securities are used in this method so the user can see them in the view
|
||||
Window mk_row_editor_dialog(RiskLimitsModification init_state, Action<RiskLimitsModification> commit, bool is_add) {
|
||||
var limits_vm = new RiskLimitsModificationVM(is_add) {
|
||||
security_map = _security_map
|
||||
};
|
||||
limits_vm.load_from(init_state);
|
||||
limits_vm.request_commit += () => commit(limits_vm.create());
|
||||
var (title, icon) = is_add ? ("New Limits", Icons.AddBoxes) : ("Edit Limits", Icons.ModifyOrder);
|
||||
var dialog = new RiskLimitParametersDialog {
|
||||
DataContext = limits_vm,
|
||||
Icon = icon,
|
||||
Title = title
|
||||
};
|
||||
dialog.SetBinding(ContentControl.ContentProperty, new Binding());
|
||||
WindowUtilities.listen_for_close_requests(dialog);
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using bts.api;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using System;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
class RiskManagementWindowFactory : ShellWindowFactory<RiskManagementSettings> {
|
||||
readonly IBus _bus;
|
||||
readonly AdminModel _model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
|
||||
public RiskManagementWindowFactory(IShellModule owner, IBus bus, UserSettingsHub settings_hub, AdminModel model) : base(owner, "RiskManagement") {
|
||||
_bus = bus;
|
||||
_model = model;
|
||||
_settings_hub = settings_hub;
|
||||
_dispose = bus.subscribe(SparkTags.ShowRiskManagement)
|
||||
.OnDispatcher()
|
||||
.Subscribe(show);
|
||||
|
||||
async void show(RiskManagementSettings settings) {
|
||||
var win = await load(settings);
|
||||
bus.post(SparkTags.ShowWindow, win);
|
||||
}
|
||||
}
|
||||
|
||||
public override Task<IShellWindow> load(RiskManagementSettings state) =>
|
||||
state is null ?
|
||||
throw new ArgumentNullException(nameof(state)) :
|
||||
Task.FromResult<IShellWindow>(new RiskManagementWindow(_bus, _model, _settings_hub.create_risk_management_settings_manager(state.grid), this, state));
|
||||
}
|
||||
}
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
using bts.admin.risk;
|
||||
using bts.utils;
|
||||
using mbts.spark.filters;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
[ProtoContract]
|
||||
class RiskManagementFilterSettings : IEquatable<RiskManagementFilterSettings> {
|
||||
static readonly IEqualityComparer<RiskManagementFilterSettings> _comparer =
|
||||
ObjectSnapshot.property_equality_comparer<RiskManagementFilterSettings>();
|
||||
|
||||
public RiskManagementFilterSettings() { }
|
||||
|
||||
public RiskManagementFilterSettings(
|
||||
string security_regex,
|
||||
string security_key_regex,
|
||||
UIntMinMaxState max_long_position,
|
||||
UIntMinMaxState max_short_position,
|
||||
bool? is_position_limit_enabled,
|
||||
UIntMinMaxState clip_limit,
|
||||
bool? is_clip_limit_enabled,
|
||||
DoubleMinMaxState min_price,
|
||||
DoubleMinMaxState max_price,
|
||||
bool? is_price_limit_enabled) {
|
||||
// security
|
||||
this.security_regex = security_regex;
|
||||
this.security_key_regex = security_key_regex;
|
||||
// position
|
||||
this.max_long_position = max_long_position;
|
||||
this.max_short_position = max_short_position;
|
||||
this.is_position_limit_enabled = is_position_limit_enabled;
|
||||
// clip
|
||||
this.clip_limit = clip_limit;
|
||||
this.is_clip_limit_enabled = is_clip_limit_enabled;
|
||||
// price
|
||||
this.min_price = min_price;
|
||||
this.max_price = max_price;
|
||||
this.is_price_limit_enabled = is_price_limit_enabled;
|
||||
}
|
||||
|
||||
// security
|
||||
[ProtoMember(1)]
|
||||
public string security_regex { get; }
|
||||
[ProtoMember(2)]
|
||||
public string security_key_regex { get; }
|
||||
// position
|
||||
[ProtoMember(3)]
|
||||
public UIntMinMaxState max_long_position { get; }
|
||||
[ProtoMember(4)]
|
||||
public UIntMinMaxState max_short_position { get; }
|
||||
[ProtoMember(5)]
|
||||
public bool? is_position_limit_enabled { get; }
|
||||
// clip
|
||||
[ProtoMember(6)]
|
||||
public UIntMinMaxState clip_limit { get; }
|
||||
[ProtoMember(7)]
|
||||
public bool? is_clip_limit_enabled { get; }
|
||||
// price
|
||||
[ProtoMember(8)]
|
||||
public DoubleMinMaxState min_price { get; }
|
||||
[ProtoMember(9)]
|
||||
public DoubleMinMaxState max_price { get; }
|
||||
[ProtoMember(10)]
|
||||
public bool? is_price_limit_enabled { get; }
|
||||
|
||||
// 'is limit enabled' items (for filter dropdown)
|
||||
public bool[] is_position_limit_enabled_items => yes_no_filter_items(is_position_limit_enabled);
|
||||
public bool[] is_clip_limit_enabled_items => yes_no_filter_items(is_clip_limit_enabled);
|
||||
public bool[] is_price_limit_enabled_items => yes_no_filter_items(is_price_limit_enabled);
|
||||
|
||||
public static bool[] yes_no_filter_items(bool? state) =>
|
||||
state.HasValue ? new[] { state.Value } : Array.Empty<bool>();
|
||||
|
||||
public static bool? yes_no_filter_state(IEnumerable<bool> selected_items) {
|
||||
var set = new HashSet<bool>(selected_items);
|
||||
return set.Count == 1 ? set.First() : (bool?)null;
|
||||
}
|
||||
|
||||
public Func<AccountSecurityRiskLimits, bool> build_predicate() {
|
||||
var pb = new ColumnPredicateBuilder<AccountSecurityRiskLimits>();
|
||||
// security
|
||||
pb.add_regex(security_regex, l => l.security.ToString());
|
||||
pb.add_regex(security_key_regex, l => l.security_key.ToString());
|
||||
// position
|
||||
pb.add_numeric(max_long_position, l => l.limits.max_long_position);
|
||||
pb.add_numeric(max_short_position, l => l.limits.max_short_position);
|
||||
pb.add_discrete_values(is_position_limit_enabled_items, l => l.limits.is_position_limit_enabled);
|
||||
// clip
|
||||
pb.add_numeric(clip_limit, l => l.limits.clip_limit);
|
||||
pb.add_discrete_values(is_clip_limit_enabled_items, l => l.limits.is_clip_limit_enabled);
|
||||
// price
|
||||
pb.add_numeric(min_price, l => l.limits.min_price);
|
||||
pb.add_numeric(max_price, l => l.limits.max_price);
|
||||
pb.add_discrete_values(is_price_limit_enabled_items, l => l.limits.is_price_limit_enabled);
|
||||
return pb.to_predicate();
|
||||
}
|
||||
|
||||
public RiskManagementFilterSettings with(
|
||||
string security_regex = null,
|
||||
string security_key_regex = null,
|
||||
UIntMinMaxState max_long_position = null,
|
||||
UIntMinMaxState max_short_position = null,
|
||||
OptionalNullable<bool>? is_position_limit_enabled = null,
|
||||
UIntMinMaxState clip_limit = null,
|
||||
OptionalNullable<bool>? is_clip_limit_enabled = null,
|
||||
DoubleMinMaxState min_price = null,
|
||||
DoubleMinMaxState max_price = null,
|
||||
OptionalNullable<bool>? is_price_limit_enabled = null) =>
|
||||
//
|
||||
new RiskManagementFilterSettings(
|
||||
// security
|
||||
security_regex ?? this.security_regex,
|
||||
security_key_regex ?? this.security_key_regex,
|
||||
// position
|
||||
max_long_position ?? this.max_long_position,
|
||||
max_short_position ?? this.max_short_position,
|
||||
is_position_limit_enabled.HasValue ? is_position_limit_enabled.Value.value : this.is_position_limit_enabled,
|
||||
// clip
|
||||
clip_limit ?? this.clip_limit,
|
||||
is_clip_limit_enabled.HasValue ? is_clip_limit_enabled.Value.value : this.is_clip_limit_enabled,
|
||||
// price
|
||||
min_price ?? this.min_price,
|
||||
max_price ?? this.max_price,
|
||||
is_price_limit_enabled.HasValue ? is_price_limit_enabled.Value.value : this.is_price_limit_enabled);
|
||||
|
||||
public bool Equals(RiskManagementFilterSettings other) =>
|
||||
_comparer.Equals(this, other);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
obj is RiskManagementFilterSettings other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
_comparer.GetHashCode(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using Columns = bts.shell.risk_management.RiskManagementControl.Columns;
|
||||
using ColumnWidths = mbts.ui.controls.DIYGrid.DefaultColumnWidths;
|
||||
using ColumnHeaders = mbts.ui.controls.DIYGrid.DefaultColumnHeaders;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
[ProtoContract]
|
||||
class RiskManagementSettings {
|
||||
[ProtoMember(3)]
|
||||
SortingCriteria[] _sorting_criteria;
|
||||
|
||||
public RiskManagementSettings() { }
|
||||
public RiskManagementSettings(
|
||||
GridSettings grid = null,
|
||||
RiskManagementFilterSettings filters = default,
|
||||
IEnumerable<SortingCriteria> sorting_criteria = null,
|
||||
uint account_id = 0,
|
||||
SparkMenuDisplaySettings? menu_display = null) {
|
||||
//
|
||||
this.grid = grid;
|
||||
this.filters = filters ?? new RiskManagementFilterSettings();
|
||||
_sorting_criteria = sorting_criteria?.ToArray();
|
||||
this.account_id = account_id;
|
||||
this.menu_display = menu_display ?? SparkMenuDisplaySettings.Default;
|
||||
}
|
||||
|
||||
public static RiskManagementSettings Default => new RiskManagementSettings(grid: DefaultGrid);
|
||||
public static GridSettings DefaultGrid => new GridSettings(columns: DefaultColumns.all);
|
||||
|
||||
[ProtoMember(1)]
|
||||
public GridSettings grid { get; }
|
||||
[ProtoMember(2)]
|
||||
public RiskManagementFilterSettings filters { get; }
|
||||
[ProtoMember(4)]
|
||||
public uint account_id { get; }
|
||||
[ProtoMember(5)]
|
||||
public SparkMenuDisplaySettings menu_display { get; }
|
||||
|
||||
public IReadOnlyList<SortingCriteria> sorting_criteria =>
|
||||
_sorting_criteria ?? Array.Empty<SortingCriteria>();
|
||||
|
||||
public RiskManagementSettings with(
|
||||
GridSettings grid = null,
|
||||
RiskManagementFilterSettings filters = default,
|
||||
IEnumerable<SortingCriteria> sorting_criteria = null,
|
||||
uint? account_id = null,
|
||||
SparkMenuDisplaySettings? menu_display = null) =>
|
||||
//
|
||||
new RiskManagementSettings(
|
||||
grid ?? this.grid,
|
||||
filters ?? this.filters,
|
||||
sorting_criteria ?? this.sorting_criteria,
|
||||
account_id ?? this.account_id,
|
||||
menu_display ?? this.menu_display);
|
||||
|
||||
public static class DefaultColumns {
|
||||
public static GridColumnSettings security => new GridColumnSettings(Columns.Security.ToString(), ColumnHeaders.Security, width: ColumnWidths.SecurityKey);
|
||||
public static GridColumnSettings security_key => new GridColumnSettings(Columns.SecurityKey.ToString(), ColumnHeaders.SecurityKey, width: ColumnWidths.SecurityKey, is_visible: false);
|
||||
// position
|
||||
public static GridColumnSettings max_long => new GridColumnSettings(Columns.MaxNetLongPosition.ToString(), "Max Long", width: 60, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings max_short => new GridColumnSettings(Columns.MaxNetShortPosition.ToString(), "Max Short", width: 60, horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings is_position_limit_enabled => new GridColumnSettings(Columns.IsPositionLimitEnabled.ToString(), "Enforce Pos", width: 70, horizontal_alignment: HorizontalAlignment.Center);
|
||||
// clip
|
||||
public static GridColumnSettings clip_limit => new GridColumnSettings(Columns.ClipLimit.ToString(), "Clip Limit", horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings is_clip_limit_enabled => new GridColumnSettings(Columns.IsClipLimitEnabled.ToString(), "Enforce Clip", width: 70, horizontal_alignment: HorizontalAlignment.Center);
|
||||
// price
|
||||
public static GridColumnSettings min_price => new GridColumnSettings(Columns.MinPrice.ToString(), "Min Price", horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings max_price => new GridColumnSettings(Columns.MaxPrice.ToString(), "Max Price", horizontal_alignment: HorizontalAlignment.Right);
|
||||
public static GridColumnSettings is_price_limit_enabled => new GridColumnSettings(Columns.IsPriceLimitEnabled.ToString(), "Enforce Price", width: 75, horizontal_alignment: HorizontalAlignment.Center);
|
||||
|
||||
public static IEnumerable<GridColumnSettings> all {
|
||||
get {
|
||||
yield return security;
|
||||
yield return max_long;
|
||||
yield return max_short;
|
||||
yield return is_position_limit_enabled;
|
||||
yield return clip_limit;
|
||||
yield return is_clip_limit_enabled;
|
||||
yield return min_price;
|
||||
yield return max_price;
|
||||
yield return is_price_limit_enabled;
|
||||
yield return security_key;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using bts.admin.risk;
|
||||
using bts.api;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows.Input;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
class RiskLimitsModificationVM : ViewModelBase, IValueFactory<RiskLimitsModification>, IRequestViewClose {
|
||||
readonly DeferrableActionWrapper _result_changed;
|
||||
RiskLimitsModification _modifications;
|
||||
IReadOnlyDictionary<uint, SecurityMD> _security_map = DictionaryHelper.empty<uint, SecurityMD>();
|
||||
|
||||
public RiskLimitsModificationVM(bool can_edit_security) {
|
||||
_result_changed = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
this.can_edit_security = can_edit_security;
|
||||
commit_cmd = new DelegateCommand(commit);
|
||||
cancel_cmd = new DelegateCommand(close);
|
||||
//
|
||||
void commit() {
|
||||
request_commit?.Invoke();
|
||||
close();
|
||||
}
|
||||
void close() =>
|
||||
RequestClose?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public event Action result_changed, request_commit;
|
||||
public event EventHandler RequestClose;
|
||||
|
||||
public SecurityImmutable? security { get => _modifications.security; set => _set_limits(_modifications.with(security: value)); }
|
||||
public SecurityCriteria security_criteria {
|
||||
get => _modifications.security_criteria ?? (security.HasValue ? SecurityCriteria.from_securities(security.Value) : null);
|
||||
set => _set_limits(_modifications.with(security_criteria: value));
|
||||
}
|
||||
public IReadOnlyDictionary<uint, SecurityMD> security_map {
|
||||
get => _security_map;
|
||||
set {
|
||||
_security_map = value;
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
}
|
||||
public bool can_edit_security { get; }
|
||||
// position
|
||||
public uint? max_long_position { get => _modifications.max_long_position; set => _set_limits(_modifications.with(max_long_position: value)); }
|
||||
public uint? max_short_position { get => _modifications.max_short_position; set => _set_limits(_modifications.with(max_short_position: value)); }
|
||||
public bool? is_position_limit_enabled { get => _modifications.is_position_limit_enabled; set => _set_limits(_modifications.with(is_position_limit_enabled: value)); }
|
||||
// clip
|
||||
public bool? is_clip_limit_enabled { get => _modifications.is_clip_limit_enabled; set => _set_limits(_modifications.with(is_clip_limit_enabled: value)); }
|
||||
public uint? clip_limit { get => _modifications.clip_limit; set => _set_limits(_modifications.with(clip_limit: value)); }
|
||||
// price
|
||||
public double? max_price { get => _modifications.max_price; set => _set_limits(_modifications.with(max_price: value)); }
|
||||
public double? min_price { get => _modifications.min_price; set => _set_limits(_modifications.with(min_price: value)); }
|
||||
public bool? is_price_limit_enabled { get => _modifications.is_price_limit_enabled; set => _set_limits(_modifications.with(is_price_limit_enabled: value)); }
|
||||
|
||||
public ICommand commit_cmd { get; }
|
||||
public ICommand cancel_cmd { get; }
|
||||
|
||||
public RiskLimitsModification create() =>
|
||||
_modifications;
|
||||
|
||||
public void load_from(RiskLimitsModification value) {
|
||||
using (_result_changed.defer_execution())
|
||||
using (detect_property_changes())
|
||||
_modifications = value;
|
||||
}
|
||||
|
||||
void _set_limits(RiskLimitsModification new_modifications, [CallerMemberName] string prop = "") =>
|
||||
_set_limits(new_modifications, new[] { prop });
|
||||
|
||||
void _set_limits(RiskLimitsModification new_modifications, params string[] invalidate_props) {
|
||||
if (!_modifications.Equals(new_modifications)) {
|
||||
_modifications = new_modifications;
|
||||
RaisePropertyChanged(invalidate_props);
|
||||
_result_changed.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly struct RiskLimitsModification {
|
||||
public RiskLimitsModification(
|
||||
SecurityImmutable? security = null,
|
||||
SecurityCriteria security_criteria = null,
|
||||
uint? max_long_position = null,
|
||||
uint? max_short_position = null,
|
||||
bool? is_position_limit_enabled = null,
|
||||
uint? clip_limit = null,
|
||||
bool? is_clip_limit_enabled = null,
|
||||
double? min_price = null,
|
||||
double? max_price = null,
|
||||
bool? is_price_limit_enabled = null) {
|
||||
//
|
||||
this.security = security;
|
||||
this.security_criteria = security_criteria;
|
||||
this.max_long_position = max_long_position;
|
||||
this.max_short_position = max_short_position;
|
||||
this.is_position_limit_enabled = is_position_limit_enabled;
|
||||
this.clip_limit = clip_limit;
|
||||
this.is_clip_limit_enabled = is_clip_limit_enabled;
|
||||
this.min_price = min_price;
|
||||
this.max_price = max_price;
|
||||
this.is_price_limit_enabled = is_price_limit_enabled;
|
||||
}
|
||||
|
||||
public SecurityImmutable? security { get; }
|
||||
public SecurityCriteria security_criteria { get; }
|
||||
// position
|
||||
public uint? max_long_position { get; }
|
||||
public uint? max_short_position { get; }
|
||||
public bool? is_position_limit_enabled { get; }
|
||||
// clip
|
||||
public uint? clip_limit { get; }
|
||||
public bool? is_clip_limit_enabled { get; }
|
||||
// price
|
||||
public double? min_price { get; }
|
||||
public double? max_price { get; }
|
||||
public bool? is_price_limit_enabled { get; }
|
||||
|
||||
public static RiskLimitsModification aggregate_common_props(IEnumerable<AccountSecurityRiskLimits> limit_items) =>
|
||||
new RiskLimitsModification(
|
||||
_common_value(limit_items, l => l.security, s => s),
|
||||
null,
|
||||
_common_value(limit_items, l => l.limits.max_long_position),
|
||||
_common_value(limit_items, l => l.limits.max_short_position),
|
||||
_common_value(limit_items, l => l.limits.is_position_limit_enabled),
|
||||
_common_value(limit_items, l => l.limits.clip_limit),
|
||||
_common_value(limit_items, l => l.limits.is_clip_limit_enabled),
|
||||
_common_value(limit_items, l => l.limits.min_price),
|
||||
_common_value(limit_items, l => l.limits.max_price),
|
||||
_common_value(limit_items, l => l.limits.is_price_limit_enabled));
|
||||
|
||||
public RiskLimits apply(RiskLimits target) =>
|
||||
new RiskLimits(
|
||||
is_position_limit_enabled: is_position_limit_enabled ?? target.is_position_limit_enabled,
|
||||
max_long_position: max_long_position ?? target.max_long_position,
|
||||
max_short_position: max_short_position ?? target.max_short_position,
|
||||
is_clip_limit_enabled: is_clip_limit_enabled ?? target.is_clip_limit_enabled,
|
||||
clip_limit: clip_limit ?? target.clip_limit,
|
||||
is_price_limit_enabled: is_price_limit_enabled ?? target.is_price_limit_enabled,
|
||||
min_price: min_price ?? target.min_price,
|
||||
max_price: max_price ?? target.max_price);
|
||||
|
||||
public RiskLimitsModification with(
|
||||
OptionalNullable<SecurityImmutable>? security = null,
|
||||
SecurityCriteria security_criteria = null,
|
||||
OptionalNullable<uint>? max_long_position = null,
|
||||
OptionalNullable<uint>? max_short_position = null,
|
||||
OptionalNullable<bool>? is_position_limit_enabled = null,
|
||||
OptionalNullable<uint>? clip_limit = null,
|
||||
OptionalNullable<bool>? is_clip_limit_enabled = null,
|
||||
OptionalNullable<double>? min_price = null,
|
||||
OptionalNullable<double>? max_price = null,
|
||||
OptionalNullable<bool>? is_price_limit_enabled = null) =>
|
||||
//
|
||||
new RiskLimitsModification(
|
||||
security.HasValue ? security.Value.value : this.security,
|
||||
security_criteria ?? this.security_criteria,
|
||||
max_long_position.HasValue ? max_long_position.Value.value : this.max_long_position,
|
||||
max_short_position.HasValue ? max_short_position.Value.value : this.max_short_position,
|
||||
is_position_limit_enabled.HasValue ? is_position_limit_enabled.Value.value : this.is_position_limit_enabled,
|
||||
clip_limit.HasValue ? clip_limit.Value.value : this.clip_limit,
|
||||
is_clip_limit_enabled.HasValue ? is_clip_limit_enabled.Value.value : this.is_clip_limit_enabled,
|
||||
min_price.HasValue ? min_price.Value.value : this.min_price,
|
||||
max_price.HasValue ? max_price.Value.value : this.max_price,
|
||||
is_price_limit_enabled.HasValue ? is_price_limit_enabled.Value.value : this.is_price_limit_enabled);
|
||||
|
||||
static T? _common_value<T>(IEnumerable<AccountSecurityRiskLimits> source, Func<AccountSecurityRiskLimits, T> get) where T : struct =>
|
||||
_common_value(source, get, common => new T?(common));
|
||||
|
||||
static TResult _common_value<T, TResult>(IEnumerable<AccountSecurityRiskLimits> source, Func<AccountSecurityRiskLimits, T> get, Func<T, TResult> convert_common, TResult default_value = default) =>
|
||||
source.Select(get).Distinct().TryGetSingle(out var common) ? convert_common(common) : default_value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
using bts.admin.risk;
|
||||
using bts.api;
|
||||
using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
class RiskLimitsVM : ViewModelBase, IValueFactory<AccountSecurityRiskLimits> {
|
||||
readonly DeferrableActionWrapper _result_changed;
|
||||
// The user can modify some props directly in the grid.
|
||||
// However, the sorting relies on immutable data
|
||||
// (binary search may break if the applicable comparisons change,
|
||||
// depending on the current sorting criteria).
|
||||
// As a work-around, the rows are sorted by the committed_data.
|
||||
// Any modifications made through the prop setters are assumed to be 'user-changes'
|
||||
// (as the props are bound to the view),
|
||||
/// and the committed data is only changed through <see cref="load_from(AccountSecurityRiskLimits)"/>.
|
||||
AccountSecurityRiskLimits _committed_data, _mutable_data;
|
||||
|
||||
public RiskLimitsVM() =>
|
||||
_result_changed = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
|
||||
public RiskLimitsVM(AccountSecurityRiskLimits data) : this() {
|
||||
_committed_data = data;
|
||||
_mutable_data = data;
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
|
||||
public SecurityImmutable? security => _committed_data.security;
|
||||
public uint security_key => _committed_data.security_key;
|
||||
public string account_name => _committed_data.account_name;
|
||||
public uint account_key => _committed_data.account_key;
|
||||
// position limit
|
||||
public bool is_position_limit_enabled { get => _mutable_limits.is_position_limit_enabled; set => _set_limits(_mutable_limits.with(is_position_limit_enabled: value)); }
|
||||
public uint max_long_position { get => _mutable_limits.max_long_position; set => _set_limits(_mutable_limits.with(max_long_position: value)); }
|
||||
public uint max_short_position { get => _mutable_limits.max_short_position; set => _set_limits(_mutable_limits.with(max_short_position: value)); }
|
||||
// clip limit
|
||||
public bool is_clip_limit_enabled { get => _mutable_limits.is_clip_limit_enabled; set => _set_limits(_mutable_limits.with(is_clip_limit_enabled: value)); }
|
||||
public uint clip_limit { get => _mutable_limits.clip_limit; set => _set_limits(_mutable_limits.with(clip_limit: value)); }
|
||||
// price limit
|
||||
public bool is_price_limit_enabled { get => _mutable_limits.is_price_limit_enabled; set => _set_limits(_mutable_limits.with(is_price_limit_enabled: value)); }
|
||||
public double max_price { get => _mutable_limits.max_price; set => _set_limits(_mutable_limits.with(max_price: value)); }
|
||||
public double min_price { get => _mutable_limits.min_price; set => _set_limits(_mutable_limits.with(min_price: value)); }
|
||||
public AccountSecurityKey row_id => new AccountSecurityKey(account_key, security_key);
|
||||
public AccountSecurityRiskLimits committed_data => _committed_data;
|
||||
|
||||
RiskLimits _mutable_limits {
|
||||
get => _mutable_data.limits;
|
||||
set => _mutable_data = _mutable_data.with(limits: value);
|
||||
}
|
||||
|
||||
public AccountSecurityRiskLimits create() => _mutable_data;
|
||||
|
||||
public void load_from(AccountSecurityRiskLimits value) {
|
||||
using (_result_changed.defer_execution())
|
||||
using (detect_property_changes()) {
|
||||
_committed_data = value;
|
||||
_mutable_data = value;
|
||||
}
|
||||
}
|
||||
|
||||
public void apply_modifications(RiskLimitsModification m) =>
|
||||
load_from(_committed_data.with(
|
||||
_committed_data.limits.with(m.is_position_limit_enabled,
|
||||
m.max_long_position,
|
||||
m.max_short_position,
|
||||
m.is_clip_limit_enabled,
|
||||
m.clip_limit,
|
||||
m.is_price_limit_enabled,
|
||||
m.min_price,
|
||||
m.max_short_position),
|
||||
new AccountTuple(_committed_data.account_key, _committed_data.account_name),
|
||||
m.security));
|
||||
|
||||
void _set_limits(RiskLimits new_limits, [CallerMemberName] string prop = "") =>
|
||||
_set_limits(new_limits, new[] { prop });
|
||||
|
||||
void _set_limits(RiskLimits new_limits, params string[] invalidate_props) {
|
||||
if (!_mutable_limits.Equals(new_limits)) {
|
||||
_mutable_limits = new_limits;
|
||||
RaisePropertyChanged(invalidate_props);
|
||||
_result_changed.execute();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,313 @@
|
||||
using bts.admin.risk;
|
||||
using bts.api.core;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows.Controls;
|
||||
using Columns = bts.shell.risk_management.RiskManagementControl.Columns;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
class RiskManagementColumnModel : DIYGridColumnModel<Columns, AccountSecurityRiskLimits, RiskManagementFilterSettings> {
|
||||
public RiskManagementColumnModel(Columns id, IComparer<AccountSecurityRiskLimits> data_comparer, IDataGridColumnFilter<AccountSecurityRiskLimits> filter, string filter_template, Func<RiskManagementFilterSettings, object> get_column_filter_settings, Func<RiskManagementFilterSettings, object, RiskManagementFilterSettings> set_column_filter_settings)
|
||||
: base(id, data_comparer, filter, filter_template, get_column_filter_settings, set_column_filter_settings) {
|
||||
}
|
||||
}
|
||||
|
||||
class RiskManagementVM : DisposableViewModelBase, IHandleGridSorting, IHandleRowSelection, IValueFactory<RiskManagementSettings> {
|
||||
static readonly IComparer<AccountSecurityRiskLimits> _data_row_id_comparer =
|
||||
new ComparerBuilder<AccountSecurityKey>(new[] {
|
||||
Comparer<uint>.Default.wrap<uint, AccountSecurityKey>(ask => ask.account),
|
||||
Comparer<uint>.Default.wrap<uint, AccountSecurityKey>(ask => ask.security)
|
||||
}).to_comparer()
|
||||
.wrap<AccountSecurityKey, AccountSecurityRiskLimits>(r => r.key);
|
||||
|
||||
static IComparer<RiskLimitsVM> _row_id_comparer =>
|
||||
_data_row_id_comparer.wrap<AccountSecurityRiskLimits, RiskLimitsVM>(r => r.committed_data);
|
||||
|
||||
readonly DeferrableActionWrapper _result_changed, _refresh_filter_changed, _selected_rows_changed;
|
||||
readonly ColumnSortManager _sort_manager;
|
||||
readonly ObservableCollection<RiskManagementColumnModel> _columns = new ObservableCollection<RiskManagementColumnModel>();
|
||||
readonly Dictionary<Columns, RiskManagementColumnModel> _column_map = new Dictionary<Columns, RiskManagementColumnModel>();
|
||||
readonly SelectorVM<AccountTuple?, uint> _accounts_vm = new SelectorVM<AccountTuple?, uint>(acc => acc?.id ?? 0) { comparer = Comparer<AccountTuple>.Default.to_nullable() };
|
||||
IReadOnlyDictionary<AccountSecurityKey, AccountSecurityRiskLimits> _cache = DictionaryHelper.empty<AccountSecurityKey, AccountSecurityRiskLimits>();
|
||||
ImmutableSortedSet<RiskLimitsVM> _selected_rows = ImmutableSortedSet.Create(_row_id_comparer);
|
||||
IList<AccountSecurityRiskLimits> _data_rows;
|
||||
IComparer<AccountSecurityRiskLimits> _data_comparer = _data_row_id_comparer;
|
||||
IComparer<RiskLimitsVM> _row_comparer;
|
||||
|
||||
public RiskManagementVM(IObservable<TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey>> data_source, IObservable<IReadOnlyList<AccountTuple>> accounts_source) {
|
||||
_result_changed = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
_selected_rows_changed = new DeferrableActionWrapper(() => selected_rows_changed?.Invoke());
|
||||
_sort_manager = new ColumnSortManager(_columns.OfType<ISortableDIYGridColumn>(), null);
|
||||
_data_rows = rows.WrapSelector(r => r.committed_data, lim => new RiskLimitsVM(lim));
|
||||
_accounts_vm.selected_item_changed += () => {
|
||||
RaisePropertyChanged(nameof(selected_account));
|
||||
_result_changed.execute();
|
||||
_refresh_filter_changed.execute();
|
||||
};
|
||||
column_map = FunctionalIndex.create<string, RiskManagementColumnModel>(_find_column);
|
||||
grid_settings_manager = new SimpleGridSettingsManagerVM<Columns>(
|
||||
_columns, null, ordered_cols => {
|
||||
CollectionHelper.sync_with_never_duplicate(_columns, ordered_cols, cs => {
|
||||
throw new InvalidOperationException();
|
||||
}, c => c.id.ToString(), cs => cs.id);
|
||||
}, null);
|
||||
_refresh_filter_changed = new DeferrableActionWrapper(() => _filter_changed?.Invoke(_mk_filter()));
|
||||
/////////////////
|
||||
// Columns
|
||||
/////////////////
|
||||
// security
|
||||
mk_column_model(
|
||||
Columns.Security,
|
||||
wrap_comp(r => r.security),
|
||||
new RegexFilterVM<AccountSecurityRiskLimits>(r => r.security.ToString(), null),
|
||||
DataGridFilterKnownTemplates.Regex,
|
||||
fs => fs.security_regex,
|
||||
(fs, v) => fs.with(security_regex: v));
|
||||
// security key
|
||||
mk_column_model(
|
||||
Columns.SecurityKey,
|
||||
wrap_comp(r => r.security_key),
|
||||
new RegexFilterVM<AccountSecurityRiskLimits>(r => r.security_key.ToString(), null),
|
||||
DataGridFilterKnownTemplates.Regex,
|
||||
fs => fs.security_key_regex,
|
||||
(fs, v) => fs.with(security_key_regex: v));
|
||||
// max long
|
||||
mk_column_model(
|
||||
Columns.MaxNetLongPosition,
|
||||
wrap_comp(r => r.limits.max_long_position),
|
||||
new UIntMinMax<AccountSecurityRiskLimits>(
|
||||
r => r.limits.max_long_position,
|
||||
null),
|
||||
DataGridFilterKnownTemplates.UIntMinMax,
|
||||
fs => fs.max_long_position,
|
||||
(fs, v) => fs.with(max_long_position: v));
|
||||
// max short
|
||||
mk_column_model(
|
||||
Columns.MaxNetShortPosition,
|
||||
wrap_comp(r => r.limits.max_short_position),
|
||||
new UIntMinMax<AccountSecurityRiskLimits>(
|
||||
r => r.limits.max_short_position,
|
||||
null),
|
||||
DataGridFilterKnownTemplates.UIntMinMax,
|
||||
fs => fs.max_short_position,
|
||||
(fs, v) => fs.with(max_short_position: v));
|
||||
// is position limit enabled
|
||||
mk_is_enabled_column(
|
||||
Columns.IsPositionLimitEnabled,
|
||||
r => r.limits.is_position_limit_enabled,
|
||||
(fs, yes_no_items) => fs.with(is_position_limit_enabled: RiskManagementFilterSettings.yes_no_filter_state(yes_no_items)),
|
||||
nameof(RiskLimitsVM.is_position_limit_enabled));
|
||||
// clip size
|
||||
mk_column_model(
|
||||
Columns.ClipLimit,
|
||||
wrap_comp(r => r.limits.clip_limit),
|
||||
new UIntMinMax<AccountSecurityRiskLimits>(r => r.limits.clip_limit, null),
|
||||
DataGridFilterKnownTemplates.UIntMinMax,
|
||||
fs => fs.clip_limit,
|
||||
(fs, v) => fs.with(clip_limit: v));
|
||||
// is clip limit enabled
|
||||
mk_is_enabled_column(
|
||||
Columns.IsClipLimitEnabled,
|
||||
r => r.limits.is_clip_limit_enabled,
|
||||
(fs, yes_no_items) => fs.with(is_clip_limit_enabled: RiskManagementFilterSettings.yes_no_filter_state(yes_no_items)),
|
||||
nameof(RiskLimitsVM.is_clip_limit_enabled));
|
||||
// min price
|
||||
mk_column_model(
|
||||
Columns.MinPrice,
|
||||
wrap_comp(r => r.limits.min_price),
|
||||
new DoubleMinMax<AccountSecurityRiskLimits>(r => r.limits.min_price, nameof(RiskLimitsVM.min_price)),
|
||||
DataGridFilterKnownTemplates.DoubleMinMax,
|
||||
fs => fs.min_price,
|
||||
(fs, v) => fs.with(min_price: v));
|
||||
// max price
|
||||
mk_column_model(
|
||||
Columns.MaxPrice,
|
||||
wrap_comp(r => r.limits.max_price),
|
||||
new DoubleMinMax<AccountSecurityRiskLimits>(r => r.limits.max_price, nameof(RiskLimitsVM.max_price)),
|
||||
DataGridFilterKnownTemplates.DoubleMinMax,
|
||||
fs => fs.max_price,
|
||||
(fs, v) => fs.with(max_price: v));
|
||||
// is price limit enabled
|
||||
mk_is_enabled_column(
|
||||
Columns.IsPriceLimitEnabled,
|
||||
r => r.limits.is_price_limit_enabled,
|
||||
(fs, yes_no_items) => fs.with(is_price_limit_enabled: RiskManagementFilterSettings.yes_no_filter_state(yes_no_items)),
|
||||
nameof(RiskLimitsVM.is_price_limit_enabled));
|
||||
|
||||
data_comparer = new ComparerBuilder<AccountSecurityRiskLimits>(new[] {
|
||||
wrap_comp(r => r.security),
|
||||
_data_row_id_comparer
|
||||
}).to_comparer();
|
||||
_column_map = _columns.ToDictionary(c => c.id);
|
||||
columns = ObservableAutoWrapper.SelectObservable(_columns, c => c);
|
||||
var filter_source =
|
||||
Observable.FromEvent<Func<AccountSecurityRiskLimits, bool>>(cb => _filter_changed += cb, cb => _filter_changed -= cb)
|
||||
.StartWith(_mk_filter);
|
||||
var filtered_data_source =
|
||||
data_source.Filter(filter_source, AccountSecurityRiskLimitsDataHelper.key_fn);
|
||||
|
||||
var filter_hooks = _columns.Select(c => {
|
||||
var f = c.filter;
|
||||
f.filter_changed += _refresh_filter_changed.execute;
|
||||
return Disposable.Create(() => f.filter_changed -= _refresh_filter_changed.execute);
|
||||
}).ToList();
|
||||
_dispose = Disposable.Create(
|
||||
ObservableAutoWrapper.connect_item_hooks(rows, subscribe_row_changes),
|
||||
accounts_source.OnDispatcher().Subscribe(on_accounts),
|
||||
Disposable.wrap_collection(filter_hooks),
|
||||
filtered_data_source.OnDispatcher().Subscribe(on_data));
|
||||
//////////////////
|
||||
// local methods
|
||||
//////////////////
|
||||
IComparer<AccountSecurityRiskLimits> wrap_comp<T>(Func<AccountSecurityRiskLimits, T> get_value) =>
|
||||
Comparer<T>.Default.wrap<T, AccountSecurityRiskLimits>(get_value);
|
||||
|
||||
RiskManagementColumnModel mk_column_model<T>(
|
||||
Columns id,
|
||||
IComparer<AccountSecurityRiskLimits> row_comp,
|
||||
IDataGridColumnPersistentFilter<AccountSecurityRiskLimits, T> filter,
|
||||
string filter_template,
|
||||
Func<RiskManagementFilterSettings, T> get_filter_settings,
|
||||
Func<RiskManagementFilterSettings, T, RiskManagementFilterSettings> set_filter_settings) {
|
||||
//
|
||||
var col = new RiskManagementColumnModel(id, row_comp,
|
||||
filter,
|
||||
filter_template,
|
||||
fs => get_filter_settings(fs),
|
||||
(fs, val) => val is T cfs ? set_filter_settings(fs, cfs) : fs);
|
||||
_columns.Add(col);
|
||||
return col;
|
||||
}
|
||||
|
||||
// the 'is enabled' columns are special cases, bc their filters produces an array of bools
|
||||
// (cheap way to reuse the discrete values filter template for true/false items),
|
||||
// but the filter protobuf has a nullable<bool>, so an extra conversion must be done explicitly (hence the get_row_val param).
|
||||
RiskManagementColumnModel mk_is_enabled_column(Columns id, Func<AccountSecurityRiskLimits, bool> get_row_val, Func<RiskManagementFilterSettings, IEnumerable<bool>, RiskManagementFilterSettings> set_filter_prop, string prop) {
|
||||
var col = new RiskManagementColumnModel(
|
||||
id,
|
||||
wrap_comp(get_row_val),
|
||||
DiscreteValuesFilterVM.Create(new[] { true, false }, get_row_val, prop),
|
||||
DataGridFilterKnownTemplates.YesNo,
|
||||
fs => RiskManagementFilterSettings.yes_no_filter_items(fs.is_position_limit_enabled),
|
||||
(fs, fstate) => set_filter_prop(fs, fstate as IEnumerable<bool>));
|
||||
_columns.Add(col);
|
||||
return col;
|
||||
}
|
||||
|
||||
IDisposable subscribe_row_changes(RiskLimitsVM r) {
|
||||
r.result_changed += on_limits_changed;
|
||||
return Disposable.Create(() => r.result_changed -= on_limits_changed);
|
||||
void on_limits_changed() {
|
||||
var lim = r.create();
|
||||
if (_cache.TryGetValue(lim.key, out var curr_lim) && !lim.Equals(curr_lim))
|
||||
request_update_limit?.Invoke(lim);
|
||||
}
|
||||
}
|
||||
|
||||
void on_accounts(IReadOnlyList<AccountTuple> new_accounts) =>
|
||||
_accounts_vm.update_items(new_accounts.Cast<AccountTuple?>());
|
||||
|
||||
void on_data(TableUpdate<AccountSecurityRiskLimits, AccountSecurityKey> table) {
|
||||
_cache = table.state;
|
||||
var comp = data_comparer;
|
||||
_data_rows.RemoveSorted(table.removed.Concat(table.changes.Old()), comp);
|
||||
_data_rows.InsertSorted(table.added.Concat(table.changes.New()), comp);
|
||||
}
|
||||
}
|
||||
|
||||
public event Action result_changed, selected_rows_changed;
|
||||
public event Action<AccountSecurityRiskLimits> request_update_limit;
|
||||
event Action<Func<AccountSecurityRiskLimits, bool>> _filter_changed;
|
||||
|
||||
public IReadOnlyIndex<string, RiskManagementColumnModel> column_map { get; }
|
||||
// this collection is needed to control the column order
|
||||
public IReadOnlyObservableCollection<IDIYGridColumn> columns { get; }
|
||||
public ObservableCollection<RiskLimitsVM> rows { get; } = new ObservableCollection<RiskLimitsVM>();
|
||||
public IReadOnlyList<RiskLimitsVM> selected_rows => _selected_rows;
|
||||
public IReadOnlyObservableCollection<AccountTuple?> accounts => _accounts_vm.items;
|
||||
public AccountTuple? selected_account {
|
||||
get => _accounts_vm.selected_item ?? default;
|
||||
set => _accounts_vm.selected_item = value;
|
||||
}
|
||||
public SimpleGridSettingsManagerVM<Columns> grid_settings_manager { get; }
|
||||
public IComparer<AccountSecurityRiskLimits> data_comparer {
|
||||
get => _data_comparer;
|
||||
private set {
|
||||
if (value != null) {
|
||||
_data_comparer = value;
|
||||
_row_comparer = null;
|
||||
_data_rows.sync_with(_data_rows.Ordered(value).ToList());
|
||||
_selected_rows = _selected_rows.WithComparer(row_comparer);
|
||||
}
|
||||
}
|
||||
}
|
||||
public IComparer<RiskLimitsVM> row_comparer =>
|
||||
_row_comparer ?? (_row_comparer = data_comparer?.wrap<AccountSecurityRiskLimits, RiskLimitsVM>(r => r.committed_data));
|
||||
|
||||
public RiskManagementSettings create() {
|
||||
var grid = grid_settings_manager.create();
|
||||
var filters = _columns.Aggregate(new RiskManagementFilterSettings(), (fs, c) => c.assign_filter_settings(fs));
|
||||
return new RiskManagementSettings(grid, filters, _sort_manager.SortingCriteria, selected_account?.id ?? 0);
|
||||
}
|
||||
|
||||
public void load_from(RiskManagementSettings settings) {
|
||||
if (settings is null)
|
||||
throw new ArgumentNullException(nameof(settings));
|
||||
using (_result_changed.defer_execution()) {
|
||||
if (settings.grid != null)
|
||||
grid_settings_manager.load_from(settings.grid);
|
||||
foreach (var col in _columns)
|
||||
col.load_from(null, settings.filters);
|
||||
_accounts_vm.try_select(settings.account_id, true);
|
||||
}
|
||||
}
|
||||
|
||||
public void load_from(GridSettings grid) {
|
||||
if (grid is null)
|
||||
throw new ArgumentNullException(nameof(grid));
|
||||
using (_result_changed.defer_execution())
|
||||
grid_settings_manager.load_from(grid);
|
||||
}
|
||||
|
||||
void IHandleRowSelection.row_selection_changed(SelectionChangedEventArgs e) {
|
||||
var bd = _selected_rows.ToBuilder();
|
||||
RowSelectionHelper.apply_changes(bd, e);
|
||||
_selected_rows = bd.ToImmutable();
|
||||
_selected_rows_changed.execute();
|
||||
}
|
||||
|
||||
void IHandleGridSorting.on_sorting(object sender, DIYGridSortingEventArgs e) {
|
||||
_sort_manager.OnColumnClick(e.Column, KeyboardExtensions.IsShiftDown());
|
||||
data_comparer = _sort_manager.BuildComparer(id => _find_column(id)?.row_comparer, _data_comparer);
|
||||
}
|
||||
|
||||
RiskManagementColumnModel _find_column(string key_str) =>
|
||||
Enum.TryParse(key_str, true, out Columns key) ? _find_column(key) : null;
|
||||
|
||||
RiskManagementColumnModel _find_column(Columns key) =>
|
||||
_column_map.GetValueOrDefault(key);
|
||||
|
||||
Func<AccountSecurityRiskLimits, bool> _mk_filter() {
|
||||
var col_filter_pred = create().filters.build_predicate();
|
||||
if (!selected_account.HasValue)
|
||||
return lim => false;
|
||||
var acc_id = selected_account.Value.id;
|
||||
return lim => lim.account_key == acc_id && (col_filter_pred is null || col_filter_pred(lim));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<Window
|
||||
x:Class="bts.shell.risk_management.RiskLimitParametersDialog"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:risk="clr-namespace:bts.shell.risk_management"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
u:WindowStyle.AllowResize="False"
|
||||
u:WindowStyle.SizeToContent="WidthAndHeight"
|
||||
FocusVisualStyle="{x:Null}"
|
||||
Style="{StaticResource BTSDialog}"
|
||||
>
|
||||
<Window.Resources>
|
||||
<sys:Double x:Key="HorizontalPush">4</sys:Double>
|
||||
<sys:Double x:Key="VerticalPush3">8</sys:Double>
|
||||
<Thickness x:Key="ParamMargin" Left="{StaticResource HorizontalPush}"/>
|
||||
<Style x:Key="HeaderText" TargetType="TextBlock">
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Foreground" Value="{x:Static u:BTSColors.header_text_brush}"/>
|
||||
<Setter Property="Margin" Value="4"/>
|
||||
</Style>
|
||||
<Style x:Key="LimitToggle" TargetType="CheckBox">
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
<Window.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type risk:RiskLimitsModificationVM}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush}}"/>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="{Binding Source={StaticResource VerticalPush3}}"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Rectangle Grid.ColumnSpan="17" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Fill="{x:Static u:BTSColors.header_brush}"/>
|
||||
<TextBlock Grid.Column="0" Text="Security" Visibility="{Binding can_edit_security, Converter={StaticResource BooleanToVisibilityConverter}}" Style="{StaticResource HeaderText}"/>
|
||||
<TextBlock Grid.Column="2" Text="Max Long" Style="{StaticResource HeaderText}"/>
|
||||
<TextBlock Grid.Column="4" Text="Max Short" Style="{StaticResource HeaderText}"/>
|
||||
<TextBlock Grid.Column="6" Text="Clip Limit" Style="{StaticResource HeaderText}"/>
|
||||
<TextBlock Grid.Column="8" Text="Min Price" Style="{StaticResource HeaderText}"/>
|
||||
<TextBlock Grid.Column="10" Text="Max Price" Style="{StaticResource HeaderText}"/>
|
||||
<TextBlock Grid.Column="12" Text="Position Limit Enabled" Style="{StaticResource HeaderText}"/>
|
||||
<TextBlock Grid.Column="14" Text="Clip Limit Enabled" Style="{StaticResource HeaderText}"/>
|
||||
<TextBlock Grid.Column="16" Text="Price Limit Enabled" Style="{StaticResource HeaderText}"/>
|
||||
|
||||
<c:ContractPickerPopup
|
||||
Grid.Row="2"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding security_criteria, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}}"
|
||||
Visibility="{Binding can_edit_security, Converter={StaticResource BooleanToVisibilityConverter}}"
|
||||
IsEditMode="True"
|
||||
IsLazy="True"
|
||||
SecurityMap="{Binding security_map}"
|
||||
SelectedCriteria="{Binding security_criteria}"
|
||||
ShowDialogSkirt="True"
|
||||
ToolTip="{Binding security_criteria, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}}"
|
||||
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityHandler}"
|
||||
/>
|
||||
<c:TickUpDownNullableUInt Grid.Row="2" Grid.Column="2" Value="{Binding max_long_position}"/>
|
||||
<c:TickUpDownNullableUInt Grid.Row="2" Grid.Column="4" Value="{Binding max_short_position}"/>
|
||||
<c:TickUpDownNullableUInt Grid.Row="2" Grid.Column="6" Value="{Binding clip_limit}"/>
|
||||
<c:TickUpDownNullableDouble Grid.Row="2" Grid.Column="8" Value="{Binding min_price}"/>
|
||||
<c:TickUpDownNullableDouble Grid.Row="2" Grid.Column="10" Value="{Binding max_price}"/>
|
||||
<CheckBox Grid.Row="2" Grid.Column="12" IsChecked="{Binding is_position_limit_enabled}" Style="{StaticResource LimitToggle}"/>
|
||||
<CheckBox Grid.Row="2" Grid.Column="14" IsChecked="{Binding is_clip_limit_enabled}" Style="{StaticResource LimitToggle}"/>
|
||||
<CheckBox Grid.Row="2" Grid.Column="16" IsChecked="{Binding is_price_limit_enabled}" Style="{StaticResource LimitToggle}"/>
|
||||
</Grid>
|
||||
<c:DialogSkirt Grid.Row="1" Margin="0,6,0,0">
|
||||
<c:DialogSkirt.RightItems>
|
||||
<x:Array Type="{x:Type Button}">
|
||||
<Button Content="OK" Command="{Binding commit_cmd}" IsDefault="True"/>
|
||||
<Button Content="Cancel" Command="{Binding cancel_cmd}" IsCancel="True"/>
|
||||
</x:Array>
|
||||
</c:DialogSkirt.RightItems>
|
||||
</c:DialogSkirt>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Window.ContentTemplate>
|
||||
</Window>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
/// <summary>
|
||||
/// Interaction logic for RiskLimitParametersDialog.xaml
|
||||
/// </summary>
|
||||
partial class RiskLimitParametersDialog : Window {
|
||||
public RiskLimitParametersDialog() =>
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
<UserControl
|
||||
x:Class="bts.shell.risk_management.RiskManagementControl"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
xmlns:cv="clr-namespace:mbts.ui.converters"
|
||||
xmlns:risk="clr-namespace:bts.shell.risk_management"
|
||||
xmlns:sys="clr-namespace:System;assembly=mscorlib"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance risk:RiskManagementVM, IsDesignTimeCreatable=True}"
|
||||
Background="{StaticResource CommonBackground}"
|
||||
>
|
||||
<UserControl.Resources>
|
||||
<Style x:Key="TextCell" TargetType="c:TextCell">
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
<Setter Property="Padding" Value="2,0"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="{Binding Path='(c:DIYGrid.Column).horizontal_alignment', RelativeSource={RelativeSource Self}}"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
<Setter Property="Background" Value="{Binding Path='(c:DIYGrid.Column).background_color', RelativeSource={RelativeSource Self}, Converter={x:Static cv:ColorToBrushConverter.Instance}}"/>
|
||||
<Setter Property="Foreground" Value="{Binding Path='(c:DIYGrid.Column).font_color', RelativeSource={RelativeSource Self}, Converter={x:Static cv:ColorToBrushConverter.Instance}}"/>
|
||||
<Setter Property="FontFamily" Value="{Binding Path='(c:DIYGrid.Column).font_family', RelativeSource={RelativeSource Self}}"/>
|
||||
<Setter Property="FontWeight" Value="{Binding Path='(c:DIYGrid.Column).font_weight', RelativeSource={RelativeSource Self}}"/>
|
||||
<Setter Property="FontSize" Value="{Binding Path='(c:DIYGrid.Column).font_size', RelativeSource={RelativeSource Self}}"/>
|
||||
</Style>
|
||||
<Style x:Key="CellBorder" TargetType="Border">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="VerticalAlignment" Value="Stretch"/>
|
||||
<Setter Property="Background" Value="{Binding Path='(c:DIYGrid.Column).background_color', RelativeSource={RelativeSource Self}, Converter={x:Static cv:ColorToBrushConverter.Instance}}"/>
|
||||
<Setter Property="Padding" Value="2,0"/>
|
||||
</Style>
|
||||
<Style x:Key="LimitToggleSwitch" TargetType="CheckBox" BasedOn="{StaticResource {x:Type CheckBox}}">
|
||||
<Setter Property="Foreground" Value="{Binding Path='(c:DIYGrid.Column).font_color', RelativeSource={RelativeSource AncestorType=Border}, Converter={x:Static cv:ColorToBrushConverter.Instance}}"/>
|
||||
<Setter Property="HorizontalAlignment" Value="{Binding Path='(c:DIYGrid.Column).horizontal_alignment', RelativeSource={RelativeSource AncestorType=Border}}"/>
|
||||
<Setter Property="VerticalAlignment" Value="Center"/>
|
||||
</Style>
|
||||
</UserControl.Resources>
|
||||
<AdornerDecorator>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="4,6">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center">Account:</TextBlock>
|
||||
<ComboBox Grid.Column="1" Margin="8,0" ItemsSource="{Binding accounts}" SelectedItem="{Binding selected_account}"/>
|
||||
</Grid>
|
||||
<c:DIYGrid
|
||||
Grid.Row="1"
|
||||
c:DIYGrid.ColumnModels="{Binding columns}"
|
||||
c:DIYGrid.SyncWithColumnGeometry="True"
|
||||
Rows="{Binding rows}"
|
||||
RowHeight="{u:DPIDouble 21.6}"
|
||||
>
|
||||
<c:DIYGrid.Resources>
|
||||
<ResourceDictionary Source="/view/Controls/DataGridEx/Filters/Views/FilterTemplates.xaml"/>
|
||||
</c:DIYGrid.Resources>
|
||||
<c:DIYGrid.Columns>
|
||||
<c:DIYGridColumnCollection/>
|
||||
</c:DIYGrid.Columns>
|
||||
<c:DIYGrid.CachedColumnParams>
|
||||
<x:Array Type="{x:Type sys:String}">
|
||||
<sys:String>font_weight</sys:String>
|
||||
<sys:String>font_size</sys:String>
|
||||
<sys:String>font_brush</sys:String>
|
||||
<sys:String>font_family</sys:String>
|
||||
<sys:String>horizontal_alignment</sys:String>
|
||||
</x:Array>
|
||||
</c:DIYGrid.CachedColumnParams>
|
||||
|
||||
<c:DIYGrid.RowTemplate>
|
||||
<DataTemplate DataType="{x:Type risk:RiskLimitsVM}">
|
||||
<Grid c:DIYGrid.SyncWithColumnGeometry="True" Height="{Binding RowHeight, RelativeSource={RelativeSource AncestorType=c:DIYGrid}}">
|
||||
<!--Security-->
|
||||
<c:TextCell
|
||||
c:DIYGrid.ColumnKey="Security"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[Security]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Text="{Binding security}"
|
||||
Style="{StaticResource TextCell}"
|
||||
/>
|
||||
<c:TextCell
|
||||
c:DIYGrid.ColumnKey="SecurityKey"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[SecurityKey]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Text="{Binding security_key}"
|
||||
Style="{StaticResource TextCell}"
|
||||
/>
|
||||
<!--Position-->
|
||||
<c:TextCell
|
||||
c:DIYGrid.ColumnKey="MaxNetLongPosition"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[MaxNetLongPosition]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Text="{Binding max_long_position}"
|
||||
Style="{StaticResource TextCell}"
|
||||
/>
|
||||
<c:TextCell
|
||||
c:DIYGrid.ColumnKey="MaxNetShortPosition"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[MaxNetShortPosition]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Text="{Binding max_short_position}"
|
||||
Style="{StaticResource TextCell}"
|
||||
/>
|
||||
<Border
|
||||
c:DIYGrid.ColumnKey="IsPositionLimitEnabled"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[IsPositionLimitEnabled]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Style="{StaticResource CellBorder}"
|
||||
>
|
||||
<CheckBox
|
||||
IsChecked="{Binding is_position_limit_enabled}"
|
||||
Style="{StaticResource LimitToggleSwitch}"
|
||||
/>
|
||||
</Border>
|
||||
<!--Clip-->
|
||||
<c:TextCell
|
||||
c:DIYGrid.ColumnKey="ClipLimit"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[ClipLimit]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Text="{Binding clip_limit}"
|
||||
Style="{StaticResource TextCell}"
|
||||
/>
|
||||
<Border
|
||||
c:DIYGrid.ColumnKey="IsClipLimitEnabled"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[IsClipLimitEnabled]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Style="{StaticResource CellBorder}"
|
||||
>
|
||||
<CheckBox
|
||||
IsChecked="{Binding is_clip_limit_enabled}"
|
||||
Style="{StaticResource LimitToggleSwitch}"
|
||||
/>
|
||||
</Border>
|
||||
<!--Price-->
|
||||
<c:TextCell
|
||||
c:DIYGrid.ColumnKey="MinPrice"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[MinPrice]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Text="{Binding min_price, StringFormat='#,0.0000'}"
|
||||
Style="{StaticResource TextCell}"
|
||||
/>
|
||||
<c:TextCell
|
||||
c:DIYGrid.ColumnKey="MaxPrice"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[MaxPrice]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Text="{Binding max_price, StringFormat='#,0.0000'}"
|
||||
Style="{StaticResource TextCell}"
|
||||
/>
|
||||
<Border
|
||||
c:DIYGrid.ColumnKey="IsPriceLimitEnabled"
|
||||
c:DIYGrid.Column="{Binding Path='DataContext.column_map[IsPriceLimitEnabled]', RelativeSource={RelativeSource AncestorType=c:DIYGrid}}"
|
||||
Style="{StaticResource CellBorder}"
|
||||
>
|
||||
<CheckBox
|
||||
IsChecked="{Binding is_price_limit_enabled}"
|
||||
Style="{StaticResource LimitToggleSwitch}"
|
||||
/>
|
||||
</Border>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</c:DIYGrid.RowTemplate>
|
||||
</c:DIYGrid>
|
||||
</Grid>
|
||||
</AdornerDecorator>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,33 @@
|
||||
using mbts.ui.controls;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace bts.shell.risk_management {
|
||||
/// <summary>
|
||||
/// Interaction logic for RiskManagementControl.xaml
|
||||
/// </summary>
|
||||
partial class RiskManagementControl : UserControl {
|
||||
public RiskManagementControl() {
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public enum Columns {
|
||||
None,
|
||||
Security,
|
||||
MaxNetLongPosition,
|
||||
MaxNetShortPosition,
|
||||
ClipLimit,
|
||||
MinPrice,
|
||||
MaxPrice,
|
||||
IsPositionLimitEnabled,
|
||||
IsClipLimitEnabled,
|
||||
IsPriceLimitEnabled,
|
||||
SecurityKey
|
||||
}
|
||||
}
|
||||
|
||||
class RiskManagementColumn : DIYGrid.ColumnBase<RiskManagementControl.Columns> {
|
||||
protected override void on_key_initialized(RiskManagementControl.Columns key) {
|
||||
id = key.ToString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using bts.api.position;
|
||||
using bts.shell.fills;
|
||||
using bts.shell.manual_fills;
|
||||
using bts.shell.risk_management;
|
||||
using bts.spark;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
@@ -27,6 +28,7 @@ namespace bts.shell {
|
||||
readonly PNLWindowFactory _plwf;
|
||||
readonly AlgoStudioWindowFactory _aswf;
|
||||
readonly ManualFillsWindowFactory _mfwf;
|
||||
readonly RiskManagementWindowFactory _rmwf;
|
||||
//
|
||||
List<SparkCommand> _actions;
|
||||
|
||||
@@ -54,6 +56,7 @@ namespace bts.shell {
|
||||
_old_plwf = new PAndLWindowFactory(this, bus, settings_hub, orders_model);
|
||||
_aswf = new AlgoStudioWindowFactory(this, bus, orders_model, error_reporter);
|
||||
_mfwf = new ManualFillsWindowFactory(this, bus, orders_model);
|
||||
_rmwf = new RiskManagementWindowFactory(this, bus, settings_hub, orders_model.admin_model);
|
||||
}
|
||||
|
||||
public ImageSource icon => Icons.Trades;
|
||||
@@ -77,7 +80,8 @@ namespace bts.shell {
|
||||
SparkCommands.PickLadder,
|
||||
SparkCommands.PickOrderTicket,
|
||||
SparkCommands.ShowAlgoStudio.complete(() => new AlgoStudioWindowFactory.StartupData()),
|
||||
SparkCommands.ShowManualFills().complete(() => ManualFillsSettings.Default)
|
||||
SparkCommands.ShowManualFills().complete(() => ManualFillsSettings.Default),
|
||||
SparkCommands.ShowRiskManagement.complete(() => RiskManagementSettings.Default)
|
||||
});
|
||||
}
|
||||
return _actions;
|
||||
@@ -95,6 +99,7 @@ namespace bts.shell {
|
||||
yield return _old_plwf;
|
||||
yield return _aswf;
|
||||
yield return _mfwf;
|
||||
yield return _rmwf;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -155,9 +155,32 @@ namespace bts {
|
||||
Disposable.dispose(ref _dispose);
|
||||
}
|
||||
|
||||
public IObservable<Update<(SecurityImmutable? sec, T val), TKey>> join<T,TKey>(
|
||||
IObservable<Update<T, TKey>> source, Func<T,security_key> skeyfn) where TKey : IEquatable<TKey> {
|
||||
return Observable.Create<Update<(SecurityImmutable?, T), TKey>>(observer => {
|
||||
public IObservable<Update<(SecurityImmutable? sec, T val), TKey>> join<T, TKey>(
|
||||
IObservable<Update<T, TKey>> source, Func<T, security_key> skeyfn) where TKey : IEquatable<TKey> {
|
||||
return join(source, skeyfn, get_incremental_props, mk_update);
|
||||
|
||||
(IEnumerable<T> all, IEnumerable<T> added, IEnumerable<Change<T>> changes, IEnumerable<T> removed, Func<T, TKey> key_extractor) get_incremental_props(Update<T, TKey> upd_in) =>
|
||||
(upd_in.all, upd_in.added, upd_in.changes, upd_in.removed, upd_in.key_extractor);
|
||||
|
||||
Update<(SecurityImmutable? sec, T val), TKey> mk_update(
|
||||
IEnumerable<(SecurityImmutable? sec, T val)> all,
|
||||
IEnumerable<(SecurityImmutable? sec, T val)> added,
|
||||
IEnumerable<Change<(SecurityImmutable? sec, T val)>> changes,
|
||||
IEnumerable<(SecurityImmutable? sec, T val)> removed,
|
||||
Func<(SecurityImmutable?, T), TKey> key_extractor) =>
|
||||
//
|
||||
new Update<(SecurityImmutable? sec, T val), TKey>(
|
||||
all, added, changes, removed, key_extractor);
|
||||
}
|
||||
|
||||
public IObservable<TOut> join<TIn, T, TKey, TOut>(
|
||||
IObservable<TIn> source,
|
||||
Func<T,security_key> skeyfn,
|
||||
Func<TIn, (IEnumerable<T> all, IEnumerable<T> added, IEnumerable<Change<T>> changes, IEnumerable<T> removed, Func<T, TKey> key_extractor)> get_incremental_props,
|
||||
Func<IEnumerable<(SecurityImmutable? sec, T val)>, IEnumerable<(SecurityImmutable? sec, T val)>, IEnumerable<Change<(SecurityImmutable? sec, T val)>>, IEnumerable<(SecurityImmutable? sec, T val)>, Func<(SecurityImmutable?, T), TKey>, TOut> create_result_update)
|
||||
where TKey : IEquatable<TKey> {
|
||||
//
|
||||
return Observable.Create<TOut>(observer => {
|
||||
var state = ImmutableDictionary<TKey, (SecurityImmutable? s, T t)>.Empty;
|
||||
var gate = new object();
|
||||
// We get thie function from the first source update
|
||||
@@ -186,11 +209,11 @@ namespace bts {
|
||||
state = builder.ToImmutable();
|
||||
// fire update
|
||||
observer.OnNext(
|
||||
new Update<(SecurityImmutable?, T), TKey>(
|
||||
state.Values,
|
||||
Enumerable.Empty<(SecurityImmutable?,T)>(),
|
||||
changed,
|
||||
Enumerable.Empty<(SecurityImmutable?,T)>(),
|
||||
create_result_update(
|
||||
state.Values,
|
||||
Enumerable.Empty<(SecurityImmutable?, T)>(),
|
||||
changed,
|
||||
Enumerable.Empty<(SecurityImmutable?, T)>(),
|
||||
rowkeyfn));
|
||||
}
|
||||
}
|
||||
@@ -198,10 +221,11 @@ namespace bts {
|
||||
|
||||
return Disposable.Create(
|
||||
// Fire update when source changes
|
||||
source.Subscribe((update) => {
|
||||
source.Subscribe((update_in) => {
|
||||
var update = get_incremental_props(update_in);
|
||||
lock (gate) {
|
||||
rowkeyfn = rowkeyfn ?? (i => update.key_extractor(i.Item2));
|
||||
var relevant_securities = get_and_ensure_securities(update.added.Concat(update.changed).Select(skeyfn));
|
||||
var relevant_securities = get_and_ensure_securities(update.added.Concat(update.changes.New()).Select(skeyfn));
|
||||
|
||||
var added = new List<(SecurityImmutable?, T)>();
|
||||
var changed = new List<Change<(SecurityImmutable? s, T t)>>();
|
||||
@@ -238,8 +262,7 @@ namespace bts {
|
||||
}
|
||||
state = builder.ToImmutable();
|
||||
|
||||
observer.OnNext(new Update<(SecurityImmutable?, T t), TKey>(
|
||||
state.Values, added, changed, removed, rowkeyfn));
|
||||
observer.OnNext(create_result_update(state.Values, added, changed, removed, rowkeyfn));
|
||||
}
|
||||
}),
|
||||
// Also fire update when securities change
|
||||
|
||||
@@ -28,6 +28,9 @@ namespace bts.spark {
|
||||
public static PartialCommand<bts.shell.orderbook.OrderBookStartupData> ShowOrders =>
|
||||
SparkCommand.create("Order Book", Icons.Book, SparkTags.ShowOrderBookTag);
|
||||
|
||||
public static PartialCommand<shell.risk_management.RiskManagementSettings> ShowRiskManagement =>
|
||||
SparkCommand.create("Risk Management", Icons.Risk, SparkTags.ShowRiskManagement);
|
||||
|
||||
// Fills
|
||||
public static PartialCommand<FillsSettings> ShowFills(KeyGesture key_gesture = null) =>
|
||||
SparkCommand.create("Fills", Icons.Payment, SparkTags.ShowFillsTag, key_gesture);
|
||||
|
||||
@@ -63,6 +63,7 @@ namespace mbts.spark {
|
||||
public static ITag<IObservable<SecurityMD>> ShowLinkedLadder { get; } = new SparkTag<IObservable<SecurityMD>>("Linked Ladder");
|
||||
public static ITag<SecurityCriteria> ShowMarketViewTag { get; } = new SparkTag<SecurityCriteria>("Show MarketView");
|
||||
public static ITag<bts.shell.orderbook.OrderBookStartupData> ShowOrderBookTag { get; } = new SparkTag<bts.shell.orderbook.OrderBookStartupData>(nameof(ShowOrderBookTag));
|
||||
public static ITag<bts.shell.risk_management.RiskManagementSettings> ShowRiskManagement { get; } = new SparkTag<bts.shell.risk_management.RiskManagementSettings>(nameof(ShowRiskManagement));
|
||||
public static ITag<PAndLWindowFactory.PAndLStartupData> ShowPAndLTag { get; } = new SparkTag<PAndLWindowFactory.PAndLStartupData>(nameof(ShowPAndLTag));
|
||||
public static ITag<bts.shell.pnl.PNLSettings> ShowPNLTag { get; } = new SparkTag<bts.shell.pnl.PNLSettings>(nameof(ShowPNLTag));
|
||||
public static ITag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)> ShowSyntheticCloneTag { get; } = new SparkTag<(SecurityImmutable security, ExecutionStrategyImmutable strategy)>(nameof(ShowSyntheticCloneTag));
|
||||
|
||||
@@ -17,6 +17,7 @@ namespace bts.settings {
|
||||
interface IMarketViewGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IPAndLGridSettingsMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface IPNLSettingsMigrationRule : ISettingsMigrationRule<PNLSettings> { }
|
||||
interface IRiskManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
interface ISyntheticManagementMigrationRule : ISettingsMigrationRule<GridSettings> { }
|
||||
|
||||
static class SettingsMigrationHelper {
|
||||
|
||||
@@ -61,6 +61,8 @@ namespace mbts.spark {
|
||||
PNLSettings _pnl;
|
||||
[ProtoMember(14)]
|
||||
GlobalAlertSettings _alerts;
|
||||
[ProtoMember(15)]
|
||||
GridSettings _risk_management_grid;
|
||||
|
||||
public UserSettings() { }
|
||||
|
||||
@@ -74,6 +76,7 @@ namespace mbts.spark {
|
||||
public GridSettings market_view_grid => _market_view_grid ?? (_market_view_grid = bts.shell.MarketViewWindowFactory.StartupData.DefaultGridSettings);
|
||||
public GridSettings time_and_sales_grid => _time_and_sales_grid ?? (_time_and_sales_grid = GridSettings.Default.with(bts.shell.time_and_sales.TimeAndSalesSettings.DefaultColumns.all));
|
||||
public GridSettings fills_grid => _fills_grid ?? (_fills_grid = bts.shell.fills.FillsSettings.DefaultGridSettings);
|
||||
public GridSettings risk_management_grid => _risk_management_grid ?? (_risk_management_grid = bts.shell.risk_management.RiskManagementSettings.DefaultGrid);
|
||||
public GridSettings synthetic_management_grid => _synthetic_management_grid ?? (_synthetic_management_grid = bts.shell.SyntheticManagementWindowFactory.StartupData.DefaultGridSettings);
|
||||
public GridSettings pnl_grid => _pnl_grid ?? (_pnl_grid = bts.shell.PAndLWindowFactory.PAndLStartupData.DefaultGridSettings);
|
||||
public PNLSettings pnl => _pnl ?? (_pnl = PNLSettings.Default);
|
||||
@@ -91,7 +94,8 @@ namespace mbts.spark {
|
||||
GridSettings provided_pnl_gridsettings = null,
|
||||
PNLSettings pnl = null,
|
||||
bts.shell.orderbook.OrderBookGridSettings order_book_grid = null,
|
||||
GlobalAlertSettings alerts = null) {
|
||||
GlobalAlertSettings alerts = null,
|
||||
GridSettings risk_management_grid = null) {
|
||||
var us = new UserSettings(ladder_profiles ?? _ladder_profiles) {
|
||||
_ladder = ladder ?? _ladder,
|
||||
_notification = notification ?? _notification,
|
||||
@@ -102,7 +106,8 @@ namespace mbts.spark {
|
||||
_pnl_grid = provided_pnl_gridsettings ?? _pnl_grid,
|
||||
_pnl = pnl ?? _pnl,
|
||||
_order_book_grid = order_book_grid ?? _order_book_grid,
|
||||
_alerts = alerts ?? _alerts
|
||||
_alerts = alerts ?? _alerts,
|
||||
_risk_management_grid = risk_management_grid ?? _risk_management_grid
|
||||
};
|
||||
us._last_ladder_profile_id = Math.Max(_last_ladder_profile_id, us.ladder_profiles.Select(p => p.id).MaxOrDefault());
|
||||
return us;
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace mbts.persist {
|
||||
_fills_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.fills_grid, (us, gs) => us.with(provided_fills_gridsettings: gs)),
|
||||
_mview_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.market_view_grid, (us, gs) => us.with(provided_market_view_gridsettings: gs)),
|
||||
_pandl_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.pnl_grid, (us, gs) => us.with(provided_pnl_gridsettings: gs)),
|
||||
_risk_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.risk_management_grid, (us, gs) => us.with(risk_management_grid: gs)),
|
||||
_synth_propi = ImmutableProperty.create<UserSettings, GridSettings>(us => us.synthetic_management_grid, (us, gs) => us.with(provided_synthetic_management_gridsettings: gs));
|
||||
|
||||
static readonly IImmutableProperty<UserSettings, PNLSettings>
|
||||
@@ -37,6 +38,7 @@ namespace mbts.persist {
|
||||
readonly IEnumerable<IMarketViewGridSettingsMigrationRule> _mview_migration_rules;
|
||||
readonly IEnumerable<IPAndLGridSettingsMigrationRule> _pandl_migration_rules;
|
||||
readonly IEnumerable<IPNLSettingsMigrationRule> _pnl_migration_rules;
|
||||
readonly IEnumerable<IRiskManagementMigrationRule> _risk_migration_rules;
|
||||
readonly IEnumerable<ISyntheticManagementMigrationRule> _synth_migration_rules;
|
||||
readonly IUserPersist _persist;
|
||||
|
||||
@@ -49,6 +51,7 @@ namespace mbts.persist {
|
||||
[ImportMany]IEnumerable<IMarketViewGridSettingsMigrationRule> mview_migration_rules,
|
||||
[ImportMany]IEnumerable<IPAndLGridSettingsMigrationRule> pandl_migration_rules,
|
||||
[ImportMany]IEnumerable<IPNLSettingsMigrationRule> pnl_migration_rules,
|
||||
[ImportMany]IEnumerable<IRiskManagementMigrationRule> risk_migration_rules,
|
||||
[ImportMany]IEnumerable<ISyntheticManagementMigrationRule> synth_migration_rules) {
|
||||
//
|
||||
_persist = persist;
|
||||
@@ -56,6 +59,7 @@ namespace mbts.persist {
|
||||
_mview_migration_rules = mview_migration_rules ?? Enumerable.Empty<IMarketViewGridSettingsMigrationRule>();
|
||||
_pandl_migration_rules = pandl_migration_rules ?? Enumerable.Empty<IPAndLGridSettingsMigrationRule>();
|
||||
_pnl_migration_rules = pnl_migration_rules ?? Enumerable.Empty<IPNLSettingsMigrationRule>();
|
||||
_risk_migration_rules = risk_migration_rules ?? Enumerable.Empty<IRiskManagementMigrationRule>();
|
||||
_synth_migration_rules = synth_migration_rules ?? Enumerable.Empty<ISyntheticManagementMigrationRule>();
|
||||
init();
|
||||
async void init() {
|
||||
@@ -68,6 +72,7 @@ namespace mbts.persist {
|
||||
_mview_migration_rules.merge().wrap(_mview_propi),
|
||||
_pandl_migration_rules.merge().wrap(_pandl_propi),
|
||||
_pnl_migration_rules.merge().wrap(_pnl_propi),
|
||||
_risk_migration_rules.merge().wrap(_risk_propi),
|
||||
_synth_migration_rules.merge().wrap(_synth_propi)
|
||||
};
|
||||
SettingsMigrationHelper.apply_quiet(ref s, wrapped_rules);
|
||||
@@ -142,6 +147,12 @@ namespace mbts.persist {
|
||||
_pnl_migration_rules,
|
||||
_pnl_propi);
|
||||
|
||||
public IWindowSettingsManager<GridSettings> create_risk_management_settings_manager(GridSettings instance_settings) =>
|
||||
_create_settings_manager(
|
||||
instance_settings,
|
||||
_risk_migration_rules,
|
||||
_risk_propi);
|
||||
|
||||
public IWindowSettingsManager<GridSettings> create_synth_settings_manager(GridSettings instance_settings) =>
|
||||
_create_settings_manager(
|
||||
instance_settings,
|
||||
@@ -170,4 +181,4 @@ namespace mbts.persist {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using mbts;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
@@ -10,6 +11,13 @@ using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace bts.utils {
|
||||
class PropertyEqualityComparerAttribute : Attribute {
|
||||
public PropertyEqualityComparerAttribute(Type comparer_type) =>
|
||||
this.comparer_type = comparer_type ?? throw new ArgumentNullException(nameof(comparer_type));
|
||||
|
||||
public Type comparer_type { get; }
|
||||
}
|
||||
|
||||
interface IPropertyChangedDetector<T> where T : IRaiseNotifyPropertyChanged {
|
||||
IDisposable detect_changes(T target);
|
||||
}
|
||||
@@ -68,6 +76,42 @@ namespace bts.utils {
|
||||
public static IObservable<bool> dirty_monitor<T>(T instance, PropertyInfo[] props) where T : INotifyPropertyChanged =>
|
||||
_dirty_monitor(instance, props.ToDictionary(p => p.Name, p => new PropNode(p, p.GetValue(instance))));
|
||||
|
||||
public static Func<T, int> property_hash_code_delegate<T>(string prop) =>
|
||||
DelegateStore<T>.property_hash_code(prop);
|
||||
|
||||
public static Func<T, int> property_hash_code_delegate<T>(IEnumerable<string> props) {
|
||||
var prop_hash_code_dels = props.Select(DelegateStore<T>.property_hash_code).ExceptNull().ToArray();
|
||||
return impl;
|
||||
int impl(T a) =>
|
||||
EqualityComparer.auto_phase(prop_hash_code_dels.Select(peq => peq(a)));
|
||||
}
|
||||
|
||||
public static Func<T, T, bool> property_equals_delegate<T>(string prop) =>
|
||||
DelegateStore<T>.property_equals(prop);
|
||||
|
||||
public static Func<T, T, bool> property_equals_delegate<T>(IEnumerable<string> props) {
|
||||
var prop_equals_dels = props.Select(DelegateStore<T>.property_equals).ExceptNull().ToArray();
|
||||
return impl;
|
||||
bool impl(T a, T b) =>
|
||||
prop_equals_dels.All(peq => peq(a, b));
|
||||
}
|
||||
|
||||
public static IEqualityComparer<T> property_equality_comparer<T>() {
|
||||
var prop_infos = PropertyInfoStore<T>.all_props;
|
||||
return property_equality_comparer<T>(
|
||||
prop_infos.Select(p => p.Name),
|
||||
// hash code should NEVER include mutable props
|
||||
prop_infos.Where(pi => !pi.CanWrite).Select(p => p.Name));
|
||||
}
|
||||
|
||||
public static IEqualityComparer<T> property_equality_comparer<T>(IEnumerable<string> props) =>
|
||||
property_equality_comparer<T>(props, props);
|
||||
|
||||
public static IEqualityComparer<T> property_equality_comparer<T>(IEnumerable<string> equals_props, IEnumerable<string> hash_code_props) =>
|
||||
EqualityComparer.Create(
|
||||
property_equals_delegate<T>(equals_props),
|
||||
property_hash_code_delegate<T>(hash_code_props));
|
||||
|
||||
static IObservable<bool> _dirty_monitor<T>(T instance, Dictionary<string, PropNode> prop_map) where T : INotifyPropertyChanged =>
|
||||
Observable.Create<bool>(o => {
|
||||
var dirty_props = new HashSet<string>();
|
||||
@@ -106,6 +150,8 @@ namespace bts.utils {
|
||||
PropertyInfo prop_info { get; }
|
||||
Func<T, object> getter { get; }
|
||||
Action<T, object> setter { get; }
|
||||
Func<T, T, bool> property_equals { get; }
|
||||
Func<T, int> property_hash_code { get; }
|
||||
(Action<T> initialize, Func<T, bool> refresh) create_snapshot();
|
||||
(Action initialize, Func<bool> refresh) create_snapshot(T source);
|
||||
}
|
||||
@@ -139,6 +185,15 @@ namespace bts.utils {
|
||||
public static Action<T, object> setter(string prop_name) =>
|
||||
accessor_data.TryGetValue(prop_name, out var dels) ? dels.setter : null;
|
||||
|
||||
public static Func<T, T, bool> property_equals(string prop_name) =>
|
||||
accessor_data.TryGetValue(prop_name, out var dels) ? dels.property_equals : null;
|
||||
|
||||
public static Func<T, int> property_hash_code(string prop_name) =>
|
||||
accessor_data.TryGetValue(prop_name, out var dels) ? dels.property_hash_code : null;
|
||||
|
||||
public static IPropertyAccessorData<T> get_accessor(string prop_name) =>
|
||||
accessor_data.GetValueOrDefault(prop_name);
|
||||
|
||||
static DelegateStore() {
|
||||
var prop_dels = new Dictionary<string, IPropertyAccessorData<T>>();
|
||||
var accessor_type_cache = new Dictionary<Type, Type>();
|
||||
@@ -185,6 +240,7 @@ namespace bts.utils {
|
||||
class PropertyAccessorData<T, TValue> : IPropertyAccessorData<T, TValue> {
|
||||
readonly Func<T, object> _ng_get;
|
||||
readonly Action<T, object> _ng_set;
|
||||
IEqualityComparer<TValue> _property_equals_comp;
|
||||
|
||||
public PropertyAccessorData(PropertyInfo prop_info) {
|
||||
this.prop_info = prop_info;
|
||||
@@ -200,11 +256,27 @@ namespace bts.utils {
|
||||
setter = set_method.CreateDelegate(typeof(Action<T, TValue>)) as Action<T, TValue>;
|
||||
_ng_set = (src, val) => setter(src, (TValue)val);
|
||||
}
|
||||
if (prop_info.GetCustomAttribute<PropertyEqualityComparerAttribute>()?.comparer_type is Type comp_type) {
|
||||
_property_equals_comp = Activator.CreateInstance(comp_type) as IEqualityComparer<TValue>;
|
||||
}
|
||||
if (_property_equals_comp is null) {
|
||||
_property_equals_comp = EqualityComparer<TValue>.Default;
|
||||
}
|
||||
}
|
||||
|
||||
public Func<T, TValue> getter { get; }
|
||||
public Action<T, TValue> setter { get; }
|
||||
public PropertyInfo prop_info { get; }
|
||||
public Func<T, T, bool> property_equals {
|
||||
get {
|
||||
return impl;
|
||||
bool impl(T a, T b) =>
|
||||
_property_equals_comp.Equals(getter(a), getter(b));
|
||||
}
|
||||
}
|
||||
public Func<T, int> property_hash_code =>
|
||||
a => _property_equals_comp.GetHashCode(getter(a));
|
||||
|
||||
Func<T, object> IPropertyAccessorData<T>.getter => _ng_get;
|
||||
Action<T, object> IPropertyAccessorData<T>.setter => _ng_set;
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
using System;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
|
||||
namespace mbts.ui.utility {
|
||||
@@ -153,6 +158,94 @@ namespace mbts.ui.utility {
|
||||
_try_match_item(_item_to_value(item), out match);
|
||||
}
|
||||
|
||||
class SelectorVM<T, TKey> {
|
||||
readonly IEqualityComparer<T> _eq_comparer;
|
||||
readonly IEqualityComparer<TKey> _key_comparer;
|
||||
readonly Func<T, TKey> _key_fn;
|
||||
readonly ObservableCollection<T> _items = new ObservableCollection<T>();
|
||||
readonly Dictionary<TKey, T> _map;
|
||||
/// not as noisy as <see cref="ObservableCollection{T}.CollectionChanged"/>,
|
||||
/// this event is only raised after a batch collection change has been completed.
|
||||
/// <see cref="update_items(IEnumerable{T})"/>.
|
||||
IComparer<T> _comparer;
|
||||
T _selected_item;
|
||||
TKey _pending_selected_item_key;
|
||||
bool _is_pending_selection;
|
||||
|
||||
public SelectorVM(Func<T, TKey> key_fn)
|
||||
: this(key_fn, EqualityComparer<TKey>.Default) { }
|
||||
|
||||
public SelectorVM(Func<T, TKey> key_fn, IEqualityComparer<TKey> key_comparer) {
|
||||
_key_fn = key_fn ?? throw new ArgumentNullException(nameof(key_fn));
|
||||
_key_comparer = key_comparer ?? throw new ArgumentNullException(nameof(key_comparer));
|
||||
_eq_comparer = EqualityComparer.Create(_key_fn, _key_comparer);
|
||||
_map = new Dictionary<TKey, T>(key_comparer);
|
||||
items = ObservableAutoWrapper.create_read_only_wrapper(_items);
|
||||
}
|
||||
|
||||
public event Action selected_item_changed;
|
||||
|
||||
public IReadOnlyObservableCollection<T> items { get; }
|
||||
public T selected_item {
|
||||
get => _selected_item;
|
||||
set {
|
||||
if (!_eq_comparer.Equals(_selected_item, value)) {
|
||||
_selected_item = value;
|
||||
selected_item_changed?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
public IComparer<T> comparer {
|
||||
get => _comparer;
|
||||
set {
|
||||
_comparer = value;
|
||||
if (value != null)
|
||||
_update_items(items, false);
|
||||
}
|
||||
}
|
||||
|
||||
public bool try_select(T item, bool wait_for_item_to_arrive = false) =>
|
||||
try_select(_key_fn(item), wait_for_item_to_arrive);
|
||||
|
||||
public bool try_select(TKey item_key, bool wait_for_item_to_arrive = false) {
|
||||
if (_map.TryGetValue(item_key, out var item)) {
|
||||
selected_item = item;
|
||||
return true;
|
||||
} else if (wait_for_item_to_arrive) {
|
||||
_pending_selected_item_key = item_key;
|
||||
_is_pending_selection = true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void update_items(IEnumerable<T> items) =>
|
||||
_update_items(items, true);
|
||||
|
||||
void _update_items(IEnumerable<T> items, bool map_changed) {
|
||||
// The currently selected item may be removed during this collection change.
|
||||
// After the items have been changed, attempt to re-select the last selected item;
|
||||
// if there is already a particular 'pending selection,' attempt to select that instead.
|
||||
var target_key = _is_pending_selection ? _pending_selected_item_key : _key_fn(_selected_item);
|
||||
if (map_changed)
|
||||
_map.Clear();
|
||||
if (items is null) {
|
||||
_items.Clear();
|
||||
} else {
|
||||
if (_comparer != null) {
|
||||
items = items.Ordered(_comparer);
|
||||
}
|
||||
_items.sync_with(items);
|
||||
}
|
||||
if (map_changed) {
|
||||
foreach (var i in this.items)
|
||||
_map[_key_fn(i)] = i;
|
||||
}
|
||||
if (_map.Count != this.items.Count)
|
||||
throw new InvalidOperationException("Map and items are out of sync (different item counts).");
|
||||
try_select(target_key, _is_pending_selection || _items.Count == 0);
|
||||
}
|
||||
}
|
||||
|
||||
static class SelectionModelExtensions {
|
||||
public static IObservable<TValue> observe_selected_item<TItem, TValue>(this SelectionModel<TItem, TValue> selection_model, bool notify_on_subscribe = false) =>
|
||||
Observable.Create<TValue>(o => {
|
||||
@@ -166,4 +259,4 @@ namespace mbts.ui.utility {
|
||||
return Disposable.Create(() => selection_model.Changed -= callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -229,7 +229,10 @@ namespace mbts.ui.controls {
|
||||
public string Text { get => GetValue(TextProperty) as string; set => SetValue(TextProperty, value); }
|
||||
#endregion
|
||||
|
||||
protected override Size ArrangeOverride(Size finalSize) => finalSize;
|
||||
protected override Size ArrangeOverride(Size finalSize) {
|
||||
_gcr.available_size = finalSize;
|
||||
return finalSize;
|
||||
}
|
||||
|
||||
protected override Size MeasureOverride(Size availableSize) {
|
||||
_gcr.available_size = availableSize;
|
||||
|
||||
@@ -237,6 +237,15 @@ namespace mbts.ui.controls {
|
||||
Color? _background_color_override;
|
||||
object _header_copy;
|
||||
bool _is_visible;
|
||||
IDisposable _model_hooks;
|
||||
|
||||
static ColumnBase() {
|
||||
WidthProperty.OverrideMetadata(typeof(ColumnBase),
|
||||
new PropertyMetadata((d, e) => {
|
||||
if (d is ColumnBase cb)
|
||||
cb.width_changed?.Invoke((double)e.NewValue);
|
||||
}));
|
||||
}
|
||||
|
||||
protected ColumnBase() {
|
||||
var gs = GridSettings.Default;
|
||||
@@ -257,9 +266,20 @@ namespace mbts.ui.controls {
|
||||
set => SetValue(SortDirectionProperty, value);
|
||||
}
|
||||
|
||||
#region IsVisible
|
||||
public static readonly DependencyProperty IsVisibleProperty =
|
||||
WPFHelper.create_dp<bool, ColumnBase>(nameof(IsVisible), (o, v) => o.is_visible = v);
|
||||
|
||||
public bool IsVisible {
|
||||
get => (bool)GetValue(IsVisibleProperty);
|
||||
set => SetValue(IsVisibleProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
public event Action<ColumnBase, ListSortDirection?> sort_direction_changed;
|
||||
public event Action font_params_changed;
|
||||
public event Action<bool> is_visible_changed;
|
||||
public event Action<double> width_changed;
|
||||
|
||||
public string id { get; set; }
|
||||
public string sort_member_path { get; set; }
|
||||
@@ -273,6 +293,7 @@ namespace mbts.ui.controls {
|
||||
_is_visible = value;
|
||||
is_visible_changed?.Invoke(value);
|
||||
raise_prop_changed();
|
||||
SetCurrentValue(IsVisibleProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -297,6 +318,26 @@ namespace mbts.ui.controls {
|
||||
set => base.Header = (_header_copy = value);
|
||||
}
|
||||
|
||||
public void set_model(IDIYGridColumnModel model) {
|
||||
Disposable.dispose(ref _model_hooks);
|
||||
model.width = Width;
|
||||
width_changed += on_width_changed;
|
||||
model.width_changed += on_model_width_changed;
|
||||
_model_hooks = Disposable.Create(() => {
|
||||
width_changed -= on_width_changed;
|
||||
model.width_changed -= on_model_width_changed;
|
||||
});
|
||||
void on_model_width_changed(double value) {
|
||||
if (!DoubleUtil.AreClose(value, Width))
|
||||
SetCurrentValue(WidthProperty, value);
|
||||
}
|
||||
void on_width_changed(double value) {
|
||||
model.width_changed -= on_model_width_changed;
|
||||
model.width = value;
|
||||
model.width_changed += on_model_width_changed;
|
||||
}
|
||||
}
|
||||
|
||||
/// important: don't access any dependency props in here.
|
||||
/// this may be called from a thread other than that of this dispatcher (i.e., when <see cref="IShellWindow.GetState(System.IO.Stream)"/> is called).
|
||||
public GridColumnSettings create() =>
|
||||
@@ -327,7 +368,7 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
if (original_width != settings.width) {
|
||||
original_width = settings.width;
|
||||
Width = original_width;
|
||||
SetCurrentValue(WidthProperty, original_width);
|
||||
}
|
||||
//
|
||||
}
|
||||
|
||||
@@ -9,10 +9,10 @@ using System.Reactive.Linq;
|
||||
namespace mbts.ui {
|
||||
class ColumnSortManager {
|
||||
readonly List<ISortableDIYGridColumn> _visible_sorting = new List<ISortableDIYGridColumn>();
|
||||
readonly IList<ISortableDIYGridColumn> _columns;
|
||||
readonly IEnumerable<ISortableDIYGridColumn> _columns;
|
||||
readonly string _primary_key;
|
||||
|
||||
public ColumnSortManager(IList<ISortableDIYGridColumn> columns, string primary_key) {
|
||||
public ColumnSortManager(IEnumerable<ISortableDIYGridColumn> columns, string primary_key) {
|
||||
_columns = columns;
|
||||
_primary_key = primary_key;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
namespace mbts.ui.controls {
|
||||
using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.controls {
|
||||
class DIYGridColumn : DIYGrid.ColumnBase<string> {
|
||||
public DIYGridColumn()
|
||||
: base() {
|
||||
@@ -6,4 +21,191 @@
|
||||
Width = 60;
|
||||
}
|
||||
}
|
||||
|
||||
interface IDIYGridColumnModel : DIYGrid.IFilterableColumn {
|
||||
string id { get; }
|
||||
string column_template { get; }
|
||||
IComparer row_comparer { get; }
|
||||
double width { get; set; }
|
||||
event Action<double> width_changed;
|
||||
}
|
||||
|
||||
interface IDIYGridColumnFactory {
|
||||
DIYGrid.ColumnBase create_view();
|
||||
}
|
||||
|
||||
class DIYGridColumnModel<TKey> : ViewModelBase, IDIYGridColumnModel, ISortableDIYGridColumn<TKey>, ISortableDIYGridColumn, IValueFactory<GridColumnSettings> where TKey : struct {
|
||||
readonly DeferrableActionWrapper _result_changed;
|
||||
|
||||
FontWeight _fallback_font_weight;
|
||||
Color _fallback_font_color;
|
||||
Color _fallback_background_color;
|
||||
double _fallback_font_size;
|
||||
FontFamily _fallback_font_family;
|
||||
|
||||
double _width = 60;
|
||||
bool _is_visible = true;
|
||||
ListSortDirection? _sort_direction;
|
||||
object _custom_header;
|
||||
string _display_name;
|
||||
HorizontalAlignment _horizontal_alignment = HorizontalAlignment.Left;
|
||||
Color? _background_color_override, _font_color_override;
|
||||
FontWeight? _font_weight_override;
|
||||
|
||||
public DIYGridColumnModel(TKey id, IComparer row_comparer, IDataGridColumnFilter filter, string filter_template = null, string column_template = null) {
|
||||
this.id = id;
|
||||
this.row_comparer = row_comparer;
|
||||
this.filter = filter;
|
||||
this.filter_template = filter_template;
|
||||
this.column_template = column_template;
|
||||
_result_changed = new DeferrableActionWrapper(() => result_changed?.Invoke());
|
||||
}
|
||||
|
||||
public event Action result_changed;
|
||||
public event Action<double> width_changed;
|
||||
|
||||
public TKey id { get; }
|
||||
string IDIYGridColumnModel.id => id.ToString();
|
||||
public IComparer row_comparer { get; }
|
||||
public string display_name {
|
||||
get => _display_name;
|
||||
set {
|
||||
if (value != _display_name) {
|
||||
_display_name = value;
|
||||
_result_changed.execute();
|
||||
RaisePropertyChanged();
|
||||
if (_custom_header is null) {
|
||||
RaisePropertyChanged(nameof(header));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public object header => _custom_header ?? display_name;
|
||||
public object custom_header { get => _custom_header;
|
||||
set {
|
||||
if (value != _custom_header) {
|
||||
_custom_header = value;
|
||||
RaisePropertyChanged(nameof(custom_header), nameof(header));
|
||||
}
|
||||
}
|
||||
}
|
||||
public double width {
|
||||
get => _width;
|
||||
set {
|
||||
if (_set(ref _width, value, DoubleUtil.AreClose))
|
||||
width_changed?.Invoke(value);
|
||||
}
|
||||
}
|
||||
double _actual_width;
|
||||
public double actual_width {
|
||||
get => _actual_width;
|
||||
set {
|
||||
_actual_width = value;
|
||||
width = value;
|
||||
RaisePropertyChanged(nameof(actual_width));
|
||||
}
|
||||
}
|
||||
public bool is_visible { get => _is_visible; set => _set(ref _is_visible, value); }
|
||||
public ListSortDirection? sort_direction { get => _sort_direction; set => Set(ref _sort_direction, value); }
|
||||
public Color? background_color_override { get => _background_color_override; set => _set(ref _background_color_override, value); }
|
||||
public Color? font_color_override { get => _font_color_override; set => _set(ref _font_color_override, value); }
|
||||
public FontWeight? font_weight_override { get => _font_weight_override; set => _set(ref _font_weight_override, value); }
|
||||
public FontFamily font_family => _fallback_font_family;
|
||||
public FontWeight font_weight => _font_weight_override ?? _fallback_font_weight;
|
||||
public Color font_color => font_color_override ?? _fallback_font_color;
|
||||
public Color background_color => background_color_override ?? _fallback_background_color;
|
||||
public double font_size => _fallback_font_size;
|
||||
public HorizontalAlignment horizontal_alignment { get => _horizontal_alignment; set => _set(ref _horizontal_alignment, value); }
|
||||
public IDataGridColumnFilter filter { get; }
|
||||
public string column_template { get; }
|
||||
public string filter_template { get; }
|
||||
|
||||
string IDIYGridColumn.id => id.ToString();
|
||||
ListSortDirection? ISortableDIYGridColumn.SortDirection { get => sort_direction; set => sort_direction = value; }
|
||||
|
||||
public GridColumnSettings create() =>
|
||||
new GridColumnSettings(
|
||||
id: id.ToString(),
|
||||
display_name: display_name,
|
||||
is_visible: is_visible,
|
||||
font_color_override: font_color_override,
|
||||
font_weight_override: font_weight_override,
|
||||
width: width,
|
||||
background_color_override: background_color_override,
|
||||
horizontal_alignment: horizontal_alignment);
|
||||
|
||||
public void set_fallback_font_params(FontFamily font_family, double font_size, Color font_color, FontWeight font_weight, Color background_color) {
|
||||
using (ReflectionHelper.detect_property_changes(this, RaisePropertyChanged, nameof(font_family), nameof(font_size), nameof(font_color), nameof(font_weight), nameof(background_color))) {
|
||||
_fallback_font_family = font_family;
|
||||
_fallback_font_size = font_size;
|
||||
_fallback_font_color = font_color;
|
||||
_fallback_background_color = background_color;
|
||||
_fallback_font_weight = font_weight;
|
||||
}
|
||||
}
|
||||
|
||||
bool _set<T>(ref T backing, T new_value, [CallerMemberName]string prop = null) =>
|
||||
_set(ref backing, new_value, EqualityComparer<T>.Default, prop);
|
||||
|
||||
bool _set<T>(ref T backing, T new_value, Func<T, T, bool> equals, [CallerMemberName] string prop = null) =>
|
||||
// Technically, the GetHashCode impl could disagree with the equals delegate.
|
||||
// However, this doesn't matter because the _set method only uses IEqualityComparer.Equals (not GetHashCode).
|
||||
// It didn't seem worth the trouble of changing the method signature, as there is no conceivable reason why that method
|
||||
// would ever be changed to use GetHashCode.
|
||||
_set(ref backing, new_value, EqualityComparer.Create(equals, a => a.GetHashCode()), prop);
|
||||
|
||||
bool _set<T>(ref T backing, T new_value, IEqualityComparer<T> comparer, [CallerMemberName]string prop = null) {
|
||||
if (Set(ref backing, new_value, comparer, prop)) {
|
||||
_result_changed.execute();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public void load_from(GridColumnSettings value) {
|
||||
if (!value.strong_id<TKey>().Equals(id))
|
||||
throw new ArgumentException("Column id in settings object does not match the column model's id.", nameof(value));
|
||||
using (_result_changed.defer_execution()) {
|
||||
display_name = value.display_name;
|
||||
is_visible = value.is_visible;
|
||||
font_color_override = value.font_color_override;
|
||||
font_weight_override = value.font_weight_override;
|
||||
background_color_override = value.background_color_override;
|
||||
width = value.width;
|
||||
horizontal_alignment = value.horizontal_alignment;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class DIYGridColumnModel<TKey, TRow, TGridFilter> : DIYGridColumnModel<TKey> where TKey : struct {
|
||||
readonly Func<TGridFilter, object> _get_column_filter_settings;
|
||||
readonly Func<TGridFilter, object, TGridFilter> _set_column_filter_settings;
|
||||
|
||||
public DIYGridColumnModel(TKey id, IComparer<TRow> row_comparer, IDataGridColumnFilter<TRow> filter, string filter_template, Func<TGridFilter, object> get_column_filter_settings, Func<TGridFilter, object, TGridFilter> set_column_filter_settings)
|
||||
: base(id, row_comparer.to_non_generic(), filter, filter_template) {
|
||||
_get_column_filter_settings = get_column_filter_settings;
|
||||
_set_column_filter_settings = set_column_filter_settings;
|
||||
this.row_comparer = row_comparer;
|
||||
this.filter = filter;
|
||||
}
|
||||
|
||||
public new IComparer<TRow> row_comparer { get; }
|
||||
|
||||
public new IDataGridColumnFilter<TRow> filter { get; }
|
||||
|
||||
public void load_from(GridColumnSettings column_settings, TGridFilter grid_filter_settings) {
|
||||
if (column_settings != null)
|
||||
load_from(column_settings);
|
||||
if (_get_column_filter_settings != null && grid_filter_settings != null)
|
||||
filter?.load_state(_get_column_filter_settings(grid_filter_settings));
|
||||
}
|
||||
|
||||
public TGridFilter assign_filter_settings(TGridFilter target) {
|
||||
if (_set_column_filter_settings != null) {
|
||||
var filter_settings = filter.get_state();
|
||||
target = _set_column_filter_settings(target, filter_settings);
|
||||
}
|
||||
return target;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using bts;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.spark.filters;
|
||||
using mbts.ui.controls.adorners;
|
||||
@@ -867,6 +868,83 @@ namespace mbts.ui.controls {
|
||||
public static void SetColumnKey(DependencyObject d, object value) => d.SetValue(ColumnKeyProperty, value);
|
||||
#endregion
|
||||
|
||||
#region Column
|
||||
public static readonly DependencyProperty ColumnProperty =
|
||||
DependencyProperty.RegisterAttached("Column", typeof(object), typeof(DIYGrid), new PropertyMetadata());
|
||||
|
||||
public static object GetColumn(DependencyObject d) => d.GetValue(ColumnProperty);
|
||||
public static void SetColumn(DependencyObject d, object value) => d.SetValue(ColumnProperty, value);
|
||||
#endregion
|
||||
|
||||
#region ColumnModels
|
||||
public static readonly DependencyProperty ColumnModelsProperty =
|
||||
DependencyProperty.RegisterAttached("ColumnModels", typeof(IReadOnlyObservableCollection<IDIYGridColumn>),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata(null, OnColumnModelsChanged));
|
||||
|
||||
static void OnColumnModelsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
IDisposable hook = null;
|
||||
if (e.NewValue is IEnumerable<IDIYGridColumnModel> col_models) {
|
||||
resort();
|
||||
if (e.NewValue is INotifyCollectionChanged col_models_notifier) {
|
||||
hook = CollectionHelper.buffer_collection_changed_callback(col_models_notifier, resort, TimeSpan.FromSeconds(0.3), TimeSpan.Zero);
|
||||
}
|
||||
void resort() {
|
||||
if (d is DIYGrid grid) {
|
||||
var cols = grid.Columns;
|
||||
if (cols != null) {
|
||||
var col_map = cols.ToDictionary(c => c.id);
|
||||
var sorted_cols = col_models.Select(cm => {
|
||||
if (col_map.TryGetValue(cm.id, out var cb))
|
||||
return cb;
|
||||
if (cm is IDIYGridColumnFactory col_factory) {
|
||||
cb = col_factory.create_view();
|
||||
} else if (!string.IsNullOrEmpty(cm.column_template)) {
|
||||
cb = grid.TryFindResource(cm.column_template) as ColumnBase;
|
||||
}
|
||||
if (cb is null) {
|
||||
cb = new DIYGridColumn { id = cm.id };
|
||||
BindingOperations.SetBinding(cb, ColumnBase.HeaderProperty, new Binding(nameof(DIYGridColumnModel<int>.header)) { Source = cm });
|
||||
BindingOperations.SetBinding(cb, ColumnBase.SortDirectionProperty, new Binding(nameof(DIYGridColumnModel<int>.sort_direction)) { Source = cm });
|
||||
cb.set_model(cm);
|
||||
BindingOperations.SetBinding(cb, ColumnBase.IsVisibleProperty, new Binding(nameof(DIYGridColumnModel<int>.is_visible)) { Source = cm });
|
||||
if (cm is IFilterableColumn fc) {
|
||||
DataGridFilterProperties.SetFilter(cb, fc.filter);
|
||||
DataGridFilterProperties.SetFilterTemplate(cb, grid.FindResource(fc.filter_template) as DataTemplate);
|
||||
BindingOperations.SetBinding(cb, DataGridFilterProperties.FilterActiveProperty, new Binding(nameof(IDataGridColumnFilter.active)) { Source = fc.filter });
|
||||
BindingOperations.SetBinding(cb, DataGridFilterProperties.FilterOpenProperty, new Binding(nameof(IDataGridColumnFilter.open)) { Source = fc.filter });
|
||||
}
|
||||
}
|
||||
return cb;
|
||||
}).ToList();
|
||||
CollectionHelper.sync_with_never_duplicate(cols, sorted_cols);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
SetColumnModelsHooks(d, hook);
|
||||
}
|
||||
|
||||
public static IReadOnlyObservableCollection<IDIYGridColumn> GetColumnModels(DependencyObject d) =>
|
||||
d.GetValue(ColumnModelsProperty) as IReadOnlyObservableCollection<IDIYGridColumn>;
|
||||
|
||||
public static void SetColumnModels(DependencyObject d, IReadOnlyObservableCollection<IDIYGridColumn> value) =>
|
||||
d.SetValue(ColumnModelsProperty, value);
|
||||
|
||||
static readonly DependencyPropertyKey ColumnModelsHooksPropertyKey =
|
||||
DependencyProperty.RegisterAttachedReadOnly("ColumnModelsHooks", typeof(IDisposable),
|
||||
typeof(DIYGrid),
|
||||
new PropertyMetadata(null, FrameworkElementLifeDependencyHelper.DisposeOld));
|
||||
|
||||
internal static DependencyProperty ColumnModelsHooksProperty => ColumnModelsHooksPropertyKey.DependencyProperty;
|
||||
|
||||
internal static IDisposable GetColumnModelsHooks(DependencyObject d) =>
|
||||
d.GetValue(ColumnModelsHooksProperty) as IDisposable;
|
||||
|
||||
static void SetColumnModelsHooks(DependencyObject d, IDisposable value) =>
|
||||
d.SetValue(ColumnModelsHooksPropertyKey, value);
|
||||
#endregion
|
||||
|
||||
#region SyncWithColumnGeometrySource
|
||||
public static readonly DependencyProperty SyncWithColumnGeometrySourceProperty =
|
||||
DependencyProperty.RegisterAttached(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
@@ -121,4 +122,111 @@ namespace mbts.ui {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SimpleGridSettingsManagerVM<TColumnKey> : ViewModelBase, IObserver<GridSettings> where TColumnKey : struct {
|
||||
readonly IReadOnlyList<DIYGridColumnModel<TColumnKey>> _columns;
|
||||
readonly Property<GridSettings> _grid_settings;
|
||||
readonly DelegateProperty<FontFamily> _font_family;
|
||||
readonly DelegateProperty<FontWeight> _font_weight;
|
||||
readonly DelegateProperty<double> _font_size;
|
||||
readonly DelegateProperty<SolidColorBrush> _font_color, _selection_highlight, _background_color;
|
||||
readonly IReadOnlyDictionary<string, ImmutableColumnSettingStore> _column_flags;
|
||||
readonly Action<IEnumerable<GridColumnSettings>> _sync_column_order;
|
||||
readonly Action _invalidate_rows;
|
||||
|
||||
public SimpleGridSettingsManagerVM(
|
||||
IReadOnlyList<DIYGridColumnModel<TColumnKey>> columns,
|
||||
IReadOnlyDictionary<string, GridColumnSettingsFeatureFlags> column_flags,
|
||||
Action<IEnumerable<GridColumnSettings>> sync_column_order,
|
||||
Action invalidate_rows) {
|
||||
//
|
||||
_columns = columns;
|
||||
_sync_column_order = sync_column_order;
|
||||
_invalidate_rows = invalidate_rows;
|
||||
_column_flags = (column_flags ?? DictionaryHelper.empty<string, GridColumnSettingsFeatureFlags>())
|
||||
.ToDictionary(kv => kv.Key, kv => ImmutableColumnSettingStore.create(kv.Value));
|
||||
_grid_settings = backing(nameof(grid_settings), GridSettings.Default);
|
||||
_font_family = backing(nameof(font_family), () => new FontFamily(grid_settings.font_family), nameof(grid_settings));
|
||||
_font_weight = backing(nameof(font_weight), () => grid_settings.font_weight, nameof(grid_settings));
|
||||
_font_size = backing(nameof(font_size), () => grid_settings.font_size, nameof(font_size));
|
||||
_font_color = backing(nameof(font_color), () => grid_settings.font_color.rgb_solid_brush().frozen(), nameof(grid_settings));
|
||||
_selection_highlight = backing(nameof(selection_highlight), () => grid_settings.selection_highlight.to_brush().frozen(), nameof(grid_settings));
|
||||
_background_color = backing(nameof(background_color), () => grid_settings.background_color.argb_color().to_solid_brush().frozen(), nameof(grid_settings));
|
||||
}
|
||||
|
||||
public GridSettings grid_settings => _grid_settings.Value;
|
||||
public FontFamily font_family => _font_family.Value;
|
||||
public FontWeight font_weight => _font_weight.Value;
|
||||
public double font_size => _font_size.Value;
|
||||
public SolidColorBrush font_color => _font_color.Value;
|
||||
public SolidColorBrush selection_highlight => _selection_highlight.Value;
|
||||
public SolidColorBrush background_color => _background_color.Value;
|
||||
|
||||
public SimpleGridSettingsVM create_vm(GridSettings settings) {
|
||||
var vm = new SimpleGridSettingsVM(_column_flags.ToDictionary(kv => kv.Key, kv => kv.Value.flags));
|
||||
vm.load_from(settings);
|
||||
return vm;
|
||||
}
|
||||
|
||||
public GridSettings create() {
|
||||
// IMPORTANT: don't overwrite current sorting
|
||||
_grid_settings.set(grid_settings.with(_columns.Select(c => {
|
||||
var col_settings = c.create();
|
||||
if (_column_flags.TryGetValue(c.id.ToString(), out ImmutableColumnSettingStore store))
|
||||
col_settings = store.apply(col_settings);
|
||||
return col_settings;
|
||||
})));
|
||||
return grid_settings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This will overwrite the columns' immutable-value store (unlike the IObserver<<see cref="GridSettings"/>> 'OnNext' implementation)
|
||||
/// </summary>
|
||||
public void load_from(GridSettings grid) => _load_from_core(grid, true);
|
||||
|
||||
void IObserver<GridSettings>.OnCompleted() { }
|
||||
void IObserver<GridSettings>.OnError(Exception error) { }
|
||||
void IObserver<GridSettings>.OnNext(GridSettings grid) {
|
||||
if (grid.columns != null) {
|
||||
var cols_with_applied_flags = grid.columns.ToList();
|
||||
for (int i = 0; i < cols_with_applied_flags.Count; i++) {
|
||||
if (_column_flags.TryGetValue(cols_with_applied_flags[i].id, out ImmutableColumnSettingStore store)) {
|
||||
cols_with_applied_flags[i] = store.apply(cols_with_applied_flags[i]);
|
||||
}
|
||||
}
|
||||
grid = grid.with(cols_with_applied_flags);
|
||||
}
|
||||
_load_from_core(grid, false);
|
||||
}
|
||||
|
||||
void _load_from_core(GridSettings grid, bool reset_immutable_flag_stores) {
|
||||
grid = grid ?? GridSettings.Default;
|
||||
_grid_settings.set(grid);
|
||||
foreach (var col in _columns) {
|
||||
col.set_fallback_font_params(new FontFamily(grid.font_family), grid.font_size, grid.font_color.rgb_color(), grid.font_weight, grid.background_color.argb_color());
|
||||
}
|
||||
// ** in the unfortunate event that we botch backwards compatability, and the columns don't bear a 1:1 relationship with the column-settings
|
||||
// don't add insult to injury by crashing **
|
||||
if (grid_settings.columns != null) {
|
||||
if (reset_immutable_flag_stores) {
|
||||
foreach (var kv in _column_flags) {
|
||||
if (grid.columns.TryGetFirst(c => c.id == kv.Key, out GridColumnSettings cs))
|
||||
kv.Value.store(cs);
|
||||
}
|
||||
}
|
||||
if (_columns != null) {
|
||||
var col_settings_id_map = grid_settings.columns.ToDictionary(cs => cs.id);
|
||||
var columns_id_map = _columns.ToDictionary(c => c.id.ToString());
|
||||
if (col_settings_id_map.Count == columns_id_map.Count && col_settings_id_map.Count == grid_settings.columns.Count)
|
||||
_sync_column_order?.Invoke(grid_settings.columns);
|
||||
// ** don't assume that columns:column_settings have a perfect 1:1 relationship (they should, but it is possible we accidentally break the settings as grids/uis change) **
|
||||
foreach (var col in _columns) {
|
||||
if (col_settings_id_map.TryGetValue(col.id.ToString(), out GridColumnSettings cs))
|
||||
col.load_from(cs);
|
||||
}
|
||||
}
|
||||
_invalidate_rows?.Invoke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -60,6 +60,7 @@ namespace mbts.spark.filters {
|
||||
TimeFilter = "TimeFilterTemplate",
|
||||
DiscreteValues = "DiscreteValuesTemplate",
|
||||
DiscreteValuesCollectionView = "DiscreteValuesCollectionViewTemplate",
|
||||
SecurityCriteriaCriteria = "SecurityCriteriaCriteria",
|
||||
EnumValues = "EnumValuesTemplate",
|
||||
Regex = "RegexTemplate",
|
||||
YesNo = "YesNoTemplate";
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace mbts.spark.filters {
|
||||
ICommand cancel { get; }
|
||||
bool filter(object row);
|
||||
event Action filter_changed;
|
||||
void load_state(object state);
|
||||
object get_state();
|
||||
}
|
||||
|
||||
interface IDataGridColumnFilter<in TRow> : IDataGridColumnFilter {
|
||||
@@ -20,6 +22,6 @@ namespace mbts.spark.filters {
|
||||
|
||||
interface IDataGridColumnPersistentFilter<in TRow, TState> : IDataGridColumnFilter<TRow> {
|
||||
void load_state(TState state);
|
||||
TState get_state();
|
||||
new TState get_state();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
@@ -53,6 +54,15 @@ namespace mbts.spark.filters {
|
||||
_raise_filter_changed();
|
||||
}
|
||||
|
||||
object IDataGridColumnFilter.get_state() =>
|
||||
get_state();
|
||||
|
||||
void IDataGridColumnFilter.load_state(object state) {
|
||||
if (state is TState fstate)
|
||||
load_state(fstate);
|
||||
else if (state != null) throw ExceptionBuilder.Argument.not_of_type<TState>(nameof(state), state?.GetType());
|
||||
}
|
||||
|
||||
protected abstract bool _get_active();
|
||||
protected abstract bool _filter(TValue value);
|
||||
protected abstract void _load_state_internal(TState state);
|
||||
@@ -111,4 +121,4 @@ namespace mbts.spark.filters {
|
||||
bool IDataGridColumnFilter.filter(object row) =>
|
||||
filter(ConvertUtils.try_convert<TRow>(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
using bts.api;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts.spark.filters {
|
||||
class SecurityCriteriaFilter<TRow> : FilterVMBase<TRow, SecurityImmutable, SecurityCriteria>, IDisposable {
|
||||
@@ -43,4 +45,50 @@ namespace mbts.spark.filters {
|
||||
void _set_default_values() =>
|
||||
_security_criteria = model.result_criteria;
|
||||
}
|
||||
}
|
||||
|
||||
class SecurityCriteriaCriteriaFilter<TRow> : FilterVMBase<TRow, SecurityCriteria, SecurityCriteria>, IDisposable {
|
||||
SecurityCriteria _security_criteria;
|
||||
|
||||
public SecurityCriteriaCriteriaFilter(Func<TRow, SecurityCriteria> get_row_val, string prop_name)
|
||||
: base(get_row_val, prop_name) { }
|
||||
|
||||
public ContractPickerModel model { get; }
|
||||
|
||||
public void Dispose() =>
|
||||
model.security_criteria_clause_changed -= _on_security_criteria_clause_changed;
|
||||
|
||||
public override SecurityCriteria get_state() =>
|
||||
_security_criteria;
|
||||
|
||||
protected override bool _filter(SecurityCriteria value) {
|
||||
if ((_security_criteria?.clauses).is_null_or_empty())
|
||||
return true;
|
||||
if (!_security_criteria.is_defined())
|
||||
return true;
|
||||
if ((value?.clauses).is_null_or_empty())
|
||||
return false;
|
||||
var result = value.clauses.Any(c => c.is_defined && _security_criteria.clauses.Any(fclause => c.is_subset_of(fclause)));
|
||||
return result;
|
||||
}
|
||||
|
||||
protected override void _load_state_internal(SecurityCriteria state) =>
|
||||
_security_criteria = state;
|
||||
|
||||
protected override void _clear() {
|
||||
_close();
|
||||
using (_clear_properties())
|
||||
_set_default_values();
|
||||
}
|
||||
|
||||
protected override bool _get_active() =>
|
||||
_security_criteria != null && _security_criteria.clauses.Length > 0;
|
||||
|
||||
void _on_security_criteria_clause_changed(object sender, SecurityCriteriaClauseChangedEventsArgs e) {
|
||||
_security_criteria = model.result_criteria;
|
||||
_raise_filter_changed();
|
||||
}
|
||||
|
||||
void _set_default_values() =>
|
||||
_security_criteria = model.result_criteria;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using bts;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
||||
@@ -60,16 +61,24 @@ namespace mbts.spark.filters {
|
||||
public TCompoundState get_state() =>
|
||||
_get_state(_filter1.get_state(), _filter2.get_state());
|
||||
|
||||
object IDataGridColumnFilter.get_state() =>
|
||||
get_state();
|
||||
|
||||
public void load_state(TCompoundState state) {
|
||||
_filter1.load_state(_get_filter1_state(state));
|
||||
_filter2.load_state(_get_filter2_state(state));
|
||||
}
|
||||
|
||||
void IDataGridColumnFilter.load_state(object state) {
|
||||
if (state is TCompoundState cstate)
|
||||
load_state(cstate);
|
||||
else throw ExceptionBuilder.Argument.not_of_type<TCompoundState>(nameof(state), state?.GetType());
|
||||
}
|
||||
|
||||
protected void _raise_request_update() =>
|
||||
RequestUpdate?.Invoke();
|
||||
|
||||
|
||||
bool IDataGridColumnFilter.filter(object row) =>
|
||||
filter(ConvertUtils.try_convert<TRow>(row));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -165,6 +165,18 @@
|
||||
/>
|
||||
</usp:FilterContentControl>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="{x:Static filters:DataGridFilterKnownTemplates.SecurityCriteriaCriteria}">
|
||||
<usp:FilterContentControl
|
||||
ApplyCommandParameter="{Binding}"
|
||||
ApplyCommand="{x:Static c:DataGridFilterProperties.CommitCommand}"
|
||||
ClearCommandParameter="{Binding}"
|
||||
ClearCommand="{x:Static c:DataGridFilterProperties.ClearCommand}"
|
||||
>
|
||||
<msp:ContractPickerView DataContext="{Binding model}">
|
||||
|
||||
</msp:ContractPickerView>
|
||||
</usp:FilterContentControl>
|
||||
</DataTemplate>
|
||||
<DataTemplate x:Key="AlgoTypeTemplate">
|
||||
<usp:FilterContentControl
|
||||
ApplyCommandParameter="{Binding}"
|
||||
|
||||
@@ -5,6 +5,8 @@ using System.Windows.Media;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
class ColorToBrushConverter : IValueConverter {
|
||||
public static ColorToBrushConverter Instance => new ColorToBrushConverter();
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (value is Color color) return new SolidColorBrush(color);
|
||||
if (value is BTSColor bts_color) return new SolidColorBrush(bts_color);
|
||||
|
||||
@@ -9,19 +9,13 @@ using System.Reactive.Linq;
|
||||
namespace mbts.spark {
|
||||
[ProtoContract]
|
||||
class SecurityCriteria : IEquatable<SecurityCriteria> {
|
||||
public SecurityCriteria()
|
||||
{
|
||||
public SecurityCriteria() {
|
||||
clauses = new SecurityCriteriaClause[] { };
|
||||
}
|
||||
|
||||
public SecurityCriteria(IEnumerable<SecurityCriteriaClause> c)
|
||||
{
|
||||
public SecurityCriteria(IEnumerable<SecurityCriteriaClause> c) {
|
||||
clauses = c.ToArray();
|
||||
}
|
||||
internal SecurityCriteria(IEnumerable<SecurityImmutable> securities) {
|
||||
clauses = new[] { new SecurityCriteriaClause(securities) };
|
||||
}
|
||||
internal SecurityCriteria(params SecurityImmutable[] securities) : this(securities.AsEnumerable()) { }
|
||||
|
||||
[ProtoMember(1)]
|
||||
public SecurityCriteriaClause[] clauses;
|
||||
@@ -31,8 +25,11 @@ namespace mbts.spark {
|
||||
new SecurityCriteriaClause (security_keys: keys.ToArray())
|
||||
});
|
||||
|
||||
internal static SecurityCriteria from_securities(ISet<SecurityImmutable> securities) =>
|
||||
new SecurityCriteria(securities ?? Enumerable.Empty<SecurityImmutable>());
|
||||
internal static SecurityCriteria from_securities(params SecurityImmutable[] securities) =>
|
||||
from_securities(securities.AsEnumerable());
|
||||
|
||||
internal static SecurityCriteria from_securities(IEnumerable<SecurityImmutable> securities) =>
|
||||
new SecurityCriteria(new[] { SecurityCriteriaClause.from_securities(securities) });
|
||||
|
||||
internal static Func<SecurityCriteria, IObservable<IEnumerable<SecurityImmutable>>> get_to_securities(IBus bus) =>
|
||||
new Func<SecurityCriteria, IObservable<IEnumerable<SecurityImmutable>>>(sc => to_securities(sc, bus));
|
||||
@@ -61,8 +58,27 @@ namespace mbts.spark {
|
||||
this.pretty_name = pretty_name;
|
||||
}
|
||||
|
||||
public SecurityCriteriaClause(IEnumerable<SecurityImmutable> securities)
|
||||
: this(security_keys: securities.Select(s => s.Id).ToArray()) { }
|
||||
public static SecurityCriteriaClause from_securities(IEnumerable<SecurityImmutable> securities) {
|
||||
var num_secs = securities.SoftCount();
|
||||
uint[] keys;
|
||||
string pretty_name = null;
|
||||
if (num_secs.HasValue) {
|
||||
keys = new uint[num_secs.Value];
|
||||
int i = 0;
|
||||
if (keys.Length == 1) {
|
||||
foreach (var s in securities) {
|
||||
keys[i++] = s.Id;
|
||||
pretty_name = s.ToString();
|
||||
}
|
||||
} else {
|
||||
foreach (var s in securities)
|
||||
keys[i++] = s.Id;
|
||||
}
|
||||
} else {
|
||||
keys = securities.Select(s => s.Id).ToArray();
|
||||
}
|
||||
return new SecurityCriteriaClause(security_keys: keys, pretty_name: pretty_name);
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public readonly string pretty_name;
|
||||
|
||||
@@ -24,6 +24,12 @@ namespace mbts.spark {
|
||||
return self == null ? all_secs : all_secs.Values.Where(s => ContractPickerModel.security_in_criteria(self, s)).ToDictionary(key => key.Id);
|
||||
}
|
||||
|
||||
public static async Task<IReadOnlyDictionary<uint,SecurityImmutable>> securities(this SecurityCriteria self, IBus bus, TimeSpan timeout) {
|
||||
var md_map = await bus.subscribe(SparkTags.SecuritiesTag).FirstOrDefaultAsync(timeout, DictionaryHelper.empty<uint, SecurityMD>());
|
||||
var all_secs = md_map.Values.Select(md => md.security).ToDictionary(key => key.Id);
|
||||
return self == null ? all_secs : all_secs.Values.Where(s => ContractPickerModel.security_in_criteria(self, s)).ToDictionary(key => key.Id);
|
||||
}
|
||||
|
||||
public static IObservable<IEnumerable<SecurityImmutable>> subscribe(this SecurityCriteria self, IBus bus) {
|
||||
var md_source = bus.subscribe(SparkTags.SecuritiesTag);
|
||||
return self == null ? md_source.Select(kv => kv.Values.Select(smd => smd.security)) :
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace mbts.ui {
|
||||
interface IValueChangedEventArgs<out T> {
|
||||
T value { get; }
|
||||
}
|
||||
|
||||
static class ValueChangedEventArgs {
|
||||
public static IValueChangedEventArgs<T> create<T>(Func<T> get_value) =>
|
||||
new LazyImpl<T>(new Lazy<T>(get_value));
|
||||
|
||||
class LazyImpl<T> : IValueChangedEventArgs<T> {
|
||||
readonly Lazy<T> _value;
|
||||
public LazyImpl(Lazy<T> value) =>
|
||||
_value = value;
|
||||
|
||||
public T value => _value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -260,13 +260,13 @@ namespace mbts.co2 {
|
||||
}
|
||||
|
||||
// If Size is wrong remember to update from
|
||||
[StructLayout(LayoutKind.Sequential, Size = 704)]
|
||||
[StructLayout(LayoutKind.Sequential, Size = 712)]
|
||||
internal struct ColdEntry {
|
||||
public int exchange_status;
|
||||
// Last trade
|
||||
public AtomicPriceSize last_trade;
|
||||
public ulong total_volume;
|
||||
public utils.LLDateTime last_trade_time;
|
||||
public utils.LLDateTime last_trade_time;
|
||||
|
||||
//// Settlement
|
||||
public UInt64 misc_timestamp;
|
||||
|
||||
@@ -24,7 +24,7 @@ static class ExchangeSpecificUitlities{
|
||||
}
|
||||
|
||||
public static bool is_ice_lots(SecurityImmutable sec){
|
||||
return sec.Ice.MarketDescription.Contains("ICE Lots");
|
||||
return sec.Ice.IsIceLots;
|
||||
}
|
||||
|
||||
public static bool is_tas(SecurityImmutable sec){
|
||||
@@ -33,25 +33,36 @@ static class ExchangeSpecificUitlities{
|
||||
|
||||
#endregion
|
||||
|
||||
#region CPP
|
||||
#region CPP
|
||||
public static double get_future_cpp (SecurityImmutable sec){
|
||||
if(sec.Symbol == "HHD")
|
||||
// only futures in HHD are the dailies, everything
|
||||
// else is a strip or spread
|
||||
return 1;
|
||||
// TODO, this should be part of the security
|
||||
if(sec.Symbol == "HHD")
|
||||
return 2500;
|
||||
if(is_ice_lots(sec))
|
||||
return 2500;
|
||||
else{
|
||||
var days_in_month = DateTime.DaysInMonth(Convert.ToInt32(sec.Future.ExpirationYear),
|
||||
Convert.ToInt32(sec.Future.ExpirationMonth));
|
||||
return days_in_month; // because the size is multiplied by 2500 already
|
||||
// other futures - HNG, HHD, HEN - are like daily
|
||||
// strips
|
||||
UInt32 days_in_month = 31;
|
||||
var mm = sec.Future.ExpirationMonth;
|
||||
if(mm <=11)
|
||||
days_in_month = Convert.ToUInt32(
|
||||
DateTime.DaysInMonth(
|
||||
Convert.ToInt32(sec.Future.ExpirationYear),
|
||||
// expects 1-12
|
||||
Convert.ToInt32(sec.Future.ExpirationMonth+1)));
|
||||
else
|
||||
// TODO : sometimes Jan is 12 !
|
||||
days_in_month = 31;
|
||||
return days_in_month * 2500;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static double get_spread_cpp(SecurityImmutable sec){
|
||||
// for purpose of pnl we are 0'ing out the spread pnl
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
public static double get_strip_cpp(PNLUnitImmutable pnl,
|
||||
SecurityImmutable sec){
|
||||
// add up all the legs
|
||||
|
||||
@@ -77,16 +77,17 @@ namespace bts.utils {
|
||||
|
||||
public static int auto_phase(params int[] hash_codes) =>
|
||||
auto_phase(Array.AsReadOnly(hash_codes));
|
||||
|
||||
|
||||
public static int auto_phase<T>(IEnumerable<T> items) =>
|
||||
auto_phase(items.Take(_max_auto_phase_buckets).ToList());
|
||||
|
||||
public static int auto_phase<T>(IReadOnlyList<T> hash_codes) {
|
||||
public static int auto_phase<T>(IReadOnlyList<T> hash_codes, int bit_span = 32) {
|
||||
Log.assert(bit_span > 0 && bit_span <= 32);
|
||||
// don't divide by zero
|
||||
if (hash_codes.Count == 0)
|
||||
return 0;
|
||||
int size = Math.Min(hash_codes.Count, _max_auto_phase_buckets);
|
||||
int bits_per_hash = 32 / size;
|
||||
int bits_per_hash = bit_span / size;
|
||||
int phase = 1 << (bits_per_hash - 1);
|
||||
int rv = hash_codes[0].GetHashCode() % phase;
|
||||
/// The idea is to split the 32 to bits into evenly sized buckets for each hash code item.
|
||||
|
||||
@@ -252,6 +252,17 @@ namespace System.Collections.Generic {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetFirstIndex<T>(this IEnumerable<T> source, Func<T, bool> predicate, out int index) {
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
index = 0;
|
||||
foreach (T element in source) {
|
||||
if (predicate(element))
|
||||
return true;
|
||||
index++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool TryGetSingle<T>(this IEnumerable<T> source, out T result) {
|
||||
if (source == null) throw new ArgumentNullException(nameof(source));
|
||||
using (var en = source.GetEnumerator()) {
|
||||
@@ -716,4 +727,4 @@ namespace System.Collections.Generic {
|
||||
IEnumerator IEnumerable.GetEnumerator() => _list.GetEnumerator();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using System;
|
||||
|
||||
namespace bts.collections {
|
||||
interface IReadOnlyIndex<TKey, TValue> {
|
||||
TValue this[TKey key] { get; }
|
||||
}
|
||||
|
||||
static class FunctionalIndex {
|
||||
public static FunctionalIndex<TKey, TValue> create<TKey, TValue>(Func<TKey, TValue> get_value) =>
|
||||
new FunctionalIndex<TKey, TValue>(get_value);
|
||||
}
|
||||
|
||||
class FunctionalIndex<TKey, TValue> : IReadOnlyIndex<TKey, TValue> {
|
||||
readonly Func<TKey, TValue> _get_value;
|
||||
|
||||
public FunctionalIndex(Func<TKey, TValue> get_value) =>
|
||||
_get_value = get_value ?? throw new ArgumentNullException(nameof(get_value));
|
||||
|
||||
public TValue this[TKey key] => _get_value(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
using mbts;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.utils {
|
||||
public static class CallbackBuffer {
|
||||
public static IDisposable buffer_callback(Func<Action, IDisposable> attach_callback, Action callback, TimeSpan delay_before_callback, TimeSpan delay_after_callback) {
|
||||
bool is_disposed = false, is_busy = false, dirty = false;
|
||||
object locker = new object();
|
||||
var detach = attach_callback(callback_buffer_impl);
|
||||
return Disposable.Create(() => {
|
||||
lock (locker) {
|
||||
if (is_disposed)
|
||||
return;
|
||||
is_disposed = true;
|
||||
dirty = false;
|
||||
}
|
||||
Disposable.dispose(ref detach);
|
||||
});
|
||||
async void callback_buffer_impl() {
|
||||
lock (locker) {
|
||||
if (is_busy) {
|
||||
dirty = true;
|
||||
return;
|
||||
}
|
||||
is_busy = true;
|
||||
}
|
||||
while (true) {
|
||||
if (delay_before_callback > TimeSpan.Zero) {
|
||||
await Task.Delay(delay_before_callback);
|
||||
}
|
||||
lock (locker) {
|
||||
if (is_disposed) {
|
||||
is_busy = false;
|
||||
dirty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
if (delay_after_callback > TimeSpan.Zero) {
|
||||
await Task.Delay(delay_after_callback);
|
||||
}
|
||||
lock (locker) {
|
||||
if (!dirty) {
|
||||
is_busy = false;
|
||||
break;
|
||||
}
|
||||
dirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IDisposable buffer_async_callback(Func<Action, IDisposable> attach_callback, Func<Task> async_callback) {
|
||||
bool is_disposed = false, is_busy = false, dirty = false;
|
||||
object locker = new object();
|
||||
var detach = attach_callback(callback_buffer_impl);
|
||||
return Disposable.Create(() => {
|
||||
lock (locker) {
|
||||
if (is_disposed)
|
||||
return;
|
||||
is_disposed = true;
|
||||
dirty = false;
|
||||
}
|
||||
Disposable.dispose(ref detach);
|
||||
});
|
||||
async void callback_buffer_impl() {
|
||||
lock (locker) {
|
||||
if (is_busy) {
|
||||
dirty = true;
|
||||
return;
|
||||
}
|
||||
is_busy = true;
|
||||
}
|
||||
while (true) {
|
||||
await async_callback();
|
||||
lock (locker) {
|
||||
if (!dirty || is_disposed) {
|
||||
is_busy = false;
|
||||
break;
|
||||
}
|
||||
dirty = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
using System;
|
||||
using mbts;
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.utils {
|
||||
static class CollectionHelper {
|
||||
@@ -77,6 +79,30 @@ namespace bts.utils {
|
||||
return ~hi;
|
||||
}
|
||||
|
||||
public static IDisposable buffer_collection_changed_callback(INotifyCollectionChanged source, Action callback, TimeSpan delay_before_callback, TimeSpan delay_after_callback) {
|
||||
// return CallbackBuffer.buffer_callback(attach_callback, callback, delay_before_callback, delay_after_callback);
|
||||
// IDisposable attach_callback(Action cb) {
|
||||
// source.CollectionChanged += on_collection_changed;
|
||||
// return Disposable.Create(() => source.CollectionChanged -= on_collection_changed);
|
||||
// void on_collection_changed(object sender, NotifyCollectionChangedEventArgs e) =>
|
||||
// cb();
|
||||
// }
|
||||
return CallbackBuffer.buffer_async_callback(attach_callback, impl);
|
||||
IDisposable attach_callback(Action cb) {
|
||||
source.CollectionChanged += on_collection_changed;
|
||||
return Disposable.Create(() => source.CollectionChanged -= on_collection_changed);
|
||||
void on_collection_changed(object sender, NotifyCollectionChangedEventArgs e) =>
|
||||
cb();
|
||||
}
|
||||
async Task impl() {
|
||||
if (delay_before_callback > TimeSpan.Zero)
|
||||
await Task.Delay(delay_before_callback);
|
||||
callback();
|
||||
if (delay_after_callback > TimeSpan.Zero)
|
||||
await Task.Delay(delay_after_callback);
|
||||
}
|
||||
}
|
||||
|
||||
public static void reflect_change<T, TData>(IList<T> target, IEnumerable<TData> source, Func<TData, T> create_item, Action<T> on_removed, NotifyCollectionChangedEventArgs e) {
|
||||
switch (e.Action) {
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
|
||||
@@ -9,11 +9,16 @@ namespace mbts.utils {
|
||||
public static ArgumentException unexpected_case<T>(T value, string name) =>
|
||||
new ArgumentException($"Unexpected {typeof(T)} '{value}.'", name);
|
||||
|
||||
public static ArgumentException not_of_type<T>(object value, string name) =>
|
||||
not_of_type(value, name, typeof(T));
|
||||
public static ArgumentException not_of_type<T>(string name, Type actual = null) =>
|
||||
actual is null ?
|
||||
not_of_type(name, typeof(T)) :
|
||||
not_of_type(name, typeof(T), actual);
|
||||
|
||||
public static ArgumentException not_of_type(object value, string name, Type t) =>
|
||||
new ArgumentException($"Value is not of the expected type '{t}.'");
|
||||
public static ArgumentException not_of_type(string name, Type t) =>
|
||||
new ArgumentException(Format.NotOfType(t), name);
|
||||
|
||||
public static ArgumentException not_of_type(string name, Type expected, Type actual) =>
|
||||
new ArgumentException(Format.NotOfType(expected, actual), name);
|
||||
|
||||
public static ArgumentException null_or_empty(string name) =>
|
||||
new ArgumentException("Value is null or empty.", name);
|
||||
@@ -49,6 +54,8 @@ namespace mbts.utils {
|
||||
public static string LessThanOrEqualTo(int value) => $"{Messages.LessThanOrEqualTo} {value}.";
|
||||
public static string GreaterThan(int value) => $"{Messages.GreaterThan} {value}.";
|
||||
public static string GreaterThanOrEqualTo(int value) => $"{Messages.GreaterThanOrEqualTo} {value}.";
|
||||
public static string NotOfType(Type type) => $"Argument is not of expected type '{type}.'";
|
||||
public static string NotOfType(Type expected, Type actual) => $"Argument is not of expected type '{expected}' (actually '{actual}').";
|
||||
}
|
||||
|
||||
public static class Messages {
|
||||
@@ -59,4 +66,4 @@ namespace mbts.utils {
|
||||
GreaterThanOrEqualTo = "Greater than or equal to";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,230 +1,238 @@
|
||||
using mbts;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System.Reactive.Linq {
|
||||
public static class ObservableExtensions {
|
||||
/// <summary>
|
||||
/// <para>Creates an "Observable Heartbeat" that will publish true/false to indicate whether or not the heartbeat is detected, it will only send updates when the 'heartbeat status' has changed.</para>
|
||||
/// <para>Whenever the source produces an update, the heartbeat evaluates to true and the heartbeat duration clock is reset.</para>
|
||||
/// <para>Whenever heartbeat duration clock expires, the heartbeat evaluates to false.</para>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of data produced by the source (bears no impact on the resultant observable heartbeat</typeparam>
|
||||
/// <param name="source">The source that will indicate alive/dead by publishing updates within the duration period</param>
|
||||
/// <param name="duration">The amount of time that must pass without receiving an update from the source to determine the heartbeat is not detected</param>
|
||||
/// <returns>An "Observable Heartbeat"</returns>
|
||||
public static IObservable<bool> AsHeartbeat<T>(this IObservable<T> source, TimeSpan duration) {
|
||||
IObservable<bool> dead = source.Throttle(duration).Select(_ => false);
|
||||
IObservable<bool> alive = source.Select(_ => true);
|
||||
return dead.Merge(alive).DistinctUntilChanged();
|
||||
}
|
||||
|
||||
public static IObservable<T> BufferLatest<T>(this IObservable<T> source, TimeSpan throttle) =>
|
||||
source.Buffer(throttle).Where(vals => vals.Count > 0).Select(vals => vals[vals.Count - 1]);
|
||||
|
||||
public static IObservable<(T1, T2)> CombineLatest<T1, T2>(this IObservable<T1> source, IObservable<T2> second) =>
|
||||
source.CombineLatest(second, (v1, v2) => (v1, v2));
|
||||
|
||||
public static async Task<(T result, bool success)> FirstAsyncTask<T>(this IObservable<T> source, TimeSpan timeout) where T : class =>
|
||||
await source.Select(val => (val, true)).Merge(Observable.Return((default(T), false)).Delay(timeout)).FirstAsync();
|
||||
|
||||
public static async Task<(T result, bool success)> FirstAsyncTask<T>(this IObservable<T> source, Func<T, bool> predicate, TimeSpan timeout) where T : class =>
|
||||
await source.Where(predicate).Select(val => (val, true)).Merge(Observable.Return((default(T), false)).Delay(timeout)).FirstAsync();
|
||||
|
||||
public static async Task<(TResult result, bool success)> FirstAsyncTask<T, TResult>(this IObservable<T> source, Func<T, bool> predicate, Func<T, TResult> selector, TimeSpan timeout) where T : class =>
|
||||
await source.Where(predicate).Select(val => (selector(val), true)).Merge(Observable.Return((default(TResult), false)).Delay(timeout)).FirstAsync();
|
||||
|
||||
/// <summary>
|
||||
/// A less verbose way to call <see cref="Observable.FromEvent{Action{TEventArgs}, TEventArgs}(Action{Action{TEventArgs}}, Action{Action{TEventArgs}})"/>.
|
||||
/// </summary>
|
||||
public static IObservable<T> FromAction<T>(Action<T> event_source) =>
|
||||
Observable.FromEvent<Action<T>, T>(cb => event_source += cb, cb => event_source -= cb);
|
||||
|
||||
public static IObservable<T> FromEvent<T>(Action<Action> add_handler, Action<Action> remove_handler, Func<T> selector) =>
|
||||
Observable.Create<T>(o => {
|
||||
callback();
|
||||
add_handler(callback);
|
||||
return Disposable.Create(() => remove_handler(callback));
|
||||
void callback() => o.OnNext(selector());
|
||||
});
|
||||
|
||||
public static IObservable<T> FromEventThrottle<T>(Action<Action<T>> add_handler, Action<Action<T>> remove_handler, TimeSpan throttle) =>
|
||||
FromEventThrottle<T>(cb => {
|
||||
add_handler(cb);
|
||||
return Disposable.Create(() => remove_handler(cb));
|
||||
}, throttle);
|
||||
|
||||
public static IObservable<T> FromEventThrottle<T>(Func<Action<T>, IDisposable> add_handler, TimeSpan throttle) =>
|
||||
Observable.Create<T>(o => {
|
||||
T last_value = default;
|
||||
bool busy = false, dirty = false;
|
||||
return add_handler(callback);
|
||||
async void callback(T value) {
|
||||
last_value = value;
|
||||
if (busy) {
|
||||
dirty = true;
|
||||
} else {
|
||||
busy = true;
|
||||
do {
|
||||
dirty = false;
|
||||
o.OnNext(last_value);
|
||||
await Task.Delay(throttle);
|
||||
} while (dirty);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
public static IObservable<TResult> FromEventThrottle<T, TResult>(Action<Action<T>> add_handler, Action<Action<T>> remove_handler, Func<T, TResult> selector, TimeSpan throttle) =>
|
||||
FromEventThrottle(cb => {
|
||||
add_handler(cb);
|
||||
return Disposable.Create(() => remove_handler(cb));
|
||||
}, selector, throttle);
|
||||
|
||||
public static IObservable<TResult> FromEventThrottle<T, TResult>(Func<Action<T>, IDisposable> add_handler, Func<T, TResult> selector, TimeSpan throttle) =>
|
||||
Observable.Create<TResult>(o => {
|
||||
T last_value = default;
|
||||
bool busy = false, dirty = false;
|
||||
return add_handler(callback);
|
||||
async void callback(T value) {
|
||||
last_value = value;
|
||||
if (busy) {
|
||||
dirty = true;
|
||||
} else {
|
||||
busy = true;
|
||||
do {
|
||||
var result = selector(last_value);
|
||||
dirty = false;
|
||||
o.OnNext(result);
|
||||
await Task.Delay(throttle);
|
||||
} while (dirty);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public static IObservable<(TSource prev, TSource curr)> PairWithPrevious<TSource>(this IObservable<TSource> source) =>
|
||||
PairWithPrevious(source, default);
|
||||
|
||||
public static IObservable<(TSource prev, TSource curr)> PairWithPrevious<TSource>(this IObservable<TSource> source, TSource default_value) =>
|
||||
source.Scan<TSource, (TSource prev, TSource curr)>((default_value, default_value), (last, next) => (last.curr, next));
|
||||
|
||||
public static IObservable<T> RefCountReplay<T>(this IObservable<T> source, int buffer_size) {
|
||||
if (buffer_size < 0) throw new ArgumentException("Buffer size is less than zero.", nameof(buffer_size));
|
||||
if (buffer_size == 0) return source.Publish().RefCount();
|
||||
int num_subscribers = 0;
|
||||
ReplaySubject<T> replay = new ReplaySubject<T>(buffer_size);
|
||||
IDisposable source_sub = null;
|
||||
return Observable.Create<T>(o => {
|
||||
if (Interlocked.Increment(ref num_subscribers) == 1)
|
||||
source_sub = source.Subscribe(replay.OnNext);
|
||||
IDisposable sub = replay.Subscribe(o);
|
||||
return Disposable.Create(() => {
|
||||
sub.Dispose();
|
||||
if (Interlocked.Decrement(ref num_subscribers) == 0)
|
||||
source_sub.Dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses each element of a source observable to create an inner observable of <typeparamref name="TResult"/>, unlike <see cref="Observable.SelectMany{TSource, TResult}(IObservable{TSource}, Func{TSource, IObservable{TResult}})"/>,
|
||||
/// each inner sequence is terminated when the next one begins.
|
||||
/// <para/>In other words, observers are notified by the inner observerables in "series," rather than "parallel."
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The type of elements in the source (outer) sequence.</typeparam>
|
||||
/// <typeparam name="TResult">The type of elements in the inner sequences.</typeparam>
|
||||
/// <param name="source">A sequence of elements to create a series of inner sequences.</param>
|
||||
/// <param name="select_next">A function to create an inner sequence.</param>
|
||||
/// <returns>An IDisposable object used to unsubscribe from the source (outer) sequence, and the latest inner sequence.</returns>
|
||||
public static IObservable<TResult> SelectManyInSeries<TSource, TResult>(this IObservable<TSource> source, Func<TSource, IObservable<TResult>> select_next) =>
|
||||
Observable.Create<TResult>(o => {
|
||||
object locker = new object();
|
||||
bool is_disposed = false;
|
||||
IDisposable inner_sub = null;
|
||||
IDisposable outer_sub = source.Subscribe(on_outer);
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void on_outer(TSource value) {
|
||||
lock (locker) {
|
||||
if (!is_disposed) {
|
||||
inner_sub?.Dispose();
|
||||
inner_sub = select_next(value)?.Subscribe(o.OnNext);
|
||||
}
|
||||
}
|
||||
}
|
||||
void dispose() {
|
||||
IDisposable temp_outer_sub = null, temp_inner_sub = null;
|
||||
lock (locker) {
|
||||
is_disposed = true;
|
||||
temp_outer_sub = outer_sub;
|
||||
outer_sub = null;
|
||||
temp_inner_sub = inner_sub;
|
||||
inner_sub = null;
|
||||
}
|
||||
temp_outer_sub?.Dispose();
|
||||
temp_inner_sub?.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
public static IObservable<T> StartWith<T>(this IObservable<T> source, Func<T> get_value) =>
|
||||
Observable.Create<T>(o => {
|
||||
o.OnNext(get_value());
|
||||
return source.Subscribe(o);
|
||||
});
|
||||
|
||||
public static IObservable<T> StartWith<T>(this IObservable<T> source, Func<T> get_value, Func<bool> can_get_value) =>
|
||||
Observable.Create<T>(o => {
|
||||
if (can_get_value())
|
||||
o.OnNext(get_value());
|
||||
return source.Subscribe(o);
|
||||
});
|
||||
|
||||
public static IDisposable Subscribe<T>(this IObservable<T> source, params IObserver<T>[] observers) {
|
||||
return source.Subscribe(on_next, on_error, on_completed);
|
||||
// local methods
|
||||
void on_next(T value) {
|
||||
for (int i = 0; i < observers.Length; i++)
|
||||
observers[i].OnNext(value);
|
||||
}
|
||||
void on_error(Exception error) {
|
||||
for (int i = 0; i < observers.Length; i++)
|
||||
observers[i].OnError(error);
|
||||
}
|
||||
void on_completed() {
|
||||
for (int i = 0; i < observers.Length; i++)
|
||||
observers[i].OnCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An inner subscription is made for each value in the source sequence using the specified delegate (<paramref name="subscribe_value"/>).
|
||||
/// When a new value arrives, the subscription for the previous value is disposed before <paramref name="subscribe_value"/> is invoked with the new value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the source sequence.</typeparam>
|
||||
/// <param name="source">A sequence of elements to invoke a subscribe function on.</param>
|
||||
/// <param name="subscribe_value">A function used to subscribe each source element.</param>
|
||||
/// <returns>An IDisposable object used to unsubscribe from the source sequence, and dispose the latest value subscription.</returns>
|
||||
public static IDisposable SubscribeEach<T>(this IObservable<T> source, Func<T, IDisposable> subscribe_value) {
|
||||
IDisposable value_sub = null;
|
||||
IDisposable source_sub = source.Subscribe(on_source);
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void on_source(T value) {
|
||||
Disposable.dispose(ref value_sub);
|
||||
value_sub = subscribe_value(value);
|
||||
}
|
||||
void dispose() {
|
||||
Disposable.dispose(ref source_sub);
|
||||
Disposable.dispose(ref value_sub);
|
||||
}
|
||||
}
|
||||
|
||||
// was sick of this scenario: observable_source.Select(enumerable => enumerable.FirstOrDefault(where)).Where(val => val != null)
|
||||
// with structs it was even more annoying because they can't be null
|
||||
public static IObservable<TResult> TrySelect<T, TResult>(this IObservable<T> source, Func<T, (TResult result, bool success)> try_select) =>
|
||||
source.Select(try_select).Where(ts => ts.success).Select(ts => ts.result);
|
||||
}
|
||||
}
|
||||
using mbts;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace System.Reactive.Linq {
|
||||
public static class ObservableExtensions {
|
||||
/// <summary>
|
||||
/// <para>Creates an "Observable Heartbeat" that will publish true/false to indicate whether or not the heartbeat is detected, it will only send updates when the 'heartbeat status' has changed.</para>
|
||||
/// <para>Whenever the source produces an update, the heartbeat evaluates to true and the heartbeat duration clock is reset.</para>
|
||||
/// <para>Whenever heartbeat duration clock expires, the heartbeat evaluates to false.</para>
|
||||
/// </summary>
|
||||
/// <typeparam name="T">Type of data produced by the source (bears no impact on the resultant observable heartbeat</typeparam>
|
||||
/// <param name="source">The source that will indicate alive/dead by publishing updates within the duration period</param>
|
||||
/// <param name="duration">The amount of time that must pass without receiving an update from the source to determine the heartbeat is not detected</param>
|
||||
/// <returns>An "Observable Heartbeat"</returns>
|
||||
public static IObservable<bool> AsHeartbeat<T>(this IObservable<T> source, TimeSpan duration) {
|
||||
IObservable<bool> dead = source.Throttle(duration).Select(_ => false);
|
||||
IObservable<bool> alive = source.Select(_ => true);
|
||||
return dead.Merge(alive).DistinctUntilChanged();
|
||||
}
|
||||
|
||||
public static IObservable<T> BufferLatest<T>(this IObservable<T> source, TimeSpan throttle) =>
|
||||
source.Buffer(throttle).Where(vals => vals.Count > 0).Select(vals => vals[vals.Count - 1]);
|
||||
|
||||
public static IObservable<(T1, T2)> CombineLatest<T1, T2>(this IObservable<T1> source, IObservable<T2> second) =>
|
||||
source.CombineLatest(second, (v1, v2) => (v1, v2));
|
||||
|
||||
public static async Task<(T result, bool success)> FirstAsyncTask<T>(this IObservable<T> source, TimeSpan timeout) where T : class =>
|
||||
await source.Select(val => (val, true)).Merge(Observable.Return((default(T), false)).Delay(timeout)).FirstAsync();
|
||||
|
||||
public static async Task<(T result, bool success)> FirstAsyncTask<T>(this IObservable<T> source, Func<T, bool> predicate, TimeSpan timeout) where T : class =>
|
||||
await source.Where(predicate).Select(val => (val, true)).Merge(Observable.Return((default(T), false)).Delay(timeout)).FirstAsync();
|
||||
|
||||
public static async Task<(TResult result, bool success)> FirstAsyncTask<T, TResult>(this IObservable<T> source, Func<T, bool> predicate, Func<T, TResult> selector, TimeSpan timeout) where T : class =>
|
||||
await source.Where(predicate).Select(val => (selector(val), true)).Merge(Observable.Return((default(TResult), false)).Delay(timeout)).FirstAsync();
|
||||
|
||||
public static Task<T> FirstOrDefaultAsync<T>(this IObservable<T> source, TimeSpan timeout, T default_value = default) {
|
||||
var cts = new CancellationTokenSource(timeout);
|
||||
var main_task = source.FirstOrDefaultAsync().ToTask(cts.Token);
|
||||
var result_task = main_task.ContinueWith(t => t.Status == TaskStatus.RanToCompletion ? t.Result : default_value);
|
||||
return result_task;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A less verbose way to call <see cref="Observable.FromEvent{Action{TEventArgs}, TEventArgs}(Action{Action{TEventArgs}}, Action{Action{TEventArgs}})"/>.
|
||||
/// </summary>
|
||||
public static IObservable<T> FromAction<T>(Action<T> event_source) =>
|
||||
Observable.FromEvent<Action<T>, T>(cb => event_source += cb, cb => event_source -= cb);
|
||||
|
||||
public static IObservable<T> FromEvent<T>(Action<Action> add_handler, Action<Action> remove_handler, Func<T> selector) =>
|
||||
Observable.Create<T>(o => {
|
||||
callback();
|
||||
add_handler(callback);
|
||||
return Disposable.Create(() => remove_handler(callback));
|
||||
void callback() => o.OnNext(selector());
|
||||
});
|
||||
|
||||
public static IObservable<T> FromEventThrottle<T>(Action<Action<T>> add_handler, Action<Action<T>> remove_handler, TimeSpan throttle) =>
|
||||
FromEventThrottle<T>(cb => {
|
||||
add_handler(cb);
|
||||
return Disposable.Create(() => remove_handler(cb));
|
||||
}, throttle);
|
||||
|
||||
public static IObservable<T> FromEventThrottle<T>(Func<Action<T>, IDisposable> add_handler, TimeSpan throttle) =>
|
||||
Observable.Create<T>(o => {
|
||||
T last_value = default;
|
||||
bool busy = false, dirty = false;
|
||||
return add_handler(callback);
|
||||
async void callback(T value) {
|
||||
last_value = value;
|
||||
if (busy) {
|
||||
dirty = true;
|
||||
} else {
|
||||
busy = true;
|
||||
do {
|
||||
dirty = false;
|
||||
o.OnNext(last_value);
|
||||
await Task.Delay(throttle);
|
||||
} while (dirty);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
public static IObservable<TResult> FromEventThrottle<T, TResult>(Action<Action<T>> add_handler, Action<Action<T>> remove_handler, Func<T, TResult> selector, TimeSpan throttle) =>
|
||||
FromEventThrottle(cb => {
|
||||
add_handler(cb);
|
||||
return Disposable.Create(() => remove_handler(cb));
|
||||
}, selector, throttle);
|
||||
|
||||
public static IObservable<TResult> FromEventThrottle<T, TResult>(Func<Action<T>, IDisposable> add_handler, Func<T, TResult> selector, TimeSpan throttle) =>
|
||||
Observable.Create<TResult>(o => {
|
||||
T last_value = default;
|
||||
bool busy = false, dirty = false;
|
||||
return add_handler(callback);
|
||||
async void callback(T value) {
|
||||
last_value = value;
|
||||
if (busy) {
|
||||
dirty = true;
|
||||
} else {
|
||||
busy = true;
|
||||
do {
|
||||
var result = selector(last_value);
|
||||
dirty = false;
|
||||
o.OnNext(result);
|
||||
await Task.Delay(throttle);
|
||||
} while (dirty);
|
||||
busy = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
public static IObservable<(TSource prev, TSource curr)> PairWithPrevious<TSource>(this IObservable<TSource> source) =>
|
||||
PairWithPrevious(source, default);
|
||||
|
||||
public static IObservable<(TSource prev, TSource curr)> PairWithPrevious<TSource>(this IObservable<TSource> source, TSource default_value) =>
|
||||
source.Scan<TSource, (TSource prev, TSource curr)>((default_value, default_value), (last, next) => (last.curr, next));
|
||||
|
||||
public static IObservable<T> RefCountReplay<T>(this IObservable<T> source, int buffer_size) {
|
||||
if (buffer_size < 0) throw new ArgumentException("Buffer size is less than zero.", nameof(buffer_size));
|
||||
if (buffer_size == 0) return source.Publish().RefCount();
|
||||
int num_subscribers = 0;
|
||||
ReplaySubject<T> replay = new ReplaySubject<T>(buffer_size);
|
||||
IDisposable source_sub = null;
|
||||
return Observable.Create<T>(o => {
|
||||
if (Interlocked.Increment(ref num_subscribers) == 1)
|
||||
source_sub = source.Subscribe(replay.OnNext);
|
||||
IDisposable sub = replay.Subscribe(o);
|
||||
return Disposable.Create(() => {
|
||||
sub.Dispose();
|
||||
if (Interlocked.Decrement(ref num_subscribers) == 0)
|
||||
source_sub.Dispose();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses each element of a source observable to create an inner observable of <typeparamref name="TResult"/>, unlike <see cref="Observable.SelectMany{TSource, TResult}(IObservable{TSource}, Func{TSource, IObservable{TResult}})"/>,
|
||||
/// each inner sequence is terminated when the next one begins.
|
||||
/// <para/>In other words, observers are notified by the inner observerables in "series," rather than "parallel."
|
||||
/// </summary>
|
||||
/// <typeparam name="TSource">The type of elements in the source (outer) sequence.</typeparam>
|
||||
/// <typeparam name="TResult">The type of elements in the inner sequences.</typeparam>
|
||||
/// <param name="source">A sequence of elements to create a series of inner sequences.</param>
|
||||
/// <param name="select_next">A function to create an inner sequence.</param>
|
||||
/// <returns>An IDisposable object used to unsubscribe from the source (outer) sequence, and the latest inner sequence.</returns>
|
||||
public static IObservable<TResult> SelectManyInSeries<TSource, TResult>(this IObservable<TSource> source, Func<TSource, IObservable<TResult>> select_next) =>
|
||||
Observable.Create<TResult>(o => {
|
||||
object locker = new object();
|
||||
bool is_disposed = false;
|
||||
IDisposable inner_sub = null;
|
||||
IDisposable outer_sub = source.Subscribe(on_outer);
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void on_outer(TSource value) {
|
||||
lock (locker) {
|
||||
if (!is_disposed) {
|
||||
inner_sub?.Dispose();
|
||||
inner_sub = select_next(value)?.Subscribe(o.OnNext);
|
||||
}
|
||||
}
|
||||
}
|
||||
void dispose() {
|
||||
IDisposable temp_outer_sub = null, temp_inner_sub = null;
|
||||
lock (locker) {
|
||||
is_disposed = true;
|
||||
temp_outer_sub = outer_sub;
|
||||
outer_sub = null;
|
||||
temp_inner_sub = inner_sub;
|
||||
inner_sub = null;
|
||||
}
|
||||
temp_outer_sub?.Dispose();
|
||||
temp_inner_sub?.Dispose();
|
||||
}
|
||||
});
|
||||
|
||||
public static IObservable<T> StartWith<T>(this IObservable<T> source, Func<T> get_value) =>
|
||||
Observable.Create<T>(o => {
|
||||
o.OnNext(get_value());
|
||||
return source.Subscribe(o);
|
||||
});
|
||||
|
||||
public static IObservable<T> StartWith<T>(this IObservable<T> source, Func<T> get_value, Func<bool> can_get_value) =>
|
||||
Observable.Create<T>(o => {
|
||||
if (can_get_value())
|
||||
o.OnNext(get_value());
|
||||
return source.Subscribe(o);
|
||||
});
|
||||
|
||||
public static IDisposable Subscribe<T>(this IObservable<T> source, params IObserver<T>[] observers) {
|
||||
return source.Subscribe(on_next, on_error, on_completed);
|
||||
// local methods
|
||||
void on_next(T value) {
|
||||
for (int i = 0; i < observers.Length; i++)
|
||||
observers[i].OnNext(value);
|
||||
}
|
||||
void on_error(Exception error) {
|
||||
for (int i = 0; i < observers.Length; i++)
|
||||
observers[i].OnError(error);
|
||||
}
|
||||
void on_completed() {
|
||||
for (int i = 0; i < observers.Length; i++)
|
||||
observers[i].OnCompleted();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An inner subscription is made for each value in the source sequence using the specified delegate (<paramref name="subscribe_value"/>).
|
||||
/// When a new value arrives, the subscription for the previous value is disposed before <paramref name="subscribe_value"/> is invoked with the new value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of elements in the source sequence.</typeparam>
|
||||
/// <param name="source">A sequence of elements to invoke a subscribe function on.</param>
|
||||
/// <param name="subscribe_value">A function used to subscribe each source element.</param>
|
||||
/// <returns>An IDisposable object used to unsubscribe from the source sequence, and dispose the latest value subscription.</returns>
|
||||
public static IDisposable SubscribeEach<T>(this IObservable<T> source, Func<T, IDisposable> subscribe_value) {
|
||||
IDisposable value_sub = null;
|
||||
IDisposable source_sub = source.Subscribe(on_source);
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void on_source(T value) {
|
||||
Disposable.dispose(ref value_sub);
|
||||
value_sub = subscribe_value(value);
|
||||
}
|
||||
void dispose() {
|
||||
Disposable.dispose(ref source_sub);
|
||||
Disposable.dispose(ref value_sub);
|
||||
}
|
||||
}
|
||||
|
||||
// was sick of this scenario: observable_source.Select(enumerable => enumerable.FirstOrDefault(where)).Where(val => val != null)
|
||||
// with structs it was even more annoying because they can't be null
|
||||
public static IObservable<TResult> TrySelect<T, TResult>(this IObservable<T> source, Func<T, (TResult result, bool success)> try_select) =>
|
||||
source.Select(try_select).Where(ts => ts.success).Select(ts => ts.result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
namespace bts.utils {
|
||||
// Mainly used as an optional override of nullable data arg.
|
||||
// Explanation: the 'with' method pattern typically interprets null args to mean "keep the existing value."
|
||||
// However, this makes it impossible for the caller to overwrite an existing value with null.
|
||||
// This struct is designed (or at least was originally) to address this use-case.
|
||||
// It is analagous to escape chars.
|
||||
readonly struct OptionalNullable<T> where T : struct {
|
||||
public OptionalNullable(T? value) => this.value = value;
|
||||
|
||||
public T? value { get; }
|
||||
|
||||
public static implicit operator OptionalNullable<T>(T? value) =>
|
||||
new OptionalNullable<T>(value);
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
set(sources files/cert_secure_logon.txt;files/futures.coconut.xz;files/onixs_ice/ConnectivityConfiguration.APITest.xml;files/onixs_ice/ConnectivityConfiguration.PerfTest.xml;files/onixs_ice/ConnectivityConfiguration.Production.xml;files/perf_trace/compiled.css;files/perf_trace/index.html;files/perf_trace/jquery-3.3.1.min.js;files/perf_trace/main.js;files/perf_trace/perf_log.sql;files/perf_trace/perf_trace_analysis.sql;files/perf_trace/plotly-latest.min.js)
|
||||
set(sources files/cert_secure_logon.txt;files/futures.coconut.xz;files/onixs_ice/ConnectivityConfiguration.APITest.xml;files/onixs_ice/ConnectivityConfiguration.APITest.xml.orig;files/onixs_ice/ConnectivityConfiguration.PerfTest.xml;files/onixs_ice/ConnectivityConfiguration.Production.xml;files/perf_trace/compiled.css;files/perf_trace/index.html;files/perf_trace/jquery-3.3.1.min.js;files/perf_trace/main.js;files/perf_trace/perf_log.sql;files/perf_trace/perf_trace_analysis.sql;files/perf_trace/plotly-latest.min.js)
|
||||
@@ -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/service_helpers/PositionUtilities.cpp;api/services/AdminService.proto;api/services/AlgoDefinitionService.proto;api/services/AlgoService.proto;api/services/ApiUnitTestService.proto;api/services/AuthService.proto;api/services/BasePositionService.proto;api/services/ErrorReportService.proto;api/services/HelloService.proto;api/services/MBOService.proto;api/services/MarginService.proto;api/services/MarketDataAuxiliaryService.proto;api/services/MarketDataRelay.proto;api/services/MarketDataService.proto;api/services/MoneyFileService.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/position_cfg.conf;config/defaults/span.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/AlgoTypes.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/position_cfg.proto;config/protos/quickbus_cfg.proto;config/protos/span.proto;config/protos/spreader.proto;config/protos/subprocess.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/sod.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/Product.cpp;md/SecurityCache.cpp;md/SecurityDB.cpp;md/SecurityIDRecorder.cpp;md/SecurityMapper.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/service/MarketDataServiceServer.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;md/vendor/OnixsUtils.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.cLine 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/service_helpers/PositionUtilities.cpp;api/services/AdminService.proto;api/services/AlgoDefinitionService.proto;api/services/AlgoService.proto;api/services/ApiUnitTestService.proto;api/services/AuthService.proto;api/services/BasePositionService.proto;api/services/ErrorReportService.proto;api/services/HelloService.proto;api/services/MBOService.proto;api/services/MarginService.proto;api/services/MarketDataAuxiliaryService.proto;api/services/MarketDataRelay.proto;api/services/MarketDataService.proto;api/services/MoneyFileService.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/position_cfg.conf;config/defaults/span.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/AlgoTypes.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/position_cfg.proto;config/protos/quickbus_cfg.proto;config/protos/span.proto;config/protos/spreader.proto;config/protos/subprocess.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/sod.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/Product.cpp;md/SecurityCache.cpp;md/SecurityDB.cpp;md/SecurityIDRecorder.cpp;md/SecurityMapper.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/service/MarketDataServiceServer.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;md/vendor/OnixsUtils.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.cLine truncated
|
||||
@@ -292,6 +292,22 @@ const BTSAccount* AdminServiceUtils::find_account (const AdminData& data,
|
||||
return account_ptr;
|
||||
return nullptr;
|
||||
}
|
||||
SecurityRiskLimitUnit const* AdminServiceUtils::get_sec_limit (
|
||||
const AdminData& data,
|
||||
uint32_t account_id,
|
||||
uint32_t sec_key){
|
||||
const BTSAccount* acct_ptr = find_account(data, account_id,false);
|
||||
if(acct_ptr != nullptr){
|
||||
if(acct_ptr->has_proprietary_risk_limits()){
|
||||
const auto& prop_limits = acct_ptr->proprietary_risk_limits();
|
||||
for (const auto& l : prop_limits.security_limits()){
|
||||
if(l.security_key() == sec_key)
|
||||
return &l;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
std::vector<const BTSAccount*> AdminServiceUtils::accounts (
|
||||
const AdminData& data,
|
||||
bool include_deleted){
|
||||
@@ -694,7 +710,26 @@ void AdminServiceUtils::update_account(bts::api::admin::AdminData& admin_data,
|
||||
validate_account_update (admin_data, updated_account, result);
|
||||
if (result.is_success()){
|
||||
bts::api::admin::BTSAccount* account_ptr = find_account(admin_data, updated_account.id());
|
||||
// prop_limits are not overwritten when the account is updated
|
||||
// from the risk portal
|
||||
const ProprietaryRiskLimits prop_limits = account_ptr->proprietary_risk_limits();
|
||||
account_ptr->CopyFrom(updated_account);
|
||||
account_ptr->mutable_proprietary_risk_limits()->CopyFrom(prop_limits);
|
||||
}
|
||||
}
|
||||
void AdminServiceUtils::update_proprietary_risk(
|
||||
AdminData& data,
|
||||
const ProprietaryRiskLimitsAPIArgument& arg,
|
||||
AdminServiceOperationResult& result){
|
||||
BTSAccount* acct_ptr = find_account(data,arg.account_id());
|
||||
if(acct_ptr == nullptr){
|
||||
result.set_is_success(false);
|
||||
result.set_error_text("Could not find account id");
|
||||
return;
|
||||
}
|
||||
else{
|
||||
result.set_is_success(true);
|
||||
acct_ptr->mutable_proprietary_risk_limits()->CopyFrom(arg.proprietary_risk_limits());
|
||||
}
|
||||
}
|
||||
void AdminServiceUtils::delete_account (bts::api::admin::AdminData& admin_data,
|
||||
|
||||
@@ -83,6 +83,9 @@ public:
|
||||
static const BTSAccount* find_account (const AdminData& data,
|
||||
uint32_t id,
|
||||
bool include_deleted=false);
|
||||
static SecurityRiskLimitUnit const* get_sec_limit (const AdminData& data,
|
||||
uint32_t account_id,
|
||||
uint32_t sec_key);
|
||||
// this function is an indirection layer for account access
|
||||
// so deletions are dealt in a more deliberate manner
|
||||
static std::vector<const BTSAccount*> accounts (const AdminData& data,
|
||||
@@ -142,6 +145,10 @@ public:
|
||||
static void update_account(AdminData& data,
|
||||
const BTSAccount& updated_account,
|
||||
AdminServiceOperationResult& result);
|
||||
static void update_proprietary_risk(
|
||||
AdminData& data,
|
||||
const ProprietaryRiskLimitsAPIArgument& arg,
|
||||
AdminServiceOperationResult& result);
|
||||
static void delete_account (AdminData& admin_data,
|
||||
const IDAPIArgument& id,
|
||||
AdminServiceOperationResult& result);
|
||||
|
||||
@@ -81,9 +81,9 @@ void PositionUtilities::apply_fill (PositionEntry& pos,
|
||||
const Fill& f,
|
||||
bool is_add){
|
||||
// strip legs are not applied to positions
|
||||
if(f.fill_kind() == Fill::FILL_KIND_STRIP_LEG ||
|
||||
f.fill_kind() == Fill::FILL_KIND_SOD_STRIP_LEG)
|
||||
return;
|
||||
// if(f.fill_kind() == Fill::FILL_KIND_STRIP_LEG ||
|
||||
// f.fill_kind() == Fill::FILL_KIND_SOD_STRIP_LEG)
|
||||
// return;
|
||||
PositionUtilities::apply_fill(pos,
|
||||
f.side(),
|
||||
f.last_price(),
|
||||
@@ -113,6 +113,13 @@ void PositionUtilities::total_position(const PositionUnit& p,
|
||||
p.admin_day().short_pos() + p.admin_sod().short_pos();
|
||||
}
|
||||
|
||||
void PositionUtilities::day_position(const PositionUnit& p,
|
||||
uint32_t& total_long,
|
||||
uint32_t& total_short){
|
||||
total_long = p.day().long_pos() + p.admin_day().long_pos();
|
||||
total_short = p.day().short_pos() + p.admin_day().short_pos();
|
||||
}
|
||||
|
||||
PositionEntry PositionUtilities::add_positions (
|
||||
const std::vector<PositionEntry const*>& v){
|
||||
double long_pos = 0, long_price = 0, short_pos = 0, short_price = 0;
|
||||
|
||||
@@ -39,6 +39,10 @@ public:
|
||||
uint32_t& total_long,
|
||||
uint32_t& total_short);
|
||||
|
||||
static void day_position(const bts::api::position::PositionUnit& p,
|
||||
uint32_t& total_long,
|
||||
uint32_t& total_short);
|
||||
|
||||
static bts::api::position::PositionEntry add_positions (
|
||||
const std::vector<bts::api::position::PositionEntry const*>& v);
|
||||
|
||||
|
||||
@@ -7,6 +7,31 @@ import "lib/api/apicore.proto";
|
||||
import "lib/domain_model/Security.proto";
|
||||
import "lib/api/services/OrderService.proto";
|
||||
|
||||
/************************************************************************/
|
||||
/********************************DJ prop*********************************/
|
||||
/************************************************************************/
|
||||
|
||||
message SecurityRiskLimitUnit{
|
||||
uint32 security_key = 10;
|
||||
bool enforce_net_pos_limits = 20;
|
||||
uint32 net_long_position_limit = 30;
|
||||
uint32 net_short_position_limit = 40;
|
||||
bool enforce_clip_limits = 50;
|
||||
uint32 clip_size = 60;
|
||||
bool enforce_price_bands = 70;
|
||||
double min_sell_price = 80;
|
||||
double max_buy_price = 90;
|
||||
}
|
||||
|
||||
message ProprietaryRiskLimits{
|
||||
repeated SecurityRiskLimitUnit security_limits = 10;
|
||||
}
|
||||
|
||||
message ProprietaryRiskLimitsAPIArgument{
|
||||
uint32 account_id = 10;
|
||||
ProprietaryRiskLimits proprietary_risk_limits = 20;
|
||||
}
|
||||
|
||||
/************************************************************************/
|
||||
/********************************Gen (temp)******************************/
|
||||
/************************************************************************/
|
||||
@@ -152,6 +177,7 @@ message BTSAccount{
|
||||
// extra parameters for risk enforcement
|
||||
RiskLimitEnforcement risk_limit_enforcement=120;
|
||||
AccountLossLimit account_loss_limit = 130;
|
||||
ProprietaryRiskLimits proprietary_risk_limits = 1000;
|
||||
}
|
||||
|
||||
message RiskLimitEnforcement{
|
||||
@@ -497,4 +523,6 @@ service AdminService {
|
||||
rpc md_entitlement_data(bts.api.VoidMessage) returns (stream MDEntitlementReportUnit);
|
||||
// misc
|
||||
rpc md_permissions_data(bts.api.VoidMessage) returns (stream UserSymbolMDPermissionUnit);
|
||||
// prop sec limits
|
||||
rpc update_proprietary_risk_limits (ProprietaryRiskLimitsAPIArgument) returns (AdminServiceOperationResult);
|
||||
}
|
||||
@@ -6,6 +6,7 @@ MarketDataConfig {
|
||||
rule: "DeploymentType.type == 'PROD'"
|
||||
value {
|
||||
use_sim: false
|
||||
use_onix: false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
#nusers: 1
|
||||
#allow concurrent : true
|
||||
#expires: 2021-Jan-01 00:00:00 637450560000000000
|
||||
#generation_time: 2020-Apr-03 18:39:31.010863 5248901378137496534
|
||||
|
||||
|
||||
#nusers: 100000
|
||||
#allow concurrent : false
|
||||
#expires: 3020-May-18 00:00:00 952822656000000000
|
||||
#generation_time: 2020-Aug-11 14:19:03.379579 5249013541861183694
|
||||
EncryptedKeyCode {
|
||||
value {
|
||||
keycode: "BJBQWsbdTXWa/iLmOf3ekE8vP/NYkf0Q/nUsp/rJ78Ze3W9pNXICCMabmmsr1xOM7aqRIclPgZd955uSSfvFkdNWFTxwPQ5IfnDGopj3QdhUVxhzgVLaXHN+vC0H/MWrDQ5K4WY5J/6VXgUn1LSN/2HNb5iR2sKXeh3Alamg87YtCWEi+scelffE7HK5ERT/3A3Y/e0pqUFhP6xouIjHRhC2FRBb6OoJQVvFwVDADSuKZNo9B6GhC3H0YdHqjFhGDoky5ecgzeKIX8uWJEQNnQXQJ0iTgQ95+YR5IMU+XWdlVfSrc0fuTmj3YWcBQ8Kl2ItEcobyZzJYega3tu6Ftw=="
|
||||
keycode: "hrelQBN4n0kURYKYLwp2nzYTaxeyJZoGs//hN9rkyyVTgp1EQpyOnZHo7klNF8r9Ci8QV8aD0Yp66FvhtWqLFkNWCZ4pp48HzC34pdRzVbXjTmyEMK6iz/nBLE/VuYtBoXihlxIqOjTwnv4sNeAgboSOadVvGY3mRb3b9tMEIRTSbuq+KdPuTM2y/swL8fg53PNLclkqzU9f6mI/250RmnCvWzZf7Liu6TBWfQAI2RfGKP1+blewONFHqkeSA/fVkGfdiZy7cJOqak9VrxzLsJwqAK5DGMNv/kk4jceT3mF3xaSzoEzbge3ekk/SxXkuAKFrT6fxrr/v381SeHoHSw=="
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ message MultiThreadedWindows {
|
||||
optional bool algo_studio = 6 [default = true];
|
||||
optional bool ladder = 7 [default = true];
|
||||
optional bool synthetic_management = 8 [default = true];
|
||||
optional bool risk_management = 10 [default = false];
|
||||
}
|
||||
|
||||
message LadderKnobs {
|
||||
|
||||
+116
-11
@@ -219,24 +219,129 @@ inline bool test_price_equality(double price_1, double price_2) {
|
||||
// ice specialization /////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
inline bool is_ice_spread(const bts::api::ICEExchangeData& ice_data){
|
||||
const auto& strip = ice_data.strip_name();
|
||||
return strip.find("/") != std::string::npos;
|
||||
// TODO, move this to its own file
|
||||
|
||||
static const uint32_t days_in_month[] = {31,28,31,30,31,30,31,31,30,31,30,31};
|
||||
|
||||
inline Quantity from_exchange_qty (Quantity exchange_qty,
|
||||
const bts::api::Security& sec){
|
||||
return Quantity(exchange_qty.value()/sec.ice().exchange_qty_mult());
|
||||
}
|
||||
|
||||
inline Quantity to_exchange_qty (Quantity internal_qty,
|
||||
const bts::api::Security& sec){
|
||||
return Quantity(internal_qty.value()*sec.ice().exchange_qty_mult());
|
||||
}
|
||||
|
||||
inline bool is_spread(const bts::api::Security& sec){
|
||||
return sec.has_ice() && sec.has_spread() &&
|
||||
sec.ice().strip_name().find("/") != std::string::npos;
|
||||
}
|
||||
|
||||
inline bool is_strip(const bts::api::Security& sec){
|
||||
return sec.has_ice() && sec.has_spread() && !is_ice_spread(sec.ice());
|
||||
auto rv = sec.has_ice() && sec.has_spread() &&
|
||||
!is_spread(sec);
|
||||
if(rv != sec.ice().is_strip())
|
||||
LOG(OrderMan, Error, "Sec:{} is_strip incorrect, expected:{}",
|
||||
sec.id(),rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
inline bool is_spread_of_strips(const bts::api::Security& sec){
|
||||
if(sec.has_spread())
|
||||
if (sec.has_ice()){
|
||||
const auto& spr = sec.spread();
|
||||
for (const auto& l : spr.legs())
|
||||
// assumption : either all legs are strips or none
|
||||
return is_strip(l.security());
|
||||
bool all_legs_strip = false;
|
||||
if(is_spread(sec)){
|
||||
all_legs_strip = true;
|
||||
for (const auto& l : sec.spread().legs())
|
||||
all_legs_strip = all_legs_strip && is_strip(l.security());
|
||||
}
|
||||
return all_legs_strip;
|
||||
}
|
||||
|
||||
inline bool is_strip_with_no_leg_fills(const bts::api::Security& sec){
|
||||
return sec.has_ice() &&
|
||||
sec.symbol() == "HHD" &&
|
||||
is_strip(sec);
|
||||
}
|
||||
|
||||
inline bool is_daily_contract(const api::Security& s){
|
||||
return s.symbol() == "HHD";
|
||||
}
|
||||
|
||||
inline bool is_tas(const api::Security& s){
|
||||
return s.has_ice() &&
|
||||
s.ice().market_description().find("TAS") != std::string::npos;
|
||||
}
|
||||
|
||||
// risk qty is the qty used for risk purposes for
|
||||
// 1 lot of this security.
|
||||
// daily futures and ice lots are considered 1,
|
||||
// other futures (HNG,HIS,HEN etc) are the days
|
||||
// in the month
|
||||
inline uint32_t get_fut_risk_qty(const api::Security& s){
|
||||
bts_assert(s.has_future());
|
||||
bts_assert(s.has_ice());
|
||||
if(is_daily_contract(s))
|
||||
return 1;
|
||||
else if(s.ice().is_ice_lots())
|
||||
return 1;
|
||||
else{
|
||||
auto mm = s.future().expiration_month();
|
||||
if(mm <=11)
|
||||
return days_in_month[mm];
|
||||
else{
|
||||
LOG(persist, Error, "Illegal month:{} for sec:{}",
|
||||
mm, s.id());
|
||||
// TODO
|
||||
// Jan is showing as 12 sometimes
|
||||
return 31;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
inline uint32_t get_spread_direct_risk_qty(const api::Security& s){
|
||||
bts_assert(s.has_spread());
|
||||
bts_assert(s.has_ice());
|
||||
if(s.ice().is_ice_lots())
|
||||
return 1;
|
||||
else
|
||||
return 30;
|
||||
}
|
||||
|
||||
inline uint32_t get_risk_qty (const api::Security& s){
|
||||
if(s.has_future())
|
||||
return get_fut_risk_qty(s);
|
||||
else if (s.has_spread())
|
||||
return get_spread_direct_risk_qty(s);
|
||||
else{
|
||||
bts_assert(false);
|
||||
return 0.0;
|
||||
}
|
||||
}
|
||||
|
||||
inline void get_parent_child_relationships(
|
||||
const std::map<uint32_t, api::Security const*>& sec_map,
|
||||
std::map<uint32_t, api::Security const*>& out_res){
|
||||
auto find_master_future =
|
||||
[&](const api::Security& child_f) -> api::Security const* {
|
||||
for (const auto& kp : sec_map){
|
||||
const auto& f = *kp.second;
|
||||
if(f.has_ice() &&
|
||||
f.has_future() &&
|
||||
!f.ice().is_ice_lots() &&
|
||||
!is_tas(f) &&
|
||||
f.symbol() == child_f.symbol() &&
|
||||
f.future().expiration_year() == child_f.future().expiration_year() &&
|
||||
f.future().expiration_month() == child_f.future().expiration_month() &&
|
||||
f.future().expiration_day() == child_f.future().expiration_day())
|
||||
return &f;
|
||||
}
|
||||
return nullptr;
|
||||
};
|
||||
for (const auto& kp : sec_map){
|
||||
const auto& f = *kp.second;
|
||||
if(f.has_ice() && f.has_future() && f.ice().is_ice_lots())
|
||||
out_res[kp.first] = find_master_future(f);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace securities
|
||||
|
||||
@@ -116,6 +116,9 @@ message ICEExchangeData {
|
||||
string strip_name = 140;
|
||||
string mic_code = 150;
|
||||
bool is_strip = 160;
|
||||
bool is_ice_lots = 170;
|
||||
// for example 2500 for HNG, this is the qty the exchange needs to see
|
||||
uint32 exchange_qty_mult = 180;
|
||||
}
|
||||
|
||||
message FutureContract {
|
||||
|
||||
@@ -35,6 +35,8 @@ FixedSecurity::FixedSecurity() :
|
||||
ice_deal_denominator(0),
|
||||
ice_settle_denominator(0),
|
||||
is_ice_strip(false),
|
||||
is_ice_lots(false),
|
||||
ice_exchange_qty_mult(1),
|
||||
_symbol(),
|
||||
_cfi_code(),
|
||||
_security_desc(),
|
||||
@@ -197,6 +199,8 @@ void convert_security(const FixedSecurity& source, Security* presult) {
|
||||
dest.mutable_ice()->set_strip_name(source.ice_strip_name());
|
||||
dest.mutable_ice()->set_mic_code(source.mic_code());
|
||||
dest.mutable_ice()->set_is_strip(source.is_ice_strip);
|
||||
dest.mutable_ice()->set_is_ice_lots(source.is_ice_lots);
|
||||
dest.mutable_ice()->set_exchange_qty_mult(source.ice_exchange_qty_mult);
|
||||
break;
|
||||
case Venue::VENUE_BTS:
|
||||
break;
|
||||
@@ -253,7 +257,6 @@ void convert_security(const FixedSecurity& source, Security* presult) {
|
||||
case SecurityType_INT_MIN_SENTINEL_DO_NOT_USE_:
|
||||
break;
|
||||
}
|
||||
|
||||
get_contract_spec(source, *dest.mutable_contract_spec());
|
||||
}
|
||||
|
||||
@@ -323,6 +326,8 @@ void convert_security(const Security& src, FixedSecurity& dest, bool set_securit
|
||||
dest.ice_deal_denominator = src.ice().deal_denominator();
|
||||
dest.ice_settle_denominator = src.ice().settle_denominator();
|
||||
dest.is_ice_strip = src.ice().is_strip();
|
||||
dest.is_ice_lots = src.ice().is_ice_lots();
|
||||
dest.ice_exchange_qty_mult = src.ice().exchange_qty_mult();
|
||||
|
||||
dest.ice_market_description(src.ice().market_description());
|
||||
dest.ice_cleared_alias(src.ice().cleared_alias());
|
||||
|
||||
@@ -63,6 +63,8 @@ public:
|
||||
int8_t ice_deal_denominator;
|
||||
int8_t ice_settle_denominator;
|
||||
bool is_ice_strip;
|
||||
bool is_ice_lots;
|
||||
uint32_t ice_exchange_qty_mult;
|
||||
|
||||
STRING_PROPERTY(ice_market_description);
|
||||
STRING_PROPERTY(ice_cleared_alias);
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "lib/utils/helper_macros.h"
|
||||
#include "lib/utils/Dispatcher.h"
|
||||
#include "lib/utils/bts_chrono.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
|
||||
#include <boost/pool/object_pool.hpp>
|
||||
#include <atomic>
|
||||
@@ -59,6 +60,32 @@ struct ICEExecutionGateway::PImpl
|
||||
md::SecurityOracle sec_oracle;
|
||||
};
|
||||
|
||||
void from_exchange_qty (const api::Security& sec, bts::orderman::ExecutionReport* exec_rep){
|
||||
LOG(OrderMan, Info, "ICEExecutionGateway::from_exchange_qty "
|
||||
"sec:{} mult:{} "
|
||||
"leg_sec:{} leg_sec_mult:{}"
|
||||
"qty:{} filled_qty:{} last_shares:{} max_show:{} "
|
||||
"price:{} last_price:{} avg_price:{}",
|
||||
sec.id(), sec.ice().exchange_qty_mult(),
|
||||
exec_rep->leg_security == nullptr ? 0 : exec_rep->leg_security->id(),
|
||||
exec_rep->leg_security == nullptr ? os::nan() : exec_rep->leg_security->ice().exchange_qty_mult(),
|
||||
exec_rep->quantity, exec_rep->filled_quantity,
|
||||
exec_rep->last_shares, exec_rep->max_show,
|
||||
exec_rep->price, exec_rep->last_price, exec_rep->avg_price);
|
||||
exec_rep->quantity = bts::securities::from_exchange_qty(
|
||||
exec_rep->quantity,sec);
|
||||
exec_rep->filled_quantity = bts::securities::from_exchange_qty(
|
||||
exec_rep->filled_quantity,sec);
|
||||
// this is specific to the leg
|
||||
const api::Security& last_shr_sec = exec_rep->leg_security == nullptr ? sec :
|
||||
*exec_rep->leg_security;
|
||||
exec_rep->last_shares = bts::securities::from_exchange_qty(
|
||||
exec_rep->last_shares,
|
||||
last_shr_sec);
|
||||
exec_rep->max_show = bts::securities::from_exchange_qty(
|
||||
exec_rep->max_show,sec);
|
||||
}
|
||||
|
||||
ICEExecutionGateway::ICEExecutionGateway(const config::JetFIX::Connection &cfg, trace::PerfLog &log, md::SecurityOracle o)
|
||||
: IExecutionGateway(), me(new PImpl(cfg, log, o))
|
||||
{
|
||||
@@ -419,7 +446,7 @@ void ICEExecutionGateway::on_exe_report(const jetfix::fix::ICEExecutionReportMsg
|
||||
|
||||
data->last_shares = msg.has<fix::LastShares>()
|
||||
? Quantity(msg.get<fix::LastShares>())
|
||||
: Quantity(0);
|
||||
: Quantity(0);
|
||||
auto last_px_sec = data->leg_security ? data->leg_security : sec;
|
||||
data->last_price = msg.has<fix::LastPx>()
|
||||
? securities::decode_exchange_price(*last_px_sec, Price(msg.get<fix::LastPx>()))
|
||||
@@ -444,6 +471,7 @@ void ICEExecutionGateway::on_exe_report(const jetfix::fix::ICEExecutionReportMsg
|
||||
data->cfg = &me->cfg;
|
||||
data->msg = text_msg;
|
||||
data->exchange = api::Venue::VENUE_ICE;
|
||||
from_exchange_qty(*sec,data);
|
||||
me->on_exe_report(*data, msg.trace());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,9 +263,9 @@ make_pos_event(api::position::PositionUnit const &pu,
|
||||
auto sec = o.resolve(SecurityKey(pu.security_id()));
|
||||
if (sec) {
|
||||
uint32_t total_long_pos, total_short_pos;
|
||||
bts::position::PositionUtilities::total_position(pu,
|
||||
total_long_pos,
|
||||
total_short_pos);
|
||||
bts::position::PositionUtilities::day_position(pu,
|
||||
total_long_pos,
|
||||
total_short_pos);
|
||||
FilledPositionEvent ev{
|
||||
*sec, AccountID(pu.account_id()),
|
||||
Quantity(total_long_pos),
|
||||
|
||||
@@ -3,9 +3,31 @@
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
using namespace bts::admin;
|
||||
using namespace bts::api::admin;
|
||||
using namespace bts::api::orderman;
|
||||
|
||||
void test_parent_child_association (
|
||||
const std::map<uint32_t, api::Security const*>& sec_map,
|
||||
std::map<uint32_t, api::Security const*>& child_parent_map){
|
||||
if(sec_map.find(4294957117) != sec_map.end() &&
|
||||
sec_map.find(4294964322) != sec_map.end())
|
||||
child_parent_map[4294957117] = sec_map.find(4294964322)->second;
|
||||
}
|
||||
|
||||
|
||||
bts::api::admin::SecurityRiskLimitUnit bypass_sec_limits(){
|
||||
bts::api::admin::SecurityRiskLimitUnit rv;
|
||||
rv.set_enforce_net_pos_limits(false);
|
||||
rv.set_enforce_clip_limits(false);
|
||||
rv.set_enforce_price_bands(false);
|
||||
return rv;
|
||||
}
|
||||
|
||||
LimitStore::LimitStore(const bts::api::admin::AdminData &admin_data,
|
||||
const std::map<uint32_t, api::Security const *> &sec_map)
|
||||
: _admin_data(bts::admin::AdminServiceUtils::sort_limits(admin_data)) {
|
||||
: _admin_data(AdminServiceUtils::sort_limits(admin_data)),
|
||||
_bypass_sec_limit(bypass_sec_limits()){
|
||||
for (const auto& acct : _admin_data.accounts())
|
||||
_account_data_map.insert(std::pair<uint32_t,AccountData>(
|
||||
acct.id(),AccountData(acct)));
|
||||
@@ -21,10 +43,39 @@ LimitStore::LimitStore(const bts::api::admin::AdminData &admin_data,
|
||||
_sec_map.insert(std::pair<uint32_t, SecurityLimitTree>(
|
||||
security.id(), SecurityLimitTree(security,
|
||||
*product_tree_ptr,
|
||||
_admin_data)));
|
||||
_admin_data,
|
||||
_bypass_sec_limit)));
|
||||
}
|
||||
_set_parent_child_relationships(sec_map);
|
||||
_set_user_data();
|
||||
}
|
||||
void LimitStore::_set_parent_child_relationships(
|
||||
const std::map<uint32_t, api::Security const*>& sec_map){
|
||||
std::map<uint32_t, api::Security const*> child_parent_map;
|
||||
bts::securities::get_parent_child_relationships(sec_map,child_parent_map);
|
||||
for (const auto& kvp : child_parent_map)
|
||||
LOG(OrderMan, Info, "Child->Parent : {0}->{1}",
|
||||
kvp.first,kvp.second != nullptr ? kvp.second->id() : 0);
|
||||
//test_parent_child_association(sec_map,child_parent_map);
|
||||
for (const auto& kp : child_parent_map){
|
||||
uint32_t child_key = kp.first;
|
||||
api::Security const* parent_sec = kp.second;
|
||||
|
||||
const auto& it = _sec_map.find(child_key);
|
||||
bts_assert(it != _sec_map.end());
|
||||
SecurityLimitTree* child_sec_tree = &it->second;
|
||||
|
||||
SecurityLimitTree* parent_sec_tree = nullptr;
|
||||
if(parent_sec != nullptr){
|
||||
const auto& it = _sec_map.find(parent_sec->id());
|
||||
bts_assert(it != _sec_map.end());
|
||||
parent_sec_tree = &it->second;
|
||||
}
|
||||
child_sec_tree->set_parent_child(true,parent_sec_tree);
|
||||
if(parent_sec_tree != nullptr)
|
||||
parent_sec_tree->set_parent_child(false,child_sec_tree);
|
||||
}
|
||||
}
|
||||
void LimitStore::copy_data_from(LimitStore &source) {
|
||||
// copy securities
|
||||
for (auto it = _sec_map.begin(); it != _sec_map.end(); it++) {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "lib/os/os.h"
|
||||
#include "lib/config/config.h"
|
||||
#include "lib/config/protos/span.pb.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
|
||||
|
||||
namespace bts {
|
||||
@@ -73,6 +74,7 @@ private:
|
||||
// entried (above)
|
||||
std::vector<ProductLimitTree> _prod_map;
|
||||
const bts::api::admin::AccountProductPositionLimitUnit _zero_product_limit;
|
||||
const bts::api::admin::SecurityRiskLimitUnit _bypass_sec_limit;
|
||||
public:
|
||||
LimitStore(const bts::api::admin::AdminData& admin_data,
|
||||
const std::map<uint32_t,api::Security const*>& sec_map);
|
||||
@@ -125,9 +127,34 @@ public:
|
||||
AccountID account_id,
|
||||
Quantity long_pos,
|
||||
Quantity short_pos){
|
||||
LOG(OrderMan, Info,
|
||||
"LimitStore::set_filled_position sec:{} sym:{} acc:{} "
|
||||
"long:{} short:{}",
|
||||
security.id(), security.symbol(), account_id, long_pos, short_pos);
|
||||
SecurityLimitTree* sec_tree = _find_security(security.id());
|
||||
bts_assert(sec_tree != nullptr);
|
||||
sec_tree->update_position(account_id, long_pos, short_pos);
|
||||
// there are some strips where we dont store the leg
|
||||
// fills (because the exchange does not give us that, for
|
||||
// example the HHD strip of daily futures). in this case,
|
||||
// we add it manually here
|
||||
if(bts::securities::is_strip_with_no_leg_fills(security)){
|
||||
bts_assert(security.has_spread());
|
||||
for (const auto& l : security.spread().legs()){
|
||||
auto lside = l.side();
|
||||
auto lqty = l.quantity();
|
||||
Quantity llong, lshort;
|
||||
if(lside == api::Side::SIDE_BUY){
|
||||
llong = Quantity(long_pos.value() * lqty);
|
||||
lshort = Quantity(short_pos.value() * lqty);
|
||||
}
|
||||
else{
|
||||
llong = Quantity(short_pos.value() * lqty);
|
||||
lshort = Quantity(long_pos.value() * lqty);
|
||||
}
|
||||
set_filled_position(l.security(),account_id,llong,lshort);
|
||||
}
|
||||
}
|
||||
}
|
||||
void set_pnl (AccountID account_id, double pnl);
|
||||
void set_excess_liquidity (AccountID account_id, double excess_liquidity);
|
||||
@@ -174,7 +201,7 @@ public:
|
||||
const api::Security& security,
|
||||
api::Side side,
|
||||
Quantity quantity,
|
||||
bool enforce_clip = true) const{
|
||||
Price price){
|
||||
Limiter::LimiterQueryResult rv; rv.result = Limiter::OK;
|
||||
const auto acct_it = _account_data_map.find(account_id.value());
|
||||
AccountData const* account_ptr = (acct_it == _account_data_map.end()) ?
|
||||
@@ -185,33 +212,22 @@ public:
|
||||
_margin_limit(account_ptr,rv);
|
||||
if(!rv.ok())
|
||||
return rv;
|
||||
const SecurityLimitTree* sec_tree = _find_security(security.id());
|
||||
SecurityLimitTree* sec_tree = _find_security(security.id());
|
||||
bts_assert(sec_tree != nullptr);
|
||||
rv = sec_tree->can_send_new_order(account_id, side, quantity, enforce_clip);
|
||||
if (rv.ok() && security.has_spread() && sec_tree->is_residual_pos_spread()){
|
||||
// if its rejected we dont need to validate further,
|
||||
// however if its ok and its a spread we need to check
|
||||
// the legs
|
||||
for (const api::ContractLeg& leg : security.spread().legs()){
|
||||
api::Side effective_leg_side = side == api::Side::SIDE_BUY ? leg.side() : LimiterUtils::invert_side(leg.side());
|
||||
Quantity effective_leg_qty = Quantity (quantity.value() * std::abs(leg.quantity()));
|
||||
// in the case of a spread with future legs the clip limits
|
||||
// for the legs are not checked. if the legs are are spreads
|
||||
// themselves then they are checked (eventually it will recuse
|
||||
// down to a non spread)
|
||||
rv = can_send_new_order (account_id, leg.security(), effective_leg_side, effective_leg_qty,
|
||||
leg.security().has_spread());
|
||||
if (!rv.ok())
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
rv = sec_tree->can_send_new_order(account_id, side, quantity, price,
|
||||
Limiter::EnforcementParams());
|
||||
if (rv.ok() && security.has_spread())
|
||||
return _check_new_order_pos_violations_recursively(security,
|
||||
account_id,
|
||||
side,
|
||||
quantity);
|
||||
return rv;
|
||||
}
|
||||
Limiter::LimiterQueryResult can_update_order(AccountID account_id,
|
||||
const api::Security& security,
|
||||
api::Side side,
|
||||
Quantity from, Quantity to,
|
||||
bool enforce_clip = true) const{
|
||||
Price price){
|
||||
Limiter::LimiterQueryResult rv; rv.result = Limiter::OK;
|
||||
const auto acct_it = _account_data_map.find(account_id.value());
|
||||
AccountData const* account_ptr = (acct_it == _account_data_map.end()) ?
|
||||
@@ -224,19 +240,20 @@ public:
|
||||
return rv;
|
||||
const SecurityLimitTree* sec_tree = _find_security(security.id());
|
||||
bts_assert(sec_tree != nullptr);
|
||||
rv = sec_tree->can_update_order(account_id, side, from, to, enforce_clip);
|
||||
// same logic for the spread legs as above
|
||||
if (rv.ok() && security.has_spread() && sec_tree->is_residual_pos_spread()){
|
||||
for (const api::ContractLeg& leg : security.spread().legs()){
|
||||
api::Side effective_leg_side = side == api::Side::SIDE_BUY ? leg.side() : LimiterUtils::invert_side(leg.side());
|
||||
Quantity effective_leg_from_qty = Quantity (from.value() * std::abs(leg.quantity()));
|
||||
Quantity effective_leg_to_qty = Quantity (to.value() * std::abs(leg.quantity()));
|
||||
rv = can_update_order (account_id, leg.security(), effective_leg_side, effective_leg_from_qty, effective_leg_to_qty,
|
||||
leg.security().has_spread());
|
||||
if (!rv.ok())
|
||||
return rv;
|
||||
}
|
||||
}
|
||||
bool check_pos = from.value() != to.value();
|
||||
// if !check_pos (ie qty did not change) no need to check for
|
||||
// clip and pos limits, for either prod or sec
|
||||
rv = sec_tree->can_update_order(account_id, side, from, to, price,
|
||||
Limiter::EnforcementParams{check_pos, check_pos, true});
|
||||
// if it is a spread we need to check for the legs
|
||||
// violating the prod or sec limits. however if the qty did
|
||||
// not change, then there is no need to
|
||||
if (rv.ok() && check_pos && security.has_spread())
|
||||
return _check_order_update_pos_violations_recursively(security,
|
||||
account_id,
|
||||
side,
|
||||
from,
|
||||
to);
|
||||
return rv;
|
||||
}
|
||||
// test only functions
|
||||
@@ -244,6 +261,10 @@ public:
|
||||
const SecurityLimitNode* test_only_func_find_security_node(AccountID account_id, uint32_t security_id);
|
||||
const ProductLimitNode* test_only_func_find_product_node(AccountID account_id, const api::Product& product);
|
||||
private:
|
||||
// called from the constructor. for all the child securities, associates their
|
||||
// SecurityLimitTree to the parent securty's SecurityLimitTree
|
||||
void _set_parent_child_relationships(
|
||||
const std::map<uint32_t, api::Security const *>&);
|
||||
SecurityLimitTree* _find_security(uint32_t sec_id){
|
||||
auto it = _sec_map.find(sec_id);
|
||||
return it == _sec_map.end() ? nullptr : &it->second;
|
||||
@@ -251,7 +272,7 @@ private:
|
||||
const SecurityLimitTree* _find_security(uint32_t sec_id) const{
|
||||
auto it = _sec_map.find(sec_id);
|
||||
return it == _sec_map.end() ? nullptr : &it->second;
|
||||
}
|
||||
}
|
||||
ProductLimitTree* _find_product_for_security(const api::Security& security);
|
||||
ProductLimitTree* _find_product(const api::Product& product);
|
||||
// for a security, adds the corresponding product tree
|
||||
@@ -320,5 +341,95 @@ private:
|
||||
out_res.error_msg = "Could not find account";
|
||||
}
|
||||
}
|
||||
|
||||
// 07/16/2020
|
||||
|
||||
// adds the working order to the current state.
|
||||
// if s is a spread, it iterates through the legs and
|
||||
// adds the legs with the appropriate side, qty
|
||||
void _add_or_remove_working_order(const api::Security& s,
|
||||
AccountID acc,
|
||||
api::Side side,
|
||||
Quantity qty,
|
||||
bool is_add,
|
||||
bool real_op){
|
||||
LOG(OrderMan, Info,
|
||||
"LimitStore::_add_or_remove_working_order "
|
||||
"real_op:{} is_add:{} sec:{} acc:{} side:{} qty:{} ",
|
||||
real_op, is_add, s.id(), acc, side, qty);
|
||||
SecurityLimitTree* sec_tree = _find_security(s.id());
|
||||
if(is_add)
|
||||
sec_tree->add_working_order(acc,side,qty,real_op);
|
||||
else
|
||||
sec_tree->remove_working_order(acc,side,qty,real_op);
|
||||
if(s.has_spread()){
|
||||
for (const auto& l : s.spread().legs()){
|
||||
const auto& lsec = l.security();
|
||||
api::Side eff_leg_side = side == api::Side::SIDE_BUY ? l.side() :
|
||||
LimiterUtils::invert_side(l.side());
|
||||
Quantity eff_leg_qty = Quantity (qty.value() * std::abs(l.quantity()));
|
||||
_add_or_remove_working_order(lsec,acc,eff_leg_side,eff_leg_qty,is_add,real_op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// for the current state of the positions, check the violations
|
||||
// on the side passed in the arg
|
||||
Limiter::LimiterQueryResult _check_curr_pos_violations_recursively(
|
||||
const api::Security& s,
|
||||
AccountID acc,
|
||||
api::Side side){
|
||||
SecurityLimitTree* sec_tree = _find_security(s.id());
|
||||
auto rv =
|
||||
sec_tree->can_send_new_order(acc,side,Quantity(0),Price(os::nan()),
|
||||
Limiter::EnforcementParams{true,false,false});
|
||||
if(rv.ok()){
|
||||
if(s.has_spread()){
|
||||
for (const auto& l : s.spread().legs()){
|
||||
const auto& lsec = l.security();
|
||||
api::Side eff_leg_side = side == api::Side::SIDE_BUY ? l.side() :
|
||||
LimiterUtils::invert_side(l.side());
|
||||
auto l_rv =_check_curr_pos_violations_recursively(lsec,acc,eff_leg_side);
|
||||
if(!l_rv.ok())
|
||||
return l_rv;
|
||||
}
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
Limiter::LimiterQueryResult _check_new_order_pos_violations_recursively(
|
||||
const api::Security& s,
|
||||
AccountID acc,
|
||||
api::Side side,
|
||||
Quantity qty){
|
||||
_add_or_remove_working_order(s,acc,side,qty,true,false);
|
||||
auto rv = _check_curr_pos_violations_recursively(s,acc,side);
|
||||
_add_or_remove_working_order(s,acc,side,qty,false,false);
|
||||
return rv;
|
||||
}
|
||||
|
||||
Limiter::LimiterQueryResult _check_order_update_pos_violations_recursively(
|
||||
const api::Security& s,
|
||||
AccountID acc,
|
||||
api::Side side,
|
||||
Quantity from,
|
||||
Quantity to){
|
||||
if(to.value() > from.value()){
|
||||
// this is the equivelant of a new order of
|
||||
// qty = to-from, on the same side
|
||||
auto eff_qty = Quantity(to.value()-from.value());
|
||||
auto eff_side = side;
|
||||
return _check_new_order_pos_violations_recursively(s,acc,eff_side,eff_qty);
|
||||
}
|
||||
else{
|
||||
// this is the equivelant of a new order of
|
||||
// qty = from-to, on the opp side
|
||||
auto eff_qty = Quantity(from.value()-to.value());
|
||||
auto eff_side = LimiterUtils::invert_side(side);
|
||||
return _check_new_order_pos_violations_recursively(s,acc,eff_side,eff_qty);
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
}}
|
||||
@@ -42,6 +42,10 @@ void Limiter::set_filled_position(api::Security const &security,
|
||||
// add_securities was called or before add was called
|
||||
// (we can put it in a queue and examine it on the
|
||||
// add_securities call)
|
||||
LOG(OrderMan, Info, "Limiter::set_filled_position sec:{} sym:{} acc:{} "
|
||||
"long:{} short:{}",
|
||||
security.id(),security.symbol(),account_id,
|
||||
long_pos,short_pos);
|
||||
if (!_contains_security(&security))
|
||||
LOG(OrderMan, Error, "Limiter::set_filled_position could not find"
|
||||
"sec_key:{} this should be investigated",
|
||||
@@ -59,6 +63,9 @@ void Limiter::add_to_working_position(api::Security const &security,
|
||||
AccountID account_id,
|
||||
api::Side side,
|
||||
Quantity quantity){
|
||||
LOG(OrderMan, Info, "Limiter::add_to_working_position "
|
||||
"sec:{} acc:{} side:{} qty:{}",
|
||||
security.id(),account_id,side,quantity);
|
||||
if (!_contains_security(&security))
|
||||
add_security(security);
|
||||
if (!me->limit_store->has_account(account_id)) {
|
||||
@@ -71,6 +78,9 @@ void Limiter::remove_from_working_position(api::Security const &security,
|
||||
AccountID account_id,
|
||||
api::Side side,
|
||||
Quantity quantity) {
|
||||
LOG(OrderMan, Info, "Limiter::remove_from_working_position "
|
||||
"sec:{} acc:{} side:{} qty:{}",
|
||||
security.id(),account_id,side,quantity);
|
||||
if (!_contains_security(&security))
|
||||
add_security(security);
|
||||
if (!me->limit_store->has_account(account_id)) {
|
||||
@@ -99,32 +109,33 @@ Limiter::LimiterQueryResult Limiter::can_trade(UserID user_id,
|
||||
return rv;
|
||||
}
|
||||
Limiter::LimiterQueryResult
|
||||
Limiter::can_send_new_order(AccountID account_id, api::Security const &security,
|
||||
api::Side side, Quantity quantity) {
|
||||
Limiter::can_send_new_order(AccountID account_id,
|
||||
api::Security const &security,
|
||||
api::Side side,
|
||||
Quantity quantity,
|
||||
Price price){
|
||||
LimiterQueryResult rv =
|
||||
me->limit_store->can_send_new_order(
|
||||
account_id, security, side, quantity);
|
||||
account_id, security, side, quantity, price);
|
||||
if (rv.result == Limiter::NO_SUCH_SECURITY) {
|
||||
add_security(security);
|
||||
rv = me->limit_store->can_send_new_order(
|
||||
account_id, security, side, quantity);
|
||||
account_id, security, side, quantity, price);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
Limiter::LimiterQueryResult
|
||||
Limiter::can_update_order(AccountID account_id, api::Security const &security,
|
||||
api::Side side, Quantity from, Quantity to) {
|
||||
LimiterQueryResult rv;
|
||||
if (from == to)
|
||||
rv.result = Limiter::OK;
|
||||
else {
|
||||
api::Side side, Quantity from, Quantity to,
|
||||
Price price) {
|
||||
LimiterQueryResult rv = me->limit_store->can_update_order(account_id,
|
||||
security,
|
||||
side,
|
||||
from, to, price);
|
||||
if (rv.result == Limiter::NO_SUCH_SECURITY) {
|
||||
add_security(security);
|
||||
rv = me->limit_store->can_update_order(account_id, security, side, from,
|
||||
to);
|
||||
if (rv.result == Limiter::NO_SUCH_SECURITY) {
|
||||
add_security(security);
|
||||
rv = me->limit_store->can_update_order(account_id, security, side, from,
|
||||
to);
|
||||
}
|
||||
to, price);
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,9 @@ class Limiter {
|
||||
// Set the current *filled* position, i.e. actual number of
|
||||
// contracts owned, for the specified sec and account. Spreads are
|
||||
// not recursively deconstructed into their legs, i.e this sets the
|
||||
// position only for the passed security.
|
||||
// position only for the passed security
|
||||
// (this function is called by the framework for the
|
||||
// the legs)
|
||||
void set_filled_position(api::Security const &, AccountID,
|
||||
Quantity long_pos,
|
||||
Quantity short_pos);
|
||||
@@ -48,11 +50,6 @@ class Limiter {
|
||||
void remove_from_working_position(api::Security const &, AccountID,
|
||||
api::Side, Quantity);
|
||||
|
||||
void update_position(const api::position::PositionUnit &);
|
||||
// this can be a partial list
|
||||
void update_position(
|
||||
const std::vector<api::position::PositionUnit const *> &positions);
|
||||
|
||||
void set_day_pnl(AccountID account_id, double pnl);
|
||||
void set_excess_liquidity(AccountID account_id, double excesss_liquidity);
|
||||
|
||||
@@ -69,7 +66,15 @@ class Limiter {
|
||||
USER_DEACTIVATED,
|
||||
NO_SUCH_USER,
|
||||
LOSS_LIMIT_BREACHED,
|
||||
MARGIN_LIMIT_BREACHED
|
||||
MARGIN_LIMIT_BREACHED,
|
||||
SEC_PRICE_BAND_BREACHED,
|
||||
NO_PARENT_SECURITY
|
||||
};
|
||||
|
||||
struct EnforcementParams{
|
||||
bool pos = true;
|
||||
bool clip = true;
|
||||
bool price_limits = true;
|
||||
};
|
||||
|
||||
struct LimiterQueryResult {
|
||||
@@ -82,10 +87,13 @@ class Limiter {
|
||||
LimiterQueryResult can_trade(UserID user, AccountID into_account) const;
|
||||
LimiterQueryResult can_send_new_order(AccountID into_account,
|
||||
api::Security const &sec,
|
||||
api::Side side, Quantity quantity);
|
||||
api::Side side,
|
||||
Quantity quantity,
|
||||
Price price);
|
||||
LimiterQueryResult can_update_order(AccountID account,
|
||||
api::Security const &sec, api::Side side,
|
||||
Quantity from, Quantity to);
|
||||
Quantity from, Quantity to,
|
||||
Price price);
|
||||
~Limiter();
|
||||
// for testing purposes only
|
||||
LimitStore &limit_store();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <memory>
|
||||
#include "lib/orderman/orderman_utils/orderman_common.h"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "Limiter.h"
|
||||
|
||||
namespace bts{
|
||||
@@ -9,12 +10,52 @@ namespace orderman{
|
||||
|
||||
class LimiterUtils{
|
||||
public:
|
||||
|
||||
static std::string product_string(const bts::api::admin::ProductScope& p){
|
||||
auto _type = [&]() -> std::string {
|
||||
switch(p.security_type()){
|
||||
case api::SecurityType::SECURITYTYPE_UNSET:
|
||||
return "UNSET";
|
||||
case api::SecurityType::SECURITYTYPE_STOCK:
|
||||
return "STOCK";
|
||||
case api::SecurityType::SECURITYTYPE_OPTION:
|
||||
return "OPTION";
|
||||
case api::SecurityType::SECURITYTYPE_FUTURE:
|
||||
return "FUTURE";
|
||||
case api::SecurityType::SECURITYTYPE_SPREAD:
|
||||
return "SPREAD";
|
||||
case api::SecurityType::SECURITYTYPE_SYNTHETIC:
|
||||
return "SYNTHETIC";
|
||||
default:
|
||||
return "UNSET";
|
||||
}
|
||||
return "UNSET";
|
||||
};
|
||||
return p.security_symbol() + " " + _type();
|
||||
}
|
||||
static void can_send_new_order_net(api::Side side,
|
||||
Quantity quantity,
|
||||
bool is_security_level,
|
||||
uint32_t long_pos_limit,
|
||||
uint32_t short_pos_limit,
|
||||
uint32_t long_pos,
|
||||
uint32_t short_pos,
|
||||
const bts::api::admin::ProductScope& p,
|
||||
Limiter::LimiterQueryResult& out_res){
|
||||
int signed_long_pos_limit = static_cast<int>(long_pos_limit),
|
||||
signed_short_pos_limit = -1 * static_cast<int>(short_pos_limit);
|
||||
int net_pos = static_cast<int>(long_pos)-static_cast<int>(short_pos);
|
||||
can_send_new_order_net(side, quantity, is_security_level,
|
||||
signed_long_pos_limit, signed_short_pos_limit,
|
||||
net_pos, p, out_res);
|
||||
}
|
||||
static void can_send_new_order_net(api::Side side,
|
||||
Quantity quantity,
|
||||
bool is_security_level,
|
||||
int long_pos_limit,
|
||||
int short_pos_limit,
|
||||
int net_pos,
|
||||
const bts::api::admin::ProductScope& p,
|
||||
Limiter::LimiterQueryResult& out_res){
|
||||
out_res.result = Limiter::OK;
|
||||
bts_assert(long_pos_limit >= 0);
|
||||
@@ -27,24 +68,49 @@ public:
|
||||
if (net_pos_new > long_pos_limit && side==api::Side::SIDE_BUY){
|
||||
if (is_security_level) {
|
||||
out_res.result = Limiter::SEC_LONG_POS_BREACHED;
|
||||
out_res.error_msg = fmt::format("Security Long Position (Max {} Current {})", long_pos_limit, net_pos);
|
||||
out_res.error_msg =
|
||||
fmt::format("Security Long Position Max:{} Violation:{}",
|
||||
long_pos_limit, net_pos);
|
||||
}
|
||||
else {
|
||||
out_res.result = Limiter::PROD_LONG_POS_BREACHED;
|
||||
out_res.error_msg = fmt::format("Product Long Position (Max {} Current {})", long_pos_limit, net_pos);
|
||||
out_res.error_msg =
|
||||
fmt::format("Product Long Position for {} Max:{} Violation:{}",
|
||||
product_string(p), long_pos_limit, net_pos_new);
|
||||
}
|
||||
}
|
||||
else if (net_pos_new < short_pos_limit && side==api::Side::SIDE_SELL){
|
||||
if (is_security_level) {
|
||||
out_res.result = Limiter::SEC_SHORT_POS_BREACHED;
|
||||
out_res.error_msg = fmt::format("Security Short Position (Max {} Current {})", short_pos_limit, net_pos);
|
||||
out_res.error_msg =
|
||||
fmt::format("Security Short Position Max:{} Violation:{}",
|
||||
short_pos_limit, net_pos);
|
||||
}
|
||||
else {
|
||||
out_res.result = Limiter::PROD_SHORT_POS_BREACHED;
|
||||
out_res.error_msg = fmt::format("Product Short Position (Max {} Current {})", short_pos_limit, net_pos);
|
||||
out_res.error_msg =
|
||||
fmt::format("Product Short Position for {} Max:{} Violation:{}",
|
||||
product_string(p), short_pos_limit, net_pos_new);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
static void can_update_order_net(api::Side side,
|
||||
Quantity from,
|
||||
Quantity to,
|
||||
bool is_security_level,
|
||||
uint32_t long_pos_limit,
|
||||
uint32_t short_pos_limit,
|
||||
uint32_t long_pos,
|
||||
uint32_t short_pos,
|
||||
const bts::api::admin::ProductScope& p,
|
||||
Limiter::LimiterQueryResult& out_res){
|
||||
int signed_long_pos_limit = static_cast<int>(long_pos_limit),
|
||||
signed_short_pos_limit = -1 * static_cast<int>(short_pos_limit);
|
||||
int net_pos = static_cast<int>(long_pos)-static_cast<int>(short_pos);
|
||||
can_update_order_net(side, from, to, is_security_level,
|
||||
signed_long_pos_limit, signed_short_pos_limit,
|
||||
net_pos, p, out_res);
|
||||
}
|
||||
static void can_update_order_net (api::Side side,
|
||||
Quantity from,
|
||||
Quantity to,
|
||||
@@ -52,8 +118,11 @@ public:
|
||||
int long_pos_limit,
|
||||
int short_pos_limit,
|
||||
int net_pos,
|
||||
const bts::api::admin::ProductScope& p,
|
||||
Limiter::LimiterQueryResult& out_res){
|
||||
out_res.result = Limiter::OK;
|
||||
bts_assert(long_pos_limit >= 0);
|
||||
bts_assert(short_pos_limit <= 0);
|
||||
if (to.value() > from.value()){
|
||||
int net_pos_new = 0;
|
||||
uint32_t diff = to.value() - from.value();
|
||||
@@ -64,21 +133,29 @@ public:
|
||||
if (net_pos_new > long_pos_limit){
|
||||
if (is_security_level) {
|
||||
out_res.result = Limiter::SEC_LONG_POS_BREACHED;
|
||||
out_res.error_msg = fmt::format("Security Long Position (Max {} Current {})", long_pos_limit, net_pos);
|
||||
out_res.error_msg =
|
||||
fmt::format("Security Long Position Max:{} Current:{}",
|
||||
long_pos_limit, net_pos);
|
||||
}
|
||||
else {
|
||||
out_res.result = Limiter::PROD_LONG_POS_BREACHED;
|
||||
out_res.error_msg = fmt::format("Product Long Position (Max {} Current {})", long_pos_limit, net_pos);
|
||||
out_res.error_msg =
|
||||
fmt::format("Product Long Position for {} Max:{} Violation:{}",
|
||||
product_string(p), long_pos_limit, net_pos_new);
|
||||
}
|
||||
}
|
||||
else if (net_pos_new < short_pos_limit){
|
||||
if (is_security_level) {
|
||||
out_res.result = Limiter::SEC_SHORT_POS_BREACHED;
|
||||
out_res.error_msg = fmt::format("Security Short Position (Max {} Current {})", short_pos_limit, net_pos);
|
||||
out_res.error_msg =
|
||||
fmt::format("Security Short Position Max:{} Current:{}",
|
||||
short_pos_limit, net_pos);
|
||||
}
|
||||
else {
|
||||
out_res.result = Limiter::SEC_SHORT_POS_BREACHED;
|
||||
out_res.error_msg = fmt::format("Product Short Position (Max {} Current {})", short_pos_limit, net_pos);
|
||||
out_res.error_msg =
|
||||
fmt::format("Product Short Position for {} Max:{} Violation:{}",
|
||||
product_string(p), short_pos_limit, net_pos_new);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,21 +166,22 @@ public:
|
||||
uint32_t short_pos_limit,
|
||||
uint32_t long_pos,
|
||||
uint32_t short_pos,
|
||||
const bts::api::admin::ProductScope& p,
|
||||
Limiter::LimiterQueryResult& out_res){
|
||||
out_res.result = Limiter::OK;
|
||||
if(side == api::Side::SIDE_BUY){
|
||||
if(long_pos + quantity.value() > long_pos_limit){
|
||||
out_res.error_msg = fmt::format("Long Position (Abs) "
|
||||
"(Max {} Current {})",
|
||||
long_pos_limit,long_pos);
|
||||
out_res.error_msg = fmt::format("Long Position (Abs) for {} "
|
||||
"(Max {} {})",
|
||||
product_string(p),long_pos_limit,long_pos + quantity.value());
|
||||
out_res.result = Limiter::SEC_LONG_POS_BREACHED;
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(short_pos + quantity.value() > short_pos_limit){
|
||||
out_res.error_msg = fmt::format("Short Position (Abs) "
|
||||
"(Max {} Current {})",
|
||||
short_pos_limit,short_pos);
|
||||
out_res.error_msg = fmt::format("Short Position (Abs) for {}"
|
||||
"(Max {} {})",
|
||||
product_string(p),short_pos_limit,short_pos + quantity.value());
|
||||
out_res.result = Limiter::SEC_SHORT_POS_BREACHED;
|
||||
}
|
||||
}
|
||||
@@ -115,23 +193,24 @@ public:
|
||||
uint32_t short_pos_limit,
|
||||
uint32_t long_pos,
|
||||
uint32_t short_pos,
|
||||
const bts::api::admin::ProductScope& p,
|
||||
Limiter::LimiterQueryResult& out_res){
|
||||
out_res.result = Limiter::OK;
|
||||
if (to.value() > from.value()){
|
||||
uint32_t diff = to.value() - from.value();
|
||||
if(side == api::Side::SIDE_BUY){
|
||||
if(long_pos + diff > long_pos_limit){
|
||||
out_res.error_msg = fmt::format("Long Position (Abs) "
|
||||
"(Max {} Current {})",
|
||||
long_pos_limit,long_pos);
|
||||
out_res.error_msg = fmt::format("Long Position (Abs) for {}"
|
||||
"(Max {} {})",
|
||||
product_string(p),long_pos_limit,long_pos);
|
||||
out_res.result = Limiter::SEC_LONG_POS_BREACHED;
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(short_pos + diff > short_pos_limit){
|
||||
out_res.error_msg = fmt::format("Short Position (Abs) "
|
||||
"(Max {} Current {})",
|
||||
short_pos_limit,short_pos);
|
||||
out_res.error_msg = fmt::format("Short Position (Abs) for {}"
|
||||
"(Max {} {})",
|
||||
product_string(p),short_pos_limit,short_pos + diff);
|
||||
out_res.result = Limiter::SEC_SHORT_POS_BREACHED;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,17 +46,37 @@ public:
|
||||
_long_pos=source._long_pos;
|
||||
_short_pos=source._short_pos;
|
||||
}
|
||||
void add_position(api::Side side, uint32_t qty){
|
||||
// real_op in both the functions below is for logging purposes only
|
||||
void add_position(api::Side side, uint32_t qty, bool real_op=true){
|
||||
if (side==api::Side::SIDE_BUY)
|
||||
_long_pos += qty;
|
||||
else
|
||||
_short_pos += qty;
|
||||
}
|
||||
void remove_position(api::Side side, uint32_t qty){
|
||||
LOG(OrderMan, Info, "ProductLimitNode::position "
|
||||
"real_op:{} add type:{} sym:{} "
|
||||
"side:{} qty:{} "
|
||||
"long_pos:{} short_pos:{}",
|
||||
real_op,
|
||||
_limit.product().security_type(),
|
||||
_limit.product().security_symbol(),
|
||||
side, qty,
|
||||
_long_pos, _short_pos);
|
||||
|
||||
}
|
||||
void remove_position(api::Side side, uint32_t qty, bool real_op=true){
|
||||
if (side==api::Side::SIDE_BUY)
|
||||
_long_pos = _subtract_unsigned(_long_pos, qty);
|
||||
else
|
||||
_short_pos = _subtract_unsigned(_short_pos, qty);
|
||||
LOG(OrderMan, Info, "ProductLimitNode::position "
|
||||
"real_op:{} remove type:{} sym:{}"
|
||||
"side:{} qty:{} "
|
||||
"long_pos:{} short_pos:{}",
|
||||
real_op,
|
||||
_limit.product().security_type(),
|
||||
_limit.product().security_symbol(),
|
||||
side, qty,
|
||||
_long_pos, _short_pos);
|
||||
}
|
||||
// risk limit checks
|
||||
////////////////////
|
||||
@@ -75,6 +95,7 @@ public:
|
||||
static_cast<int>(_long_pos_limit),
|
||||
-1 * static_cast<int>(_short_pos_limit),
|
||||
net_pos(),
|
||||
_limit.product(),
|
||||
result);
|
||||
else
|
||||
LimiterUtils::can_send_new_order_abs (side,
|
||||
@@ -83,6 +104,7 @@ public:
|
||||
_short_pos_limit,
|
||||
_long_pos,
|
||||
_short_pos,
|
||||
_limit.product(),
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
@@ -103,6 +125,7 @@ public:
|
||||
static_cast<int>(_long_pos_limit),
|
||||
-1 * static_cast<int>(_short_pos_limit),
|
||||
net_pos(),
|
||||
_limit.product(),
|
||||
result);
|
||||
else
|
||||
LimiterUtils::can_update_order_abs (side,
|
||||
@@ -112,6 +135,7 @@ public:
|
||||
_short_pos_limit,
|
||||
_long_pos,
|
||||
_short_pos,
|
||||
_limit.product(),
|
||||
result);
|
||||
return result;
|
||||
}
|
||||
@@ -124,6 +148,9 @@ public:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
const bts::api::admin::ProductScope& product_scope() const{
|
||||
return _limit.product();
|
||||
}
|
||||
// test only functions
|
||||
//////////////////////
|
||||
const bts::api::admin::AccountProductPositionLimitUnit&
|
||||
|
||||
@@ -6,14 +6,25 @@ namespace orderman {
|
||||
SecurityLimitNode::SecurityLimitNode(
|
||||
uint32_t account_id,
|
||||
ProductLimitNode& product_node,
|
||||
uint32_t clip_size)
|
||||
uint32_t prod_clip_size,
|
||||
const bts::api::admin::SecurityRiskLimitUnit& sec_limit)
|
||||
:
|
||||
_account_id(account_id),
|
||||
_clip_size(clip_size),
|
||||
_prod_clip_size(prod_clip_size),
|
||||
_product_node(product_node),
|
||||
_sec_limit(sec_limit),
|
||||
_long_pos(0),
|
||||
_short_pos(0)
|
||||
{}
|
||||
|
||||
_long_working_pos(0),
|
||||
_short_pos(0),
|
||||
_short_working_pos(0),
|
||||
_is_child(false), // all nodes are parents with no child by default
|
||||
_relation(nullptr)
|
||||
{}
|
||||
void SecurityLimitNode::set_parent_child(bool is_child,
|
||||
SecurityLimitNode* relation){
|
||||
_is_child = is_child;
|
||||
_relation = relation;
|
||||
}
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -18,47 +18,198 @@ namespace bts{
|
||||
namespace orderman{
|
||||
|
||||
// Scope : <security, account>
|
||||
// Created by : SecurityLimitTree
|
||||
// Keeps the running position and clip limits
|
||||
// for a single <security, account>.
|
||||
// Has a reference to the ProductLimitNode for the product
|
||||
// that this security belongs to.
|
||||
// Position limits are enforced through the ProductLimitNode
|
||||
// and clip limits are enforced here
|
||||
class SecurityLimitNode{
|
||||
private:
|
||||
const uint32_t _account_id;
|
||||
const uint32_t _clip_size;
|
||||
const uint32_t _prod_clip_size;
|
||||
ProductLimitNode& _product_node;
|
||||
const bts::api::admin::SecurityRiskLimitUnit& _sec_limit;
|
||||
// this is the committed position. working order
|
||||
// adds and removes are passed straight to the
|
||||
// product node. Position updates are changed to
|
||||
// increments and then passed
|
||||
uint32_t _long_pos;
|
||||
uint32_t _short_pos;
|
||||
uint32_t _long_pos, _long_working_pos;
|
||||
uint32_t _short_pos, _short_working_pos;
|
||||
bool _is_child;
|
||||
SecurityLimitNode* _relation;
|
||||
int _net_pos() const {
|
||||
return static_cast<int>(_long_pos)-static_cast<int>(_short_pos);
|
||||
}
|
||||
private:
|
||||
bool _is_parent() const {return !_is_child && _relation != nullptr;}
|
||||
// helper functions
|
||||
uint32_t _eff_long_pos() const{
|
||||
return (_long_pos + _long_working_pos) +
|
||||
(_is_parent() ? _relation->_eff_long_pos() : 0);
|
||||
}
|
||||
uint32_t _eff_short_pos() const{
|
||||
return (_short_pos + _short_working_pos) +
|
||||
(_is_parent() ? _relation->_eff_short_pos() : 0);
|
||||
}
|
||||
uint32_t _subtract_unsigned(uint32_t n1, uint32_t n2){
|
||||
if(n1 >= n2)
|
||||
return n1-n2;
|
||||
else{
|
||||
LOG(OrderMan, Error, "SecurityLimitNode._subtract_unsigned {0}-{1} ",
|
||||
n1, n2);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
void _check_sec_clip (Quantity q,
|
||||
Limiter::LimiterQueryResult& out_res) const{
|
||||
if(_sec_limit.enforce_clip_limits() && q.value()>_sec_limit.clip_size()){
|
||||
out_res.result = Limiter::CLIP_SIZE_BREACHED;
|
||||
out_res.error_msg =
|
||||
fmt::format("Security Clip Size Max:{} Violation:{})",
|
||||
_sec_limit.clip_size(), q.value());
|
||||
}
|
||||
}
|
||||
void _check_prod_clip (Quantity q,
|
||||
Limiter::LimiterQueryResult& out_res) const{
|
||||
if (q.value() > _prod_clip_size){
|
||||
out_res.result = Limiter::CLIP_SIZE_BREACHED;
|
||||
out_res.error_msg =
|
||||
fmt::format("Product Clip Size Limit:{} Violation:{})",
|
||||
_prod_clip_size, q.value());
|
||||
}
|
||||
}
|
||||
void _check_sec_price_band(api::Side s, Price p,
|
||||
Limiter::LimiterQueryResult& out_res) const{
|
||||
if(_sec_limit.enforce_price_bands()){
|
||||
if(s == api::Side::SIDE_BUY){
|
||||
if(p.value() > _sec_limit.max_buy_price()){
|
||||
out_res.result = Limiter::SEC_PRICE_BAND_BREACHED;
|
||||
out_res.error_msg =
|
||||
fmt::format("Price Band Violation Limit:{} Violation:{})",
|
||||
_sec_limit.max_buy_price(), p.value());
|
||||
}
|
||||
}
|
||||
else{
|
||||
if(p.value() < _sec_limit.min_sell_price()){
|
||||
out_res.result = Limiter::SEC_PRICE_BAND_BREACHED;
|
||||
out_res.error_msg =
|
||||
fmt::format("Price Band Violation Limit:{} Violation:{})",
|
||||
_sec_limit.min_sell_price(), p.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
void _check_pos_on_new_order(api::Side side,
|
||||
Quantity quantity,
|
||||
Limiter::LimiterQueryResult& out_res) const{
|
||||
if(_sec_limit.enforce_net_pos_limits()){
|
||||
// sec
|
||||
LimiterUtils::can_send_new_order_net(
|
||||
side,
|
||||
quantity,
|
||||
true,
|
||||
_sec_limit.net_long_position_limit(),
|
||||
_sec_limit.net_short_position_limit(),
|
||||
_eff_long_pos(),
|
||||
_eff_short_pos(),
|
||||
_product_node.product_scope(),
|
||||
out_res);
|
||||
if(!out_res.ok())
|
||||
return;
|
||||
}
|
||||
// prod
|
||||
out_res = _product_node.can_send_new_order(side, quantity);
|
||||
if(!out_res.ok())
|
||||
return;
|
||||
// if its a slave, check with master
|
||||
if(_is_child){
|
||||
// if not master symbol found return with that
|
||||
if(_relation == nullptr){
|
||||
out_res.result = Limiter::LimiterErrorCode::NO_PARENT_SECURITY;
|
||||
out_res.error_msg = "Could not find parent security"; // TODO
|
||||
return;
|
||||
}
|
||||
else
|
||||
out_res = _relation->can_send_new_order(
|
||||
side,quantity,Price(0),
|
||||
Limiter::EnforcementParams{true,false,false});
|
||||
}
|
||||
}
|
||||
void _check_pos_on_order_update(api::Side side,
|
||||
Quantity from,
|
||||
Quantity to,
|
||||
Limiter::LimiterQueryResult& out_res) const{
|
||||
// sec
|
||||
if(_sec_limit.enforce_net_pos_limits()){
|
||||
LimiterUtils::can_update_order_net(side,
|
||||
from,
|
||||
to,
|
||||
true,
|
||||
_sec_limit.net_long_position_limit(),
|
||||
_sec_limit.net_short_position_limit(),
|
||||
_eff_long_pos(),
|
||||
_eff_short_pos(),
|
||||
_product_node.product_scope(),
|
||||
out_res);
|
||||
if(!out_res.ok())
|
||||
return;
|
||||
}
|
||||
// prod
|
||||
out_res = _product_node.can_update_order(side, from, to);
|
||||
if(!out_res.ok())
|
||||
return;
|
||||
// if its a slave, check with master
|
||||
if(_is_child){
|
||||
// if not master symbol found return with that
|
||||
if(_relation == nullptr){
|
||||
out_res.result = Limiter::LimiterErrorCode::NO_PARENT_SECURITY;
|
||||
out_res.error_msg = "Could not find parent security";
|
||||
return;
|
||||
}
|
||||
else
|
||||
out_res = _relation->can_update_order(
|
||||
side, from, to, Price(0),
|
||||
Limiter::EnforcementParams{true,false,false});
|
||||
}
|
||||
}
|
||||
public:
|
||||
SecurityLimitNode(uint32_t account_id,
|
||||
ProductLimitNode& product_limit,
|
||||
uint32_t clip_size);
|
||||
uint32_t prod_clip_size,
|
||||
const bts::api::admin::SecurityRiskLimitUnit& sec_limit);
|
||||
// accessors ///////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
uint32_t account_id() const {
|
||||
return _account_id;
|
||||
}
|
||||
}
|
||||
// position updates ////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
void copy_positions_from(const SecurityLimitNode& source){
|
||||
_long_pos = source._long_pos;
|
||||
_short_pos = source._short_pos;
|
||||
_long_working_pos = source._long_working_pos;
|
||||
_short_working_pos = source._short_working_pos;
|
||||
// this operation will be repeated since multiple
|
||||
// security nodes share the same product node, but
|
||||
// since its a copy (and not as addition) for
|
||||
// simplicity's sake we do this
|
||||
_product_node.copy_positions_from(source._product_node);
|
||||
}
|
||||
void add_working_order(api::Side side, uint32_t qty){
|
||||
_product_node.add_position(side, qty);
|
||||
void add_working_order(api::Side side, uint32_t qty, bool real_op=true){
|
||||
if(side == api::Side::SIDE_BUY)
|
||||
_long_working_pos += qty;
|
||||
else
|
||||
_short_working_pos += qty;
|
||||
_product_node.add_position(side, qty, real_op);
|
||||
}
|
||||
void remove_working_order(api::Side side, uint32_t qty){
|
||||
_product_node.remove_position(side, qty);
|
||||
void remove_working_order(api::Side side, uint32_t qty, bool real_op=true){
|
||||
if(side == api::Side::SIDE_BUY)
|
||||
_long_working_pos = _subtract_unsigned(_long_working_pos,qty);
|
||||
else
|
||||
_short_working_pos = _subtract_unsigned(_short_working_pos,qty);
|
||||
_product_node.remove_position(side, qty, real_op);
|
||||
}
|
||||
void update_position (AccountID account_id,
|
||||
Quantity long_pos,
|
||||
@@ -83,36 +234,65 @@ public:
|
||||
_short_pos-new_short);
|
||||
_short_pos = new_short;
|
||||
}
|
||||
// risk limit checks ///////////////////////////////////////////////////////
|
||||
// parent child hierarchy //////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
void set_parent_child (bool is_child, SecurityLimitNode* relation);
|
||||
// risk limit checks ///////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
Limiter::LimiterQueryResult can_send_new_order(
|
||||
api::Side side,
|
||||
Quantity quantity,
|
||||
bool enforce_clip = true) const{
|
||||
api::Side side,
|
||||
Quantity quantity,
|
||||
Price price,
|
||||
Limiter::EnforcementParams enforcement) const{
|
||||
Limiter::LimiterQueryResult result;result.result= Limiter::OK;
|
||||
if (enforce_clip && quantity.value() > _clip_size){
|
||||
result.result = Limiter::CLIP_SIZE_BREACHED;
|
||||
result.error_msg = fmt::format("Security Clip Size (Max {} Qty {})",
|
||||
_clip_size, quantity.value());
|
||||
return result;
|
||||
// clip
|
||||
if(enforcement.clip){
|
||||
// sec
|
||||
_check_sec_clip(quantity, result);
|
||||
if(!result.ok())
|
||||
return result;
|
||||
// prod
|
||||
_check_prod_clip(quantity, result);
|
||||
if(!result.ok())
|
||||
return result;
|
||||
}
|
||||
if (result.ok())
|
||||
result = _product_node.can_send_new_order(side, quantity);
|
||||
//price band
|
||||
if(enforcement.price_limits){
|
||||
// only sec level price band limits
|
||||
_check_sec_price_band(side, price, result);
|
||||
if(!result.ok())
|
||||
return result;
|
||||
}
|
||||
// pos limit
|
||||
if(enforcement.pos)
|
||||
_check_pos_on_new_order(side,quantity,result);
|
||||
return result;
|
||||
}
|
||||
Limiter::LimiterQueryResult can_update_order(api::Side side,
|
||||
Quantity from,
|
||||
Quantity to,
|
||||
bool enforce_clip = true) const{
|
||||
Limiter::LimiterQueryResult can_update_order(
|
||||
api::Side side,
|
||||
Quantity from,
|
||||
Quantity to,
|
||||
Price price,
|
||||
Limiter::EnforcementParams enforcement) const{
|
||||
Limiter::LimiterQueryResult result;result.result=Limiter::OK;
|
||||
if (enforce_clip && to.value() > _clip_size){
|
||||
result.result = Limiter::CLIP_SIZE_BREACHED;
|
||||
result.error_msg = fmt::format("Security Clip Size (Max {} Qty {})",
|
||||
_clip_size, to.value());
|
||||
return result;
|
||||
// clip lim
|
||||
if(enforcement.clip){
|
||||
// sec
|
||||
_check_sec_clip(to, result);
|
||||
if(!result.ok())
|
||||
return result;
|
||||
// prod
|
||||
_check_prod_clip(to, result);
|
||||
if(!result.ok())
|
||||
return result;
|
||||
}
|
||||
if (result.ok())
|
||||
result = _product_node.can_update_order(side, from, to);
|
||||
// sec price band
|
||||
_check_sec_price_band(side, price, result);
|
||||
if(!result.ok())
|
||||
return result;
|
||||
// pos limit
|
||||
if(enforcement.pos)
|
||||
_check_pos_on_order_update(side,from,to,result);
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -7,14 +7,31 @@ using namespace bts::admin;
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
SecurityLimitTree::QtyNormalization get_qty_norm (const api::Security& s){
|
||||
if(s.has_ice())
|
||||
return SecurityLimitTree::QtyNormalization
|
||||
{
|
||||
s.ice().exchange_qty_mult(),
|
||||
bts::securities::get_risk_qty(s)
|
||||
};
|
||||
else
|
||||
return SecurityLimitTree::QtyNormalization {1,1};
|
||||
}
|
||||
|
||||
SecurityLimitTree::SecurityLimitTree(
|
||||
const api::Security &security,
|
||||
ProductLimitTree &product_tree,
|
||||
const AdminData &admin_data)
|
||||
const AdminData &admin_data,
|
||||
const bts::api::admin::SecurityRiskLimitUnit& bypass_sec_limit)
|
||||
:
|
||||
_sec_key(security.id()),
|
||||
_is_residual_pos_spread(
|
||||
bts::admin::AdminServiceUtils::residual_pos_spread(security))
|
||||
{
|
||||
bts::admin::AdminServiceUtils::residual_pos_spread(security)),
|
||||
_qty_norm(get_qty_norm(security)){
|
||||
LOG(OrderMan,Info,"SecurityLimitTree sec:{} qty_ratio:{} risk_qty:{}",
|
||||
security.id(),
|
||||
_qty_norm.exchange_qty_mult,
|
||||
_qty_norm.unit_risk_qty);
|
||||
for (const auto &account : admin_data.accounts()) {
|
||||
ProductLimitNode *product_node_ptr =
|
||||
product_tree.find_account(account.id());
|
||||
@@ -22,11 +39,31 @@ SecurityLimitTree::SecurityLimitTree(
|
||||
uint32_t clip_size =
|
||||
AdminServiceUtils::find_account_security_clip_size(account,
|
||||
security);
|
||||
SecurityRiskLimitUnit const* sec_lim = AdminServiceUtils::get_sec_limit(
|
||||
admin_data,
|
||||
account.id(),
|
||||
security.id());
|
||||
_tree.emplace_back(
|
||||
account.id(),
|
||||
*product_node_ptr,
|
||||
clip_size);
|
||||
clip_size,
|
||||
sec_lim == nullptr ? bypass_sec_limit : *sec_lim);
|
||||
}
|
||||
}
|
||||
void SecurityLimitTree::set_parent_child (bool is_child,
|
||||
SecurityLimitTree* relation){
|
||||
if(relation != nullptr){
|
||||
bts_assert(relation->_tree.size()==_tree.size());
|
||||
for (auto& node : _tree){
|
||||
SecurityLimitNode* other_node =
|
||||
relation->_find_account(node.account_id());
|
||||
bts_assert(other_node != nullptr);
|
||||
node.set_parent_child(is_child, other_node);
|
||||
}
|
||||
}
|
||||
else
|
||||
for (auto& node : _tree)
|
||||
node.set_parent_child(is_child, nullptr);
|
||||
}
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -14,37 +14,82 @@
|
||||
namespace bts{
|
||||
namespace orderman{
|
||||
// Scope : <security>
|
||||
// this keeps the state a single security for all accounts
|
||||
// Created by : LimitStore
|
||||
// this keeps the state of a single security for all accounts
|
||||
// The only state is a vector of SecurityLimitNode (which keeps
|
||||
// the actual data)
|
||||
class SecurityLimitTree{
|
||||
public:
|
||||
// This is to handle the condition when the risk control
|
||||
// is entered in different terms than what the order management
|
||||
// system (and exchange) deal with.
|
||||
// This is a 2 step process
|
||||
// exchange_qty_mult : exchange deals with qty's that are
|
||||
// different than what the ui (both trading and risk)
|
||||
// deals with. for example HNG at ICE
|
||||
// is multiple of 2500, but the ui shows it
|
||||
// as 1, 5000 as 2 etc.
|
||||
// unit_risk_qty : this is purely for risk reasons. for example
|
||||
// the risk for HNG is entered in terms of the ICE lot,
|
||||
// for HHD it is entered in terms of the daily future etc.
|
||||
struct QtyNormalization{
|
||||
const uint32_t exchange_qty_mult = 1;
|
||||
const uint32_t unit_risk_qty = 1;
|
||||
};
|
||||
private:
|
||||
const SecurityKey _sec_key;
|
||||
const bool _is_residual_pos_spread;
|
||||
const QtyNormalization _qty_norm;
|
||||
std::vector<SecurityLimitNode> _tree;
|
||||
public:
|
||||
public:
|
||||
SecurityLimitTree(
|
||||
const api::Security& security,
|
||||
ProductLimitTree& product_tree,
|
||||
const bts::api::admin::AdminData& admin_data);
|
||||
const bts::api::admin::AdminData& admin_data,
|
||||
const bts::api::admin::SecurityRiskLimitUnit& bypass_sec_limit);
|
||||
// position updates ////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// real_op is used for logging purposes in all the functions below
|
||||
void add_working_order(AccountID account_id,
|
||||
api::Side side,
|
||||
Quantity quantity){
|
||||
Quantity quantity_arg,
|
||||
bool real_op=true){
|
||||
auto quantity = _convert_qty(quantity_arg);
|
||||
LOG(OrderMan, Info, "SecurityLimitTree::add_working_order "
|
||||
"real_op:{} sec:{} acc:{} "
|
||||
"side:{} qty_arg:{} qty:{}",
|
||||
real_op, _sec_key, account_id,
|
||||
side, quantity_arg, quantity);
|
||||
SecurityLimitNode* node_ptr = _find_account(account_id.value());
|
||||
bts_assert(node_ptr != nullptr);
|
||||
node_ptr->add_working_order(side, quantity.value());
|
||||
node_ptr->add_working_order(side, quantity.value(),real_op);
|
||||
}
|
||||
void remove_working_order(AccountID account_id,
|
||||
api::Side side,
|
||||
Quantity quantity){
|
||||
Quantity quantity_arg,
|
||||
bool real_op=true){
|
||||
auto quantity = _convert_qty(quantity_arg);
|
||||
LOG(OrderMan, Info, "SecurityLimitTree::remove_working_order "
|
||||
"real_op:{} sec:{} acc:{} "
|
||||
"side:{} qty_arg:{} qty:{}",
|
||||
real_op, _sec_key, account_id,
|
||||
side, quantity_arg, quantity);
|
||||
SecurityLimitNode* account_ptr = _find_account(account_id.value());
|
||||
bts_assert(account_ptr != nullptr);
|
||||
account_ptr->remove_working_order(side, quantity.value());
|
||||
account_ptr->remove_working_order(side, quantity.value(),real_op);
|
||||
}
|
||||
void update_position (AccountID account_id,
|
||||
Quantity long_pos,
|
||||
Quantity short_pos){
|
||||
Quantity long_pos_arg,
|
||||
Quantity short_pos_arg){
|
||||
auto long_pos = _convert_qty(long_pos_arg);
|
||||
auto short_pos = _convert_qty(short_pos_arg);
|
||||
LOG(OrderMan, Info, "SecurityLimitTree::update_position "
|
||||
"sec:{} acc:{} "
|
||||
"long_pos_arg:{} long_pos:{} "
|
||||
"short_pos_arg:{} short_pos:{} ",
|
||||
_sec_key, account_id,
|
||||
long_pos_arg, long_pos,
|
||||
short_pos_arg, short_pos);
|
||||
SecurityLimitNode* account_ptr = _find_account(account_id.value());
|
||||
bts_assert(account_ptr != nullptr);
|
||||
account_ptr->update_position(account_id, long_pos, short_pos);
|
||||
@@ -59,26 +104,34 @@ public:
|
||||
node.copy_positions_from(*source_node_ptr);
|
||||
}
|
||||
}
|
||||
// parent child hierarchy //////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
void set_parent_child (bool is_child, SecurityLimitTree* ms_relation);
|
||||
// risk limit checks ///////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
Limiter::LimiterQueryResult can_send_new_order(AccountID account_id,
|
||||
api::Side side,
|
||||
Quantity quantity,
|
||||
bool enforce_clip = true)
|
||||
Price price,
|
||||
Limiter::EnforcementParams enforcement)
|
||||
const{
|
||||
quantity = _convert_qty(quantity);
|
||||
const SecurityLimitNode* node_ptr = _find_account(account_id.value());
|
||||
bts_assert(node_ptr!=nullptr);
|
||||
return node_ptr->can_send_new_order(side, quantity, enforce_clip);
|
||||
return node_ptr->can_send_new_order(side, quantity, price, enforcement);
|
||||
}
|
||||
Limiter::LimiterQueryResult can_update_order(AccountID account_id,
|
||||
api::Side side,
|
||||
Quantity from,
|
||||
Quantity to,
|
||||
bool enforce_clip = true)
|
||||
Price price,
|
||||
Limiter::EnforcementParams enforcement)
|
||||
const{
|
||||
from = _convert_qty(from);
|
||||
to = _convert_qty(to);
|
||||
const SecurityLimitNode* node_ptr = _find_account(account_id.value());
|
||||
bts_assert(node_ptr!=nullptr);
|
||||
return node_ptr->can_update_order(side, from, to, enforce_clip);
|
||||
return node_ptr->can_update_order(side, from, to, price, enforcement);
|
||||
}
|
||||
// accessors ///////////////////////////////////////////////////////////////
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
@@ -101,5 +154,8 @@ private:
|
||||
return &node;
|
||||
return nullptr;
|
||||
}
|
||||
Quantity _convert_qty (Quantity qty) const{
|
||||
return Quantity(qty.value() *_qty_norm.unit_risk_qty);
|
||||
}
|
||||
};
|
||||
}}
|
||||
@@ -399,7 +399,7 @@ NativeOrder::_on_activate(log::Level reject_level, log::Level transition_level,
|
||||
// can we trade this security, with this size?
|
||||
lqr = me->services->limiter().can_send_new_order(
|
||||
AccountID(me->definition.account_id()), *me->sec, me->definition.side(),
|
||||
q);
|
||||
q, p);
|
||||
|
||||
if (!lqr.ok()) {
|
||||
LOG(OrderMan, reject_level,
|
||||
@@ -602,7 +602,8 @@ OrderOpResult NativeOrder::on_update(api::orderman::Update const &update) {
|
||||
auto lqr = me->services->limiter().can_update_order(
|
||||
AccountID(me->definition.account_id()), *me->sec,
|
||||
me->definition.side(), Quantity(previous_working_qty),
|
||||
Quantity(current_working_qty));
|
||||
Quantity(current_working_qty),
|
||||
Price(p));
|
||||
|
||||
// are we under the position limits?
|
||||
if (!lqr.ok()) {
|
||||
|
||||
@@ -79,6 +79,13 @@ struct ICENativeOrder::PImpl
|
||||
int buffered_tries = 0;
|
||||
};
|
||||
|
||||
// on outgoing order, converts the qty to exchange terms
|
||||
// (ex HNG that is represented as 1 in the system and
|
||||
// multiples of 2500 at ICE)
|
||||
Quantity to_exchange_qty (const api::Security& sec, Quantity internal_qty){
|
||||
return bts::securities::to_exchange_qty(internal_qty,sec);
|
||||
}
|
||||
|
||||
ICENativeOrder::ICENativeOrder() : me(new PImpl()) {}
|
||||
|
||||
const OrderDefinition& ICENativeOrder::definition() const
|
||||
@@ -349,10 +356,10 @@ void ICENativeOrder::warm_up_activate(OrderServicesData &data, OrderServices &se
|
||||
|
||||
OrderOpResult
|
||||
ICENativeOrder::_on_activate(log::Level reject_level, log::Level transition_level,
|
||||
Price p, Quantity q, bts::orderman::OrderServices &services)
|
||||
Price p, Quantity activate_q, bts::orderman::OrderServices &services)
|
||||
{
|
||||
me->state.set_price(p.value());
|
||||
me->state.set_quantity(q.value());
|
||||
me->state.set_quantity(activate_q.value());
|
||||
me->state.set_filled_quantity(0);
|
||||
me->state.set_last_price(0);
|
||||
me->state.set_avg_price(0);
|
||||
@@ -387,7 +394,7 @@ ICENativeOrder::_on_activate(log::Level reject_level, log::Level transition_leve
|
||||
}
|
||||
|
||||
// can we trade this security, with this size?
|
||||
lqr = me->services->limiter().can_send_new_order( AccountID(me->definition.account_id()), *me->sec, me->definition.side(), q );
|
||||
lqr = me->services->limiter().can_send_new_order( AccountID(me->definition.account_id()), *me->sec, me->definition.side(), activate_q, p);
|
||||
|
||||
if (!lqr.ok())
|
||||
{
|
||||
@@ -444,8 +451,8 @@ ICENativeOrder::_on_activate(log::Level reject_level, log::Level transition_leve
|
||||
bts_assert(false);
|
||||
break;
|
||||
}
|
||||
|
||||
me->create_quantity_bm = encoding.overwrite(me->create_quantity_bm, q.value(), quantity_sz());
|
||||
auto exchange_qty = to_exchange_qty(*me->sec,activate_q);
|
||||
me->create_quantity_bm = encoding.overwrite(me->create_quantity_bm, exchange_qty.value(), quantity_sz());
|
||||
const auto &ts = services.get_time_str();
|
||||
me->create_transact_time_bm = encoding.overwrite(me->create_transact_time_bm, &ts.value()[0], ts.value().size());
|
||||
return OrderOpResult{};
|
||||
@@ -550,7 +557,7 @@ OrderOpResult ICENativeOrder::on_update(Price p, Quantity q)
|
||||
OrderOpResult ICENativeOrder::on_update(api::orderman::Update const &update)
|
||||
{
|
||||
auto p = Price(update.price());
|
||||
auto q = Quantity(update.quantity());
|
||||
auto update_q = Quantity(update.quantity());
|
||||
auto scoped_log = me->services->log_scope("ICENativeOrder::on_update", key().value());
|
||||
auto result = OrderOpResult{};
|
||||
|
||||
@@ -563,12 +570,12 @@ OrderOpResult ICENativeOrder::on_update(api::orderman::Update const &update)
|
||||
previous_working_qty = me->state.quantity() - me->state.filled_quantity();
|
||||
|
||||
auto current_working_qty = 0;
|
||||
if (q.value() > me->state.filled_quantity())
|
||||
current_working_qty = q.value() - me->state.filled_quantity();
|
||||
if (update_q.value() > me->state.filled_quantity())
|
||||
current_working_qty = update_q.value() - me->state.filled_quantity();
|
||||
|
||||
auto lqr = me->services->limiter().can_update_order( AccountID(me->definition.account_id()), *me->sec,
|
||||
me->definition.side(), Quantity(previous_working_qty),
|
||||
Quantity(current_working_qty));
|
||||
Quantity(current_working_qty),Price(p));
|
||||
|
||||
// are we under the position limits?
|
||||
if (!lqr.ok())
|
||||
@@ -606,7 +613,8 @@ OrderOpResult ICENativeOrder::on_update(api::orderman::Update const &update)
|
||||
}
|
||||
me->update_price_bm = encoding.overwrite(me->update_price_bm, securities::encode_exchange_price(*me->sec, p).value(),
|
||||
me->price_encoding_precision, price_sz());
|
||||
me->update_quantity_bm = encoding.overwrite(me->update_quantity_bm, q.value(), quantity_sz());
|
||||
auto exchange_qty = to_exchange_qty(*me->sec,update_q);
|
||||
me->update_quantity_bm = encoding.overwrite(me->update_quantity_bm, exchange_qty.value(), quantity_sz());
|
||||
|
||||
const auto &ts = me->services->get_time_str();
|
||||
me->update_transact_time_bm = encoding.overwrite( me->update_transact_time_bm, &ts.value()[0], ts.value().size() );
|
||||
@@ -622,8 +630,8 @@ OrderOpResult ICENativeOrder::on_update(api::orderman::Update const &update)
|
||||
|
||||
LOG(OrderMan, Info,
|
||||
"ICE may not honor this update if we recently issued an unacknowledged cancelreplace {}. Update: {}@{}. Buffering",
|
||||
key().value(), q.value(), p.value());
|
||||
me->buffered_update.emplace(p, q);
|
||||
key().value(), update_q.value(), p.value());
|
||||
me->buffered_update.emplace(p, update_q);
|
||||
|
||||
// early return so we skip report_state (for speed)
|
||||
return OrderOpResult{};
|
||||
@@ -633,8 +641,8 @@ OrderOpResult ICENativeOrder::on_update(api::orderman::Update const &update)
|
||||
case ORDER_STATUS_PENDING_WORKING:
|
||||
LOG(OrderMan, Warn,
|
||||
"Cannot apply update to order {}. Update: {}@{}. Buffering",
|
||||
key().value(), q.value(), p.value());
|
||||
me->buffered_update.emplace(p, q);
|
||||
key().value(), update_q.value(), p.value());
|
||||
me->buffered_update.emplace(p, update_q);
|
||||
break;
|
||||
|
||||
case ORDER_STATUS_REJECTED_BY_SPARK:
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
#include "DormanAlgo.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/Book.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoServices.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/orderman/orders/algo/utils/Accumulator.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace dorman {
|
||||
|
||||
using namespace api::orderman;
|
||||
|
||||
struct FutureQty {
|
||||
Quantity size;
|
||||
TimeStamp when;
|
||||
};
|
||||
|
||||
struct DormanAlgo::Impl {
|
||||
IAlgoServices *algo = nullptr;
|
||||
AlgoParams params;
|
||||
api::Side side;
|
||||
Quantity exposed_qty = {};
|
||||
std::optional<FutureQty> next_size;
|
||||
|
||||
OrderDefinition splitter_definition;
|
||||
OrderKey child_id;
|
||||
ChildState child_state;
|
||||
|
||||
uint32_t fills ={};
|
||||
|
||||
std::vector<OrderKey> profit_orders;
|
||||
std::optional<Accumulator> trade_accumulator;
|
||||
|
||||
std::optional<TimeStamp> pause_until;
|
||||
};
|
||||
|
||||
DormanAlgo::DormanAlgo() :
|
||||
me(new Impl())
|
||||
{ }
|
||||
DormanAlgo::~DormanAlgo() { }
|
||||
|
||||
OrderOpResult DormanAlgo::do_before_launch(IBeforeLaunchAlgoServices& ibl) {
|
||||
const auto& def = ibl.definition();
|
||||
// Validation
|
||||
bts_assert(def.has_custom_def());
|
||||
bts_assert(def.custom_def().has_dynamic());
|
||||
|
||||
auto params = to_dorman_params(def.custom_def().dynamic());
|
||||
if (auto error = std::get_if<OrderOpResult>(¶ms)) {
|
||||
return *error;
|
||||
}
|
||||
me->params = std::get<AlgoParams>(params);
|
||||
me->side = def.side();
|
||||
if (me->side != api::Side::SIDE_BUY && me->side != api::Side::SIDE_SELL) {
|
||||
return OrderOpResult { ORDER_ERROR_INVALID_ORDER_DEFINITON, "Invalid side" };
|
||||
}
|
||||
|
||||
if (me->params.trade_accumulation) {
|
||||
auto& t = me->params.trade_accumulation.value();
|
||||
me->trade_accumulator = {Accumulator {t.contracts, t.seconds, ibl.get_time()}};
|
||||
}
|
||||
|
||||
CustomOrderDefinition default_order;
|
||||
default_order.mutable_limit();
|
||||
auto child_def =
|
||||
get_if<CustomOrderDefinition>(def.custom_def().dynamic().order_params(), "Child")
|
||||
.value_or(default_order);
|
||||
|
||||
update_child_template(ibl, child_def);
|
||||
|
||||
if (ibl.is_attaching()) {
|
||||
return OrderOpResult{ORDER_ERROR_OP_NOT_SUPPORTED,
|
||||
"This algo does not support attach."};
|
||||
}
|
||||
|
||||
return {/*ok*/};
|
||||
}
|
||||
|
||||
|
||||
void DormanAlgo::do_launch(IAlgoServices& algo) {
|
||||
me->algo = &algo;
|
||||
auto skey = SecurityKey(algo.security().id());
|
||||
algo.subscribe_book(skey);
|
||||
algo.subscribe_timer();
|
||||
if (me->trade_accumulator) {
|
||||
algo.subscribe_ticker(skey);
|
||||
}
|
||||
}
|
||||
|
||||
void DormanAlgo::do_launch_attached(IAlgoServices&, OrderKey, ChildState) {
|
||||
bts_assert(false);
|
||||
}
|
||||
void DormanAlgo::do_update(const api::orderman::Update& update) {
|
||||
LOG(OrderMan, Diag, "Dorman order {} receiving update: {}",
|
||||
algo().order_key().value(), utils::pb::to_string(update));
|
||||
|
||||
if (update.has_custom()) {
|
||||
if (!update.custom().has_dynamic()) {
|
||||
algo().cancel("Update cannot change order type");
|
||||
return;
|
||||
}
|
||||
auto params = to_dorman_params(update.custom().dynamic());
|
||||
|
||||
if (auto error = std::get_if<OrderOpResult>(¶ms)) {
|
||||
algo().cancel(error->error_msg);
|
||||
}
|
||||
|
||||
me->params = std::get<AlgoParams>(params);
|
||||
auto skey = SecurityKey(algo().security().id());
|
||||
if (me->params.trade_accumulation && !me->trade_accumulator) {
|
||||
algo().subscribe_ticker(skey);
|
||||
auto& t = me->params.trade_accumulation.value();
|
||||
me->trade_accumulator = {Accumulator {t.contracts, t.seconds, algo().get_time()}};
|
||||
}
|
||||
else if (!me->params.trade_accumulation && me->trade_accumulator) {
|
||||
algo().unsubscribe_ticker(skey);
|
||||
me->trade_accumulator = std::nullopt;
|
||||
}
|
||||
else if (me->params.trade_accumulation &&
|
||||
me->trade_accumulator &&
|
||||
(me->trade_accumulator.value().threshold != me->params.trade_accumulation.value().contracts ||
|
||||
me->trade_accumulator.value().bucket_interval != std::chrono::seconds(me->params.trade_accumulation.value().seconds))
|
||||
) {
|
||||
// accumulator params have changed
|
||||
auto& t = me->params.trade_accumulation.value();
|
||||
me->trade_accumulator = {Accumulator {t.contracts, t.seconds, algo().get_time()}};
|
||||
}
|
||||
|
||||
// Check for child
|
||||
auto child = get_if<CustomOrderDefinition>(update.custom().dynamic().order_params(), "Child");
|
||||
if (child) {
|
||||
update_child_template(algo(), child.value());
|
||||
}
|
||||
}
|
||||
|
||||
if (me->fills >= update.quantity()) {
|
||||
complete();
|
||||
}
|
||||
else {
|
||||
// In case of price/qty change always update the child
|
||||
// NOTE: it might be useful to cache the available market data size
|
||||
// and update based on that if the update contains a size table change
|
||||
update_child(me->exposed_qty);
|
||||
}
|
||||
}
|
||||
|
||||
void DormanAlgo::on_child_change(OrderKey key, const ChildState& state) {
|
||||
if (key != me->child_id) return;
|
||||
|
||||
if (state.status == ChildStatus::CANCELED && !is_paused()) {
|
||||
algo().cancel("Unexpected child cancel");
|
||||
return;
|
||||
}
|
||||
|
||||
auto delta = state.total_fills.value() - me->child_state.total_fills.value();
|
||||
if (delta != 0) {
|
||||
me->fills += delta;
|
||||
algo().update_fill_state(Quantity {me->fills}, state.last_price);
|
||||
send_profit_order(state.last_price, Quantity {delta});
|
||||
}
|
||||
me->child_state = state;
|
||||
|
||||
if (me->params.scratch &&
|
||||
me->fills >= me->params.scratch.value().max_overfill) {
|
||||
do_scratch();
|
||||
}
|
||||
|
||||
if (me->fills >= algo().quantity().value() || state.status == ChildStatus::COMPLETED) {
|
||||
complete();
|
||||
}
|
||||
}
|
||||
|
||||
void DormanAlgo::on_book(SecurityKey key, const Book& book) {
|
||||
if (key.value() != me->algo->security().id()) { return; }
|
||||
auto price = me->algo->price();
|
||||
auto tick = me->algo->security().contract_spec().low_tick();
|
||||
|
||||
// Determine available size on the market
|
||||
auto& side = book.side(me->side);
|
||||
uint32_t available = 0;
|
||||
for (const auto& level : side) {
|
||||
if (securities::test_price_equality(level.price(), price.value(), tick)) {
|
||||
available += level.size();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Check the step_table
|
||||
auto target = Quantity {0};
|
||||
if (available > 0) {
|
||||
for (const auto& entry : me->params.steps.step_table) {
|
||||
if (std::get<0>(entry) > available) {
|
||||
break;
|
||||
}
|
||||
else {
|
||||
target = Quantity { std::get<1>(entry) };
|
||||
}
|
||||
}
|
||||
}
|
||||
target = Quantity { std::min(target.value(), me->params.steps.maximum) };
|
||||
|
||||
// First on_book
|
||||
if (!child_active()) {
|
||||
update_child(target);
|
||||
}
|
||||
// See if we need to adjust
|
||||
else {
|
||||
if (target == me->exposed_qty) {
|
||||
me->next_size = std::nullopt;
|
||||
}
|
||||
// If target < size reduce immediately
|
||||
else if (target < me->exposed_qty) {
|
||||
update_child(target);
|
||||
me->next_size = std::nullopt;
|
||||
}
|
||||
// if target > size schedule increase
|
||||
else if (target > me->exposed_qty) {
|
||||
if (me->next_size) {
|
||||
// already planning to increase, update the target
|
||||
me->next_size.value().size = target;
|
||||
}
|
||||
else {
|
||||
// no existing scheduled increase, create one now
|
||||
me->next_size = { FutureQty { target, me->algo->get_time() + std::chrono::seconds(me->params.steps.trail_seconds) } };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
void DormanAlgo::on_timer() {
|
||||
// Check for pending size changes
|
||||
auto now = algo().get_time();
|
||||
if (me->pause_until &&
|
||||
now >= me->pause_until.value()) {
|
||||
|
||||
resume();
|
||||
}
|
||||
if (me->next_size &&
|
||||
now >= me->next_size.value().when) {
|
||||
auto size = me->next_size.value().size;
|
||||
update_child(size);
|
||||
me->next_size = std::nullopt;
|
||||
}
|
||||
}
|
||||
void DormanAlgo::on_ticker(SecurityKey, const TradeVector& vec) {
|
||||
if (me->trade_accumulator) {
|
||||
size_t contracts = 0;
|
||||
for (const auto& t : vec) {
|
||||
contracts += t.quantity.value();
|
||||
}
|
||||
auto& acc = me->trade_accumulator.value();
|
||||
acc.push_back(algo().get_time(), contracts);
|
||||
if (acc.exceeded()) {
|
||||
auto delay = std::chrono::seconds(me->params.trade_accumulation.value().sleep_seconds);
|
||||
pause_for(delay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IAlgoOrder::OnCancelResponse DormanAlgo::on_cancel() {
|
||||
// detach any remaining profit orders so they keep running
|
||||
for(const auto& key : me->profit_orders) {
|
||||
algo().detach_child(key);
|
||||
}
|
||||
return OnCancelResponse::HONOR;
|
||||
}
|
||||
|
||||
IAlgoServices& DormanAlgo::algo() const { return *me->algo; }
|
||||
|
||||
//
|
||||
// private
|
||||
|
||||
void DormanAlgo::update_child(Quantity exposed) {
|
||||
auto& splitter = *me->splitter_definition.mutable_custom_def()->mutable_splitter();
|
||||
splitter.set_max_order_qty(me->params.clip_limit);
|
||||
me->exposed_qty = exposed;
|
||||
splitter.set_exposed_qty(is_paused() ? 0 : exposed.value());
|
||||
|
||||
if (!child_active()) {
|
||||
// launch child
|
||||
me->child_id = algo().launch_child(me->splitter_definition, algo().price(), algo().quantity());
|
||||
}
|
||||
else {
|
||||
Update update;
|
||||
update.set_price(algo().price().value());
|
||||
update.set_quantity(algo().quantity().value());
|
||||
*update.mutable_custom()->mutable_splitter() = splitter;
|
||||
algo().update_child(me->child_id, update);
|
||||
}
|
||||
}
|
||||
|
||||
void DormanAlgo::update_child_template(IBeforeLaunchAlgoServices& ibl, const CustomOrderDefinition& child) {
|
||||
auto child_def = ibl.definition();
|
||||
// Insert a spliter between this and the configured child definition.
|
||||
api::orderman::SplitterOrderDefinition& splitter_def = *child_def.mutable_custom_def()->mutable_splitter();
|
||||
*splitter_def.mutable_child() = child;
|
||||
me->splitter_definition = child_def;
|
||||
}
|
||||
|
||||
Quantity DormanAlgo::exposed_qty() const { return me->exposed_qty; }
|
||||
|
||||
bool DormanAlgo::child_active() const { return me->child_id.value() != 0; }
|
||||
|
||||
void DormanAlgo::do_scratch() {
|
||||
// When the # of fills exceeds the max overfill quantity
|
||||
// We enter fills opposite fills at the fill price and terminate
|
||||
// Use a splitter to bypass clip limit if necessary
|
||||
|
||||
OrderDefinition def = me->splitter_definition;
|
||||
def.set_side(securities::other_side(me->side));
|
||||
def.mutable_custom_def()->mutable_splitter()->set_exposed_qty(me->fills);
|
||||
|
||||
auto price = me->child_state.last_price;
|
||||
auto scratch_id = algo().launch_child(def, price, Quantity { me->fills }, false);
|
||||
|
||||
algo().detach_child(scratch_id);
|
||||
complete();
|
||||
}
|
||||
|
||||
void DormanAlgo::send_profit_order(Price p, Quantity q) {
|
||||
if (me->params.profit_orders) {
|
||||
auto side = securities::other_side(me->side);
|
||||
Price offset =
|
||||
Price { algo().security().contract_spec().low_tick() * me->params.profit_orders.value().ticks_away };
|
||||
auto price =
|
||||
securities::worsen_price(side, p, offset);
|
||||
|
||||
OrderDefinition def = me->splitter_definition;
|
||||
// BTS Simulator does not currently support GTC
|
||||
// Note: This could be made a configurable order type in the algo parameters
|
||||
def.set_time_in_force(TimeInForce::TIME_IN_FORCE_DAY);
|
||||
def.set_side(side);
|
||||
def.mutable_custom_def()->mutable_limit();
|
||||
me->profit_orders.push_back(
|
||||
algo().launch_child(def, price, q, false));
|
||||
}
|
||||
}
|
||||
|
||||
void DormanAlgo::complete() {
|
||||
// Allow profit orders to keep running
|
||||
if (me->params.profit_orders) {
|
||||
for(const auto& key : me->profit_orders) {
|
||||
algo().detach_child(key);
|
||||
}
|
||||
}
|
||||
algo().complete();
|
||||
}
|
||||
|
||||
|
||||
void DormanAlgo::pause_for(Duration d) {
|
||||
if (me->params.trade_accumulation) {
|
||||
if (is_paused()) {
|
||||
me->pause_until = std::max(me->pause_until.value(), { algo().get_time() + d });
|
||||
}
|
||||
else {
|
||||
me->pause_until = { algo().get_time() + d };
|
||||
// When paused child update will always expose 0
|
||||
update_child(me->exposed_qty);
|
||||
}
|
||||
}
|
||||
}
|
||||
bool DormanAlgo::is_paused() const {
|
||||
return (bool)(me->pause_until);
|
||||
}
|
||||
void DormanAlgo::resume() {
|
||||
if (is_paused()) {
|
||||
me->pause_until = std::nullopt;
|
||||
update_child(me->exposed_qty);
|
||||
}
|
||||
}
|
||||
|
||||
}}} //namespace
|
||||
@@ -1,96 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <map>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace dorman {
|
||||
|
||||
//
|
||||
// Algo parameters
|
||||
//
|
||||
|
||||
// This defines the market quantity vs the order quantity
|
||||
struct StepParams {
|
||||
std::vector<std::tuple<uint32_t, uint32_t>> step_table;
|
||||
// This is the cap on the maximum quantity,
|
||||
// even though the step table can allow for higher quantities (depending on the
|
||||
// clip size this could be divided across multiple exchange orders)
|
||||
uint32_t maximum;
|
||||
// If the market increases don't follow until it has stayed high for trail_seconds;
|
||||
uint32_t trail_seconds;
|
||||
};
|
||||
|
||||
// If the number of accumulated fills (net of profit orders) touches or goes beyond
|
||||
// this, the fills will be scratched by entering opposite orders at the fill price
|
||||
struct ScratchParams {
|
||||
uint32_t max_overfill;
|
||||
};
|
||||
// If this is enabled, then as fills accumulate, profit GTC orders are
|
||||
// immediately sent N ticks away on the opposite side.
|
||||
struct ProfitOrderParams {
|
||||
uint32_t ticks_away;
|
||||
};
|
||||
// If N contracts trade in T seconds, then the algo is paused (ie child exchange order
|
||||
// is/are cancelled) for X seconds. Once the condition clears order is reentered.
|
||||
struct TradeAccumulationParams {
|
||||
uint32_t contracts;
|
||||
uint32_t seconds;
|
||||
uint32_t sleep_seconds;
|
||||
};
|
||||
struct AlgoParams {
|
||||
StepParams steps;
|
||||
uint32_t clip_limit;
|
||||
std::optional<ScratchParams> scratch;
|
||||
std::optional<ProfitOrderParams> profit_orders;
|
||||
std::optional<TradeAccumulationParams> trade_accumulation;
|
||||
};
|
||||
|
||||
|
||||
class DormanAlgo : public IAlgoOrder {
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> me;
|
||||
IAlgoServices& algo() const;
|
||||
|
||||
public:
|
||||
DormanAlgo();
|
||||
~DormanAlgo();
|
||||
|
||||
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;
|
||||
void on_book(SecurityKey, const Book&) override;
|
||||
void on_timer() override;
|
||||
void on_ticker(SecurityKey, const TradeVector&) override;
|
||||
IAlgoOrder::OnCancelResponse on_cancel() override;
|
||||
|
||||
Quantity exposed_qty() const;
|
||||
|
||||
private:
|
||||
//
|
||||
// Child management
|
||||
void update_child_template(IBeforeLaunchAlgoServices&, const api::orderman::CustomOrderDefinition&);
|
||||
void update_child(Quantity exposed);
|
||||
bool child_active() const;
|
||||
|
||||
void do_scratch();
|
||||
void send_profit_order(Price, Quantity);
|
||||
void complete();
|
||||
|
||||
void pause_for(Duration);
|
||||
bool is_paused() const;
|
||||
void resume();
|
||||
|
||||
};
|
||||
|
||||
template <class OK,class ERR>
|
||||
using Result = std::variant<OK,ERR>;
|
||||
Result<AlgoParams,OrderOpResult> to_dorman_params(const api::orderman::DynamicOrderDefinition&);
|
||||
|
||||
}}} //namespace
|
||||
@@ -1,401 +0,0 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/LimitCoverAlgo.h"
|
||||
#include "lib/sparkproto/SparkProtos.pb.h"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoServices.h"
|
||||
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h"
|
||||
#include "lib/os/os.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include "lib/orderman/orders/algo/utils/DynamicOrderAdapter.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverDefinition.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverAdapter.h"
|
||||
#include <map>
|
||||
#include <limits>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
|
||||
#define DEFAULT_LOG_LEVEL Info
|
||||
|
||||
#define SUMMARIZE_TRANSACTION_BEGIN(id, type, ...) Summary __LOG_TRANS(this, id, Formatter::fmt(__VA_ARGS__), type, DEFAULT_LOG_LEVEL)
|
||||
|
||||
#define SUMMARIZE_TRANSACTION(id, type) Summary __LOG_TRANS(this, id, "", type, DEFAULT_LOG_LEVEL)
|
||||
#define SUMMARIZE SUMMARIZE_TRANSACTION(__FUNCTION__, SummaryType::StatusChange)
|
||||
|
||||
// define 'PRINT_SUMMARIES' to print all logs to the console
|
||||
// (assuming logs are made with the MSG/MSG_LEVEL macros)
|
||||
#ifdef PRINT_SUMMARIES
|
||||
#define _PRINT_MSG(x) std::cout << x << std::endl;
|
||||
#else
|
||||
#define _PRINT_MSG(x)
|
||||
#endif
|
||||
|
||||
#define _MSG(level, msg) \
|
||||
_PRINT_MSG(msg); \
|
||||
LOG(OrderMan, level, "{}", msg)
|
||||
|
||||
#define MSG_LEVEL(level, ...) _MSG(level, (Formatter::fmt(__VA_ARGS__)))
|
||||
// these automatically prepend log_header() to the args
|
||||
#define MSG(...) MSG_LEVEL(DEFAULT_LOG_LEVEL, log_header(), " ", __VA_ARGS__)
|
||||
#define DIAG(...) MSG_LEVEL(Diag, log_header(), " ", __VA_ARGS__)
|
||||
#define INFO(...) MSG_LEVEL(Info, log_header(), " ", __VA_ARGS__)
|
||||
#define WARN(...) MSG_LEVEL(Warn, log_header(), " ", __VA_ARGS__)
|
||||
#define ERROR(...) MSG_LEVEL(Error, log_header(), " ", __VA_ARGS__)
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
using namespace api::orderman;
|
||||
using namespace bts::orderman::limit_cover::utils;
|
||||
using Adapter = bts::orderman::LimitCoverAdapter;
|
||||
|
||||
namespace limit_cover {
|
||||
namespace utils {
|
||||
void init_delete_and_cover(OrderDefinition& target, const DeleteAndCoverDefinition& def) {
|
||||
if (!target.has_custom_def())
|
||||
*target.mutable_custom_def() = CustomOrderDefinition();
|
||||
init_delete_and_cover(*target.mutable_custom_def(), def);
|
||||
}
|
||||
|
||||
void init_delete_and_cover(CustomOrderDefinition& target, const DeleteAndCoverDefinition& def) {
|
||||
if (!target.has_delete_and_cover()) {
|
||||
*target.mutable_delete_and_cover() = DeleteAndCoverDefinition(def);
|
||||
} else {
|
||||
target.mutable_delete_and_cover()->CopyFrom(def);
|
||||
}
|
||||
}
|
||||
|
||||
void init_child_update(Update& target, uint32_t qty, double price, const DeleteAndCoverDefinition& def) {
|
||||
target.set_quantity(qty);
|
||||
target.set_price(price);
|
||||
init_delete_and_cover(*target.mutable_custom(), def);
|
||||
}
|
||||
|
||||
#define INVALID_DEF(x) OrderOpResult(api::orderman::ORDER_ERROR_INVALID_ORDER_DEFINITON, x)
|
||||
|
||||
OrderOpResult validate_custom_def(const CustomOrderDefinition& custom_def) {
|
||||
if (!custom_def.has_dynamic())
|
||||
return INVALID_DEF("DynamicOrderDefinition is undefined");
|
||||
return Adapter::validate_proto(custom_def.dynamic());
|
||||
}
|
||||
OrderOpResult validate_def(const OrderDefinition& def) {
|
||||
if (def.attach_target_order_key() != 0) {
|
||||
return INVALID_DEF("Attach not supported.");
|
||||
} else if (def.has_custom_def()) {
|
||||
return validate_custom_def(def.custom_def());
|
||||
} else {
|
||||
return INVALID_DEF("CustomOrderDefinition is undefined.");
|
||||
}
|
||||
}
|
||||
|
||||
OrderOpResult validate_update(const Update& upd) {
|
||||
return upd.has_custom() ?
|
||||
validate_custom_def(upd.custom()) : OrderOpResult();
|
||||
}
|
||||
} // namespace utils
|
||||
} // namespace limit_cover
|
||||
|
||||
struct LimitCoverAlgo::PImpl {
|
||||
IAlgoServices *algo = nullptr;
|
||||
Status status = Status::WAITING_TO_DEPLOY;
|
||||
OrderKey child_key = null_order_key();
|
||||
double child_price = os::nan();
|
||||
uint32_t child_qty = 0;
|
||||
Book book;
|
||||
uint32_t limit_volume = 0;
|
||||
std::map<uint32_t, LimitCoverGate> gates;
|
||||
double settlement = os::nan();
|
||||
double price_offset_from_settle = 0;
|
||||
double limit_price = os::nan();
|
||||
};
|
||||
|
||||
struct LimitCoverAlgo::Summary {
|
||||
Summary(LimitCoverAlgo* owner, std::string transaction_id, std::string begin_tag = {}, SummaryType type = SummaryType::StatusChange, log::Level level = DEFAULT_LOG_LEVEL)
|
||||
: _owner(owner), _transaction_id(transaction_id), _original_status(owner->me->status), _type(type), _level(level) {
|
||||
if (_owner != nullptr && !_transaction_id.empty()) {
|
||||
std::stringstream str;
|
||||
str << owner->log_header() << " begin " << transaction_id << ".";
|
||||
if (!begin_tag.empty()) {
|
||||
str << " " << begin_tag << ".";
|
||||
}
|
||||
if (type == SummaryType::FullState) {
|
||||
str << " Algo State: " << _owner->fmt_state() << ".";
|
||||
}
|
||||
MSG_LEVEL(level, str.str());
|
||||
}
|
||||
}
|
||||
|
||||
~Summary() {
|
||||
std::stringstream msg_str;
|
||||
if (!_transaction_id.empty()) {
|
||||
msg_str << " end " << _transaction_id << ".";
|
||||
}
|
||||
switch (_type) {
|
||||
case SummaryType::StatusChange:
|
||||
if (_owner->me->status != _original_status)
|
||||
msg_str << " Status changed from " << _original_status << " to " << _owner->me->status << ".";
|
||||
break;
|
||||
case SummaryType::FullState:
|
||||
msg_str << " Algo State: " << _owner->fmt_state() << ".";
|
||||
break;
|
||||
}
|
||||
auto msg = msg_str.str();
|
||||
if (!msg.empty()) {
|
||||
MSG_LEVEL(_level, _owner->log_header(), msg);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
LimitCoverAlgo *_owner;
|
||||
std::string _transaction_id;
|
||||
Status _original_status;
|
||||
SummaryType _type;
|
||||
log::Level _level;
|
||||
};
|
||||
|
||||
LimitCoverAlgo::LimitCoverAlgo() : me(new PImpl()) {}
|
||||
LimitCoverAlgo::~LimitCoverAlgo() {}
|
||||
|
||||
OrderOpResult
|
||||
LimitCoverAlgo::do_before_launch(IBeforeLaunchAlgoServices &balgo) {
|
||||
SUMMARIZE;
|
||||
bts_assert(me->status == Status::WAITING_TO_DEPLOY);
|
||||
bts_assert(me->gates.empty());
|
||||
auto vd = validate_def(balgo.definition());
|
||||
if (vd.ok()) {
|
||||
auto lcdef = Adapter::from_proto(balgo.definition());
|
||||
me->price_offset_from_settle = lcdef.price_offset();
|
||||
me->gates = lcdef.normal_gates();
|
||||
}
|
||||
return vd;
|
||||
}
|
||||
|
||||
void LimitCoverAlgo::do_launch(IAlgoServices& algo) {
|
||||
SUMMARIZE;
|
||||
bts_assert(me->status == Status::WAITING_TO_DEPLOY);
|
||||
me->algo = &algo;
|
||||
me->child_qty = algo.quantity().value();
|
||||
algo.subscribe_book(SecurityKey(algo.security().id()));
|
||||
algo.subscribe_settlement(SecurityKey(algo.security().id()));
|
||||
}
|
||||
|
||||
void LimitCoverAlgo::do_update(Update const &upd) {
|
||||
SUMMARIZE_TRANSACTION_BEGIN(__FUNCTION__, SummaryType::StatusChange, utils::pb::to_string(upd));
|
||||
auto vup = validate_update(upd);
|
||||
if (!vup.ok()) {
|
||||
me->status = Status::DONE;
|
||||
algo().terminate_error(Formatter::fmt("Invalid update: ", vup.error_msg, "."));
|
||||
return;
|
||||
}
|
||||
if (upd.has_custom()) {
|
||||
auto lcdef = Adapter::from_proto(upd.custom());
|
||||
if (me->status == Status::WAITING_TO_DEPLOY) {
|
||||
me->price_offset_from_settle = lcdef.price_offset();
|
||||
recompute_limit_price();
|
||||
recompute_limit_volume();
|
||||
}
|
||||
me->gates = lcdef.normal_gates();
|
||||
if (me->status == Status::WORKING)
|
||||
recompute_gate();
|
||||
}
|
||||
switch (me->status) {
|
||||
case Status::WAITING_TO_DEPLOY:
|
||||
try_deploy();
|
||||
break;
|
||||
case Status::WORKING: {
|
||||
// don't change the child price; the price param is irrelevant to this algo.
|
||||
if (upd.has_custom() || me->child_qty != upd.quantity())
|
||||
update_child(upd.quantity());
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LimitCoverAlgo::on_book(SecurityKey, Book const &b) {
|
||||
SUMMARIZE_TRANSACTION_BEGIN(__FUNCTION__, SummaryType::StatusChange, "BBO: ", Formatter::to_string(b));
|
||||
me->book = b;
|
||||
recompute_limit_volume();
|
||||
switch (me->status) {
|
||||
case Status::WAITING_TO_DEPLOY:
|
||||
try_deploy();
|
||||
break;
|
||||
case Status::WORKING: {
|
||||
if (recompute_gate())
|
||||
update_child(me->child_qty);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LimitCoverAlgo::update_child(uint32_t qty) {
|
||||
bts_assert(me->algo != nullptr);
|
||||
bts_assert(me->status == Status::WORKING);
|
||||
me->child_qty = qty;
|
||||
Update upd;
|
||||
init_child_update(upd, qty, me->child_price, curr_gate().definition());
|
||||
MSG("updating algo child: ", utils::pb::to_string(upd));
|
||||
me->algo->update_child(me->child_key, upd);
|
||||
}
|
||||
|
||||
void LimitCoverAlgo::on_child_change(OrderKey key, ChildState const& state) {
|
||||
bts_assert(key == me->child_key);
|
||||
SUMMARIZE_TRANSACTION_BEGIN(__FUNCTION__, SummaryType::FullState, "Child State: ", Formatter::to_string(state));
|
||||
algo().update_fill_state(state.total_fills, state.last_price);
|
||||
me->child_price = state.price.value();
|
||||
switch (me->status) {
|
||||
case Status::WORKING:
|
||||
switch (state.status) {
|
||||
case ChildStatus::WORKING:
|
||||
break;
|
||||
case ChildStatus::CANCELED: {
|
||||
auto msg = Formatter::fmt("main child (", key, ") canceled unexpectedly.");
|
||||
MSG(msg, ". ", fmt_state());
|
||||
me->status = Status::DONE;
|
||||
algo().terminate_error(msg);
|
||||
break;
|
||||
}
|
||||
case ChildStatus::COMPLETED:
|
||||
me->status = Status::DONE;
|
||||
algo().complete();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case Status::DONE:
|
||||
break;
|
||||
default:
|
||||
bts_assert_msg(false, Formatter::fmt(log_header(), " unexpected status: ", me->status));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void LimitCoverAlgo::on_settlement(SecurityKey, Price settlement) {
|
||||
if (me->status == Status::WAITING_TO_DEPLOY) {
|
||||
MSG("updating settlement from ", me->settlement, " to ", settlement);
|
||||
me->settlement = settlement.value();
|
||||
DIAG("recomputing limit price...");
|
||||
recompute_limit_price();
|
||||
DIAG("limit price is ", me->limit_price);
|
||||
DIAG("recomputing limit volume...");
|
||||
recompute_limit_volume();
|
||||
DIAG("limit volume is ", me->limit_volume);
|
||||
try_deploy();
|
||||
} else {
|
||||
MSG("ignored settlement update to ", settlement,
|
||||
": settlement updates are only processed while the algo is ", Status::WAITING_TO_DEPLOY,
|
||||
". Settlement at time of deploy was ", me->settlement);
|
||||
}
|
||||
}
|
||||
|
||||
void LimitCoverAlgo::try_deploy() {
|
||||
MSG("trying to deploy. ", fmt_state());
|
||||
bts_assert(me->status == Status::WAITING_TO_DEPLOY);
|
||||
bts_assert(me->algo != nullptr);
|
||||
// this is probably redundant (better safe than sorry tho).
|
||||
bts_assert(me->child_key == null_order_key());
|
||||
if (os::is_nan(me->settlement)) {
|
||||
MSG("still waiting for settle.");
|
||||
} else if (me->limit_volume < activation_threshold()) {
|
||||
MSG("still waiting for limit volume (", me->limit_volume, ") to breech activation threshold (", activation_threshold(), ").");
|
||||
} else {
|
||||
bts_assert(!os::is_nan(me->limit_price));
|
||||
recompute_gate();
|
||||
OrderDefinition child_def;
|
||||
bts_assert(!me->gates.empty());
|
||||
child_def.CopyFrom(algo().definition());
|
||||
init_delete_and_cover(child_def, curr_gate().definition());
|
||||
bts_assert(child_def.custom_def().has_delete_and_cover());
|
||||
MSG("deploying child order: ", utils::pb::to_string(child_def));
|
||||
me->child_key = algo().launch_child(child_def, Price(me->limit_price), algo().quantity());
|
||||
me->status = Status::WORKING;
|
||||
algo().set_ladder_visibility(false);
|
||||
}
|
||||
}
|
||||
|
||||
template<class T>
|
||||
bool did_change(T& backing, T new_value) {
|
||||
if (backing == new_value)
|
||||
return false;
|
||||
backing = new_value;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool LimitCoverAlgo::recompute_gate() {
|
||||
if (me->gates.empty())
|
||||
return false;
|
||||
auto orig_sz = me->gates.size();
|
||||
auto it = me->gates.lower_bound(me->limit_volume);
|
||||
if (it == me->gates.end() || (it != me->gates.begin() && it->first > me->limit_volume))
|
||||
it--;
|
||||
me->gates.erase(me->gates.begin(), it);
|
||||
return me->gates.size() < orig_sz;
|
||||
}
|
||||
|
||||
// limit price
|
||||
double LimitCoverAlgo::compute_limit_price() const {
|
||||
return os::is_nan(me->settlement) ? os::nan() :
|
||||
securities::improve_price(side(), Price(me->settlement), Price(me->price_offset_from_settle))
|
||||
.value();
|
||||
}
|
||||
bool LimitCoverAlgo::recompute_limit_price() {
|
||||
return did_change(me->limit_price, compute_limit_price());
|
||||
}
|
||||
|
||||
// limit volume
|
||||
uint32_t LimitCoverAlgo::compute_limit_volume() const {
|
||||
return me->algo == nullptr ? 0 :
|
||||
BookUtils::volume_at_or_better(me->book, side(), me->limit_price);
|
||||
}
|
||||
bool LimitCoverAlgo::recompute_limit_volume() {
|
||||
return did_change(me->limit_volume, compute_limit_volume());
|
||||
}
|
||||
|
||||
// pseudo properties (const state accessors)
|
||||
IAlgoServices &LimitCoverAlgo::algo() const { return *me->algo; }
|
||||
|
||||
api::Side LimitCoverAlgo::side() const {
|
||||
return me->algo == nullptr ? api::SIDE_UNDEFINED : me->algo->definition().side();
|
||||
}
|
||||
|
||||
const LimitCoverGate& LimitCoverAlgo::curr_gate() const {
|
||||
bts_assert(!me->gates.empty());
|
||||
return me->gates.begin()->second;
|
||||
}
|
||||
|
||||
uint32_t LimitCoverAlgo::activation_threshold() const {
|
||||
return me->gates.empty() ? std::numeric_limits<uint32_t>::max() : me->gates.begin()->first;
|
||||
}
|
||||
|
||||
std::string LimitCoverAlgo::log_header() const {
|
||||
std::stringstream ostr;
|
||||
ostr << "LimitCoverAlgo";
|
||||
if (me->algo != nullptr) {
|
||||
ostr << "(" << me->algo->order_key().value() << ")";
|
||||
}
|
||||
ostr << "[" << fmt_status(me->status) << "]";
|
||||
return ostr.str();
|
||||
}
|
||||
|
||||
std::string LimitCoverAlgo::fmt_state() const {
|
||||
std::stringstream ostr;
|
||||
if (me->algo != nullptr) {
|
||||
ostr << " [" << fmt_status(me->status) << "]"
|
||||
<< Formatter::to_string(side()) << " " << me->algo->quantity().value()
|
||||
<< " @ " << me->algo->price().value()
|
||||
<< " (" << me->price_offset_from_settle
|
||||
<< " away from settle).";
|
||||
if (me->child_key != null_order_key()) {
|
||||
ostr << " Main child key: " << me->child_key << ", ";
|
||||
}
|
||||
ostr << " Settle: " << me->settlement
|
||||
<< ", Limit Price: " << me->limit_price
|
||||
<< ", BBO: " << Formatter::to_string(me->book);
|
||||
} else {
|
||||
ostr << "(initializing)";
|
||||
}
|
||||
return ostr.str();
|
||||
}
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -1,94 +0,0 @@
|
||||
#pragma once
|
||||
#include "lib/orderman/orderman_utils/BookUtils.h"
|
||||
#include "lib/orderman/orderman_utils/Formatter.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverDefinition.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverGate.h"
|
||||
#include <map>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
class LimitCoverAlgo : public IAlgoOrder {
|
||||
public:
|
||||
LimitCoverAlgo();
|
||||
~LimitCoverAlgo();
|
||||
|
||||
OrderOpResult do_before_launch(IBeforeLaunchAlgoServices &) override;
|
||||
void do_launch(IAlgoServices &) override;
|
||||
void do_update(api::orderman::Update const &) override;
|
||||
void on_book(SecurityKey, Book const &) override;
|
||||
void on_settlement(SecurityKey, Price settlement) override;
|
||||
void on_child_change(OrderKey, ChildState const &) override;
|
||||
|
||||
enum class Status {
|
||||
WAITING_TO_DEPLOY,
|
||||
WORKING,
|
||||
DONE
|
||||
};
|
||||
using Status = bts::orderman::LimitCoverAlgo::Status;
|
||||
|
||||
enum SummaryType {
|
||||
StatusChange,
|
||||
FullState
|
||||
};
|
||||
|
||||
private:
|
||||
struct PImpl;
|
||||
struct Summary;
|
||||
std::unique_ptr<PImpl> me;
|
||||
|
||||
IAlgoServices &algo() const;
|
||||
api::Side side() const;
|
||||
std::string log_header() const;
|
||||
std::string fmt_state() const;
|
||||
// returns the first gate in gates (asserts that gates isn't empty).
|
||||
// Note: after the child is deployed, gates with lower thresholds are
|
||||
// removed when the limit volume passes the next gates' thresh.
|
||||
const LimitCoverGate& curr_gate() const;
|
||||
/// On the compute/recompute methods:
|
||||
/// compute_blah just returns the result (doesn't mutate any state),
|
||||
/// while recompute_blah assigns the result to the appropriate field,
|
||||
/// and returns a bool indicating whether or not the value changed.
|
||||
// limit price
|
||||
double compute_limit_price() const;
|
||||
bool recompute_limit_price();
|
||||
// limit volume
|
||||
uint32_t compute_limit_volume() const;
|
||||
bool recompute_limit_volume();
|
||||
// recomputes the current gate (i.e., the one with the
|
||||
// highest activation thresh that is still less than
|
||||
// the limit volume) then removes all gates with lower threshs.
|
||||
bool recompute_gate();
|
||||
uint32_t activation_threshold() const;
|
||||
//
|
||||
void try_deploy();
|
||||
void update_child(uint32_t qty);
|
||||
};
|
||||
|
||||
namespace limit_cover {
|
||||
namespace utils {
|
||||
inline std::string fmt_status(LimitCoverAlgo::Status status) {
|
||||
switch (status) {
|
||||
case LimitCoverAlgo::Status::WAITING_TO_DEPLOY: return "WAITING_TO_DEPLOY";
|
||||
case LimitCoverAlgo::Status::WORKING: return "WORKING";
|
||||
case LimitCoverAlgo::Status::DONE: return "DONE";
|
||||
default: return Formatter::fmt("STATUS_", static_cast<int>(status));
|
||||
}
|
||||
}
|
||||
void init_delete_and_cover(api::orderman::OrderDefinition& target, const api::orderman::DeleteAndCoverDefinition& def);
|
||||
void init_delete_and_cover(api::orderman::CustomOrderDefinition& target, const api::orderman::DeleteAndCoverDefinition& def);
|
||||
void init_child_update(api::orderman::Update& target, uint32_t qty, double price, const api::orderman::DeleteAndCoverDefinition& def);
|
||||
OrderOpResult validate_def(const api::orderman::OrderDefinition&);
|
||||
OrderOpResult validate_custom_def(const api::orderman::CustomOrderDefinition&);
|
||||
OrderOpResult validate_update(const api::orderman::Update&);
|
||||
} // namespace utils
|
||||
} // namespace limit_cover
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& ostr, const LimitCoverAlgo::Status& status) {
|
||||
ostr << limit_cover::utils::fmt_status(status);
|
||||
return ostr;
|
||||
}
|
||||
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -14,6 +14,7 @@
|
||||
#include <google/protobuf/util/time_util.h>
|
||||
#include <boost/tokenizer.hpp>
|
||||
#include <stdint.h>
|
||||
#include "lib/plugin/SparkServices.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
@@ -242,6 +243,8 @@ void OnSignalAlgo::_switch_state (State new_state){
|
||||
static_cast<int>(new_state));
|
||||
me->state = new_state;
|
||||
}
|
||||
extern "C" void init_spark_plugin(bts::SparkServices &) {
|
||||
}
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
} // namespace bts
|
||||
@@ -1,584 +0,0 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/PyramidAlgo.h"
|
||||
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoServices.h"
|
||||
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h"
|
||||
#include "lib/os/os.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include <stdint.h>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
using namespace api::orderman;
|
||||
|
||||
enum class PyramidAlgo::Status { WAITING_FOR_MD, WAITING_FOR_MD_ATTACH, WORKING, DONE };
|
||||
enum class PyramidAlgo::ExitPath { CANCEL, ERR, COMPLETE };
|
||||
|
||||
struct LegData{
|
||||
const size_t index;
|
||||
const SecurityKey sec_key;
|
||||
const double tick_size;
|
||||
const api::Side side; //trading side
|
||||
ChildTemplate* order_template = nullptr;
|
||||
OrderKey order_key;
|
||||
ChildState order_state;
|
||||
bool recv_book;
|
||||
Book book;
|
||||
Quantity top_delete; // this will be DONTCARE for covers
|
||||
Quantity cover_payup;// this will be DONTCARE for top
|
||||
LegData (size_t index_a,
|
||||
SecurityKey sec_key_a,
|
||||
double tick_size_a,
|
||||
api::Side side_a,
|
||||
ChildTemplate* order_template_a,
|
||||
Quantity qty)
|
||||
:
|
||||
index (index_a),
|
||||
sec_key (sec_key_a),
|
||||
tick_size(tick_size_a),
|
||||
side (side_a),
|
||||
order_template (order_template_a),
|
||||
order_key(null_order_key()),
|
||||
order_state(),
|
||||
recv_book(false),
|
||||
book(),
|
||||
top_delete(index == 0 ? qty.value() : 0),
|
||||
cover_payup(index == 0 ? 0 : qty.value())
|
||||
{
|
||||
}
|
||||
const BookSide& opp_md() const{
|
||||
return book.side(SpreaderUtils::invert_side(side));
|
||||
}
|
||||
bool is_top_spread() const{
|
||||
return index == 0;
|
||||
}
|
||||
void set_book (const Book& book_a){
|
||||
recv_book = true;
|
||||
book = book_a;
|
||||
}
|
||||
};
|
||||
|
||||
struct PyramidAlgo::PImpl {
|
||||
Status state = Status::WAITING_FOR_MD;
|
||||
IAlgoServices* algo_services = nullptr;
|
||||
IBeforeLaunchAlgoServices* bl_algo_services;
|
||||
PyramidAlgo::PyramidAlgoParams params;
|
||||
std::vector<LegData> legs;
|
||||
api::Side side;
|
||||
Price out_price;
|
||||
Quantity out_qty;
|
||||
PImpl(){
|
||||
}
|
||||
LegData* find_leg_from_sec (SecurityKey sec_key){
|
||||
for (auto& l : legs)
|
||||
if(l.sec_key == sec_key)
|
||||
return &l;
|
||||
return nullptr;
|
||||
}
|
||||
LegData* find_leg_from_ord_key (OrderKey ord_key){
|
||||
for (auto& l : legs)
|
||||
if(l.order_key == ord_key)
|
||||
return &l;
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
PyramidAlgo::PyramidAlgo() : me(new PImpl()) {}
|
||||
PyramidAlgo::~PyramidAlgo() {}
|
||||
IAlgoServices& PyramidAlgo::_algo() const { return *me->algo_services; }
|
||||
|
||||
// initialization /////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
OrderOpResult PyramidAlgo::do_before_launch(IBeforeLaunchAlgoServices& serv) {
|
||||
me->bl_algo_services = &serv;
|
||||
auto& def = serv.definition();
|
||||
if (def.has_custom_def() && def.custom_def().has_dynamic()){
|
||||
auto conv_res = _get_creation_params(def.custom_def(), serv);
|
||||
if (std::get<1>(conv_res)){
|
||||
// params are good
|
||||
me->params = std::get<0>(conv_res);
|
||||
me->side = def.side();
|
||||
// top spread
|
||||
auto* t = _create_top_order_template(serv);
|
||||
double tick_size = serv.lookup_security(me->params.top_spread)->contract_spec().low_tick();
|
||||
me->legs.emplace_back(0, me->params.top_spread, tick_size, def.side(), t,
|
||||
me->params.top_delete);
|
||||
// covers
|
||||
size_t i = 0;
|
||||
for (SecurityKey key : me->params.cover_spreads){
|
||||
auto side = SpreaderUtils::invert_side(def.side());
|
||||
auto* t = _create_hedge_order_template(serv,i);
|
||||
double tick_size = serv.lookup_security(key)->contract_spec().low_tick();
|
||||
me->legs.emplace_back(i+1, key, tick_size, side, t, me->params.cover_payups[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
return {OrderError::ORDER_ERROR_INVALID_PARAMETER,
|
||||
std::get<2>(conv_res)};
|
||||
}
|
||||
else
|
||||
return {OrderError::ORDER_ERROR_INVALID_PARAMETER,
|
||||
std::string("Missing parameters")};
|
||||
return {};
|
||||
}
|
||||
void PyramidAlgo::do_launch(IAlgoServices& algo_services) {
|
||||
me->algo_services = &algo_services;
|
||||
me->out_price = algo_services.price();
|
||||
me->out_qty = algo_services.quantity();
|
||||
for (const LegData& l : me->legs)
|
||||
algo_services.subscribe_book(l.sec_key);
|
||||
}
|
||||
void PyramidAlgo::do_launch_attached(IAlgoServices& algo_services,
|
||||
OrderKey key,
|
||||
ChildState c) {
|
||||
me->algo_services = &algo_services;
|
||||
if(c.quantity != algo_services.quantity() ||
|
||||
!bts::securities::is_same(c.price, algo_services.price())){
|
||||
_cxl_and_end(ExitPath::ERR,
|
||||
"Pyramid price or quantity did not match the working order",
|
||||
true);
|
||||
}
|
||||
else{
|
||||
me->legs[0].order_key = key;
|
||||
me->out_price = c.price;
|
||||
me->out_qty = c.quantity;
|
||||
for (const LegData& l : me->legs)
|
||||
algo_services.subscribe_book(l.sec_key);
|
||||
_switch_state(PyramidAlgo::Status::WAITING_FOR_MD_ATTACH);
|
||||
on_child_change(key,c);
|
||||
}
|
||||
}
|
||||
// updates ////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void PyramidAlgo::do_update(const api::orderman::Update& update) {
|
||||
LOG(OrderMan, Diag, "PyramidAlgo::do_update order:{} state:{} "
|
||||
"receiving update: {}",
|
||||
_algo().order_key().value(),
|
||||
static_cast<int>(me->state),
|
||||
utils::pb::to_string(update));
|
||||
auto conv_res = _get_update_params(update, me->params);
|
||||
if (!std::get<1>(conv_res)){
|
||||
LOG(OrderMan, Warn, "PyramidAlgo::do_update order:{} bad update, ignoring",
|
||||
_algo().order_key().value());
|
||||
return;
|
||||
}
|
||||
const PyramidAlgoParams& algo_params = std::get<0>(conv_res);
|
||||
Price price = Price(update.price());
|
||||
Quantity qty = Quantity(update.quantity());
|
||||
switch(me->state){
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD:
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD_ATTACH:
|
||||
LOG(OrderMan, Info, "PyramidAlgo::do_update order:{} state:{} ignored",
|
||||
_algo().order_key().value(),static_cast<int>(me->state));
|
||||
break;
|
||||
case PyramidAlgo::Status::WORKING:{
|
||||
bool pq_changed = _copy_update(algo_params, price, qty);
|
||||
if(_target_reached()){
|
||||
_cxl_and_end (PyramidAlgo::ExitPath::CANCEL, "Target reached");
|
||||
return;
|
||||
}
|
||||
if (_working_state_met()){
|
||||
if(pq_changed)
|
||||
_modify_top_spread_order();
|
||||
}
|
||||
else
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order cancelled (Cover prices not met)");
|
||||
break;
|
||||
}
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// callbacks //////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void PyramidAlgo::on_book(SecurityKey key, const Book& book) {
|
||||
LegData* leg_data = me->find_leg_from_sec(key);
|
||||
if(leg_data != nullptr){
|
||||
leg_data->set_book(book);
|
||||
_analyze_md_and_act();
|
||||
}
|
||||
}
|
||||
// this only gets called on am explicit algo cancel
|
||||
// from the outside
|
||||
IAlgoOrder::OnCancelResponse PyramidAlgo::on_cancel(){
|
||||
// cancel top spread, let hedges work, cancel algo
|
||||
switch(me->state){
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD:
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD_ATTACH:
|
||||
case PyramidAlgo::Status::WORKING:
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,"Algo cancelled");
|
||||
break;
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
return IAlgoOrder::OnCancelResponse::IGNORE;
|
||||
}
|
||||
void PyramidAlgo::on_child_change(OrderKey key, const ChildState &st) {
|
||||
LOG(OrderMan, Info,
|
||||
"PyramidAlgo::on_child_change key:{} child_key:{} state:{} status:{} "
|
||||
"total_fills:{} last_price:{}",
|
||||
_algo().order_key(),
|
||||
key,
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(st.status),
|
||||
st.total_fills,
|
||||
st.last_price);
|
||||
LegData* leg = me->find_leg_from_ord_key (key);
|
||||
if (me->legs[0].order_key == key){
|
||||
switch(me->state){
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD:
|
||||
bts_assert_msg (false, "No child orders");
|
||||
break;
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD_ATTACH:
|
||||
leg->order_state = st;
|
||||
_algo().update_fill_state(st.total_fills, st.avg_price);
|
||||
_check_for_completion();
|
||||
break;
|
||||
case PyramidAlgo::Status::WORKING:
|
||||
leg->order_state = st;
|
||||
_algo().update_fill_state(st.total_fills, st.avg_price);
|
||||
_send_hedges(st.last_fills);
|
||||
_check_for_completion();
|
||||
break;
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// stateless conversion ///////////////////////////////////////////////////////
|
||||
|
||||
PyramidAlgo::PyramidAlgoParams PyramidAlgo::_convert_params (
|
||||
const CustomOrderDefinition& cod){
|
||||
const DynamicOrderDefinition& def = cod.dynamic();
|
||||
const auto& params = def.order_params();
|
||||
PyramidAlgoParams rv;
|
||||
// top contract
|
||||
auto r = get_if<uint32_t>(params, "Top Spread");
|
||||
rv.top_spread = r.has_value() ? SecurityKey(r.value()) : SecurityKey(0);
|
||||
// top delete
|
||||
r = get_if<uint32_t>(params, "Delete");
|
||||
rv.top_delete = Quantity(r.has_value() ? r.value() : UINT32_MAX);
|
||||
for (size_t i = 0; i < 5; i++){
|
||||
auto idx = std::to_string(i+1);
|
||||
auto b = get_if<uint32_t>(params, "Spread " + idx);
|
||||
if (b.has_value() && b.value() != 0){
|
||||
rv.cover_spreads.emplace_back(b.value());
|
||||
auto pu = get_if<uint32_t>(params, "Spread " + idx + " Payup");
|
||||
rv.cover_payups.emplace_back(pu.has_value() ? pu.value() : UINT32_MAX);
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
std::tuple<PyramidAlgo::PyramidAlgoParams,bool,std::string>
|
||||
PyramidAlgo::_get_creation_params(const CustomOrderDefinition& cod,
|
||||
IBeforeLaunchAlgoServices& serv){
|
||||
PyramidAlgo::PyramidAlgoParams rv = _convert_params(cod);
|
||||
// error checking
|
||||
if(rv.top_spread.value() != serv.security().id())
|
||||
return std::make_tuple(rv,false,
|
||||
"Algo can only be launched from the Top Spread");
|
||||
if(rv.cover_spreads.size() < 2)
|
||||
return std::make_tuple(rv,false,
|
||||
"Not enough cover spreads");
|
||||
if (rv.cover_spreads[0].value() == 0 ||
|
||||
rv.cover_spreads[1].value() == 0)
|
||||
return std::make_tuple(rv,false,
|
||||
"Not enough cover spreads");
|
||||
return std::make_tuple(rv,true,std::string(""));
|
||||
}
|
||||
|
||||
std::tuple<PyramidAlgo::PyramidAlgoParams,bool,std::string>
|
||||
PyramidAlgo::_get_update_params(const Update& update,
|
||||
const PyramidAlgoParams& curr_params){
|
||||
if(update.has_custom() && update.custom().has_dynamic()){
|
||||
// means there is a params change
|
||||
PyramidAlgoParams new_params = _convert_params(update.custom());
|
||||
if(new_params.top_spread != curr_params.top_spread ||
|
||||
new_params.cover_spreads.size() != curr_params.cover_spreads.size())
|
||||
return std::make_tuple(new_params,false,"Contracts can not be updated");
|
||||
for (size_t i=0; i < curr_params.cover_spreads.size(); i++)
|
||||
if(new_params.cover_spreads[i] != curr_params.cover_spreads[i])
|
||||
return std::make_tuple(new_params,false,"Contracts can not be updated");
|
||||
return std::make_tuple(new_params,true,std::string(""));
|
||||
}
|
||||
else
|
||||
return std::make_tuple(curr_params,true,std::string(""));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// order template creation/////////////////////////////////////////////////////
|
||||
|
||||
ChildTemplate* PyramidAlgo::_create_top_order_template (
|
||||
IBeforeLaunchAlgoServices& serv) const{
|
||||
OrderDefinition order_def = serv.definition();
|
||||
order_def.set_security_key (me->params.top_spread.value());
|
||||
order_def.set_side(me->side);
|
||||
order_def.mutable_custom_def()->mutable_limit()->MergeFrom (
|
||||
LimitOrderDefinition());
|
||||
return &(serv.make_child_template(order_def));
|
||||
}
|
||||
|
||||
ChildTemplate* PyramidAlgo::_create_hedge_order_template (
|
||||
IBeforeLaunchAlgoServices& serv,
|
||||
size_t hedge_idx) const{
|
||||
OrderDefinition order_def = serv.definition();
|
||||
order_def.set_attach_target_order_key(null_order_key().value());
|
||||
order_def.set_security_key (me->params.cover_spreads[hedge_idx].value());
|
||||
order_def.set_side(SpreaderUtils::invert_side(me->side));
|
||||
order_def.set_sub_type(ORDER_SUB_TYPE_HEDGING_LEG);
|
||||
LeanOrderDefinition lean;
|
||||
lean.mutable_cross_threshold()->set_absolute(me->params.cover_payups[hedge_idx].value());
|
||||
lean.set_max_chase_ticks(1);
|
||||
lean.set_trade_toward_behavior(LeanOrderDefinition::NO_CHANGE);
|
||||
lean.mutable_child()->mutable_limit()->MergeFrom (
|
||||
LimitOrderDefinition());
|
||||
order_def.mutable_custom_def()->mutable_lean()->MergeFrom (lean);
|
||||
return &(serv.make_child_template(order_def));
|
||||
}
|
||||
|
||||
void PyramidAlgo::_analyze_md_and_act(){
|
||||
switch(me->state){
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD:
|
||||
if(!_md_recvd())
|
||||
return;
|
||||
// received data for the first time
|
||||
if (_working_state_met()){
|
||||
// enter order, and switch state
|
||||
LegData& top_leg = me->legs[0];
|
||||
// this is the only place top spread order is sent (once)
|
||||
top_leg.order_key = _algo().launch_child(
|
||||
*top_leg.order_template,
|
||||
me->out_price,
|
||||
me->out_qty);
|
||||
LOG(OrderMan, Info,"PyramidAlgo::_analyze_md_and_act launching order key:{} "
|
||||
"child_key:{} price:{} qty:{}",
|
||||
_algo().order_key(),
|
||||
top_leg.order_key,
|
||||
me->out_price,
|
||||
me->out_qty);
|
||||
_switch_state(PyramidAlgo::Status::WORKING);
|
||||
}
|
||||
else
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order cancelled (Cover prices not met)");
|
||||
break;
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD_ATTACH:
|
||||
if(!_md_recvd())
|
||||
return;
|
||||
// received data for the first time
|
||||
if (_working_state_met())
|
||||
_switch_state(PyramidAlgo::Status::WORKING);
|
||||
else
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order detached (Cover prices not met)",
|
||||
true);
|
||||
break;
|
||||
case PyramidAlgo::Status::WORKING:
|
||||
if (!_working_state_met())
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order cancelled (Cover prices not met)");
|
||||
break;
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidAlgo::_switch_state(Status new_state){
|
||||
switch(new_state){
|
||||
case (PyramidAlgo::Status::WAITING_FOR_MD):
|
||||
bts_assert(false);
|
||||
break;
|
||||
case (PyramidAlgo::Status::WAITING_FOR_MD_ATTACH):
|
||||
bts_assert(me->state == PyramidAlgo::Status::WAITING_FOR_MD);
|
||||
break;
|
||||
case (PyramidAlgo::Status::WORKING):
|
||||
bts_assert(me->state == PyramidAlgo::Status::WAITING_FOR_MD ||
|
||||
me->state == PyramidAlgo::Status::WAITING_FOR_MD_ATTACH);
|
||||
break;
|
||||
case (PyramidAlgo::Status::DONE):
|
||||
bts_assert(
|
||||
// when an order is entered and the criteria is not met
|
||||
me->state == PyramidAlgo::Status::WAITING_FOR_MD ||
|
||||
// when an order is attached and the market conditions are not met
|
||||
me->state == PyramidAlgo::Status::WAITING_FOR_MD_ATTACH ||
|
||||
// when the child order gets into an unexpected state
|
||||
me->state == PyramidAlgo::Status::WORKING);
|
||||
break;
|
||||
}
|
||||
LOG(OrderMan, Info,"PyramidAlgo::_switch_state key:{} {}->{} ",
|
||||
_algo().order_key(),
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(new_state));
|
||||
me->state = new_state;
|
||||
}
|
||||
|
||||
void PyramidAlgo::_send_hedges (Quantity filled_qty){
|
||||
if (filled_qty.value() > 0)
|
||||
for (auto& l : me->legs)
|
||||
if(!l.is_top_spread()){
|
||||
const auto& inside_md = l.opp_md()[0];
|
||||
Price hedge_price = Price(inside_md.price());
|
||||
uint32_t inside_md_qty = inside_md.size();
|
||||
if (l.cover_payup.value() != 0 &&
|
||||
inside_md_qty > l.cover_payup.value())
|
||||
hedge_price = securities::worsen_price(l.side, hedge_price, Price(l.tick_size));
|
||||
// check payup
|
||||
l.order_key = _algo().launch_child(
|
||||
*l.order_template,
|
||||
hedge_price,
|
||||
filled_qty);
|
||||
_algo().detach_child(l.order_key);
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidAlgo::_modify_top_spread_order(){
|
||||
Update child_update;
|
||||
child_update.set_price(me->out_price.value());
|
||||
child_update.set_quantity(me->out_qty.value());
|
||||
LOG(OrderMan, Info,"PyramidAlgo::_modify_top_spread_order modifying order key:{} "
|
||||
"child_key:{} price:{} qty:{}",
|
||||
_algo().order_key(),
|
||||
me->legs[0].order_key,
|
||||
me->out_price,
|
||||
me->out_qty);
|
||||
_algo().update_child(me->legs[0].order_key, child_update);
|
||||
}
|
||||
// copies the params, price and qty to the state
|
||||
// retuns true is the price or qty has changed
|
||||
bool PyramidAlgo::_copy_update(const PyramidAlgoParams& algo_params,
|
||||
Price price, Quantity qty){
|
||||
// copy input params (which are stored seperately)
|
||||
me->params.top_delete = algo_params.top_delete;
|
||||
size_t cov_idx = 0;
|
||||
for (Quantity payup : algo_params.cover_payups)
|
||||
me->params.cover_payups[cov_idx++] = payup;
|
||||
// copy leg data
|
||||
cov_idx = 0;
|
||||
me->legs[0].top_delete = algo_params.top_delete;
|
||||
for (Quantity pu : algo_params.cover_payups){
|
||||
LegData& cover_leg = me->legs[cov_idx+1];
|
||||
if(cover_leg.cover_payup != pu){
|
||||
cover_leg.cover_payup = pu;
|
||||
cover_leg.order_template = _create_hedge_order_template(
|
||||
*me->bl_algo_services,
|
||||
cov_idx);
|
||||
}
|
||||
cov_idx++;
|
||||
}
|
||||
// copy price and qty (returns true is they have changed)
|
||||
bool pq_changed = me->out_qty.value() != qty.value() ||
|
||||
!bts::securities::is_same(me->out_price, price);
|
||||
me->out_price = price;
|
||||
me->out_qty = qty;
|
||||
return pq_changed;
|
||||
}
|
||||
// if the top spread order is in a final state, the algo
|
||||
// algo goes to the final state
|
||||
void PyramidAlgo::_check_for_completion(){
|
||||
const ChildState& top_ord = me->legs[0].order_state;
|
||||
switch (top_ord.status){
|
||||
case (ChildStatus::WORKING):
|
||||
break;
|
||||
case (ChildStatus::COMPLETED):
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::ERR,"");
|
||||
break;
|
||||
case (ChildStatus::CANCELED):
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::ERR,
|
||||
"Top spread unexpectedly cancelled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidAlgo::_cxl_and_end(PyramidAlgo::ExitPath exit_path,
|
||||
const std::string& msg,
|
||||
bool detach_top){
|
||||
if(me->legs[0].order_key != null_order_key()){
|
||||
const ChildState& top_ord_state = me->legs[0].order_state;
|
||||
if(top_ord_state.status == ChildStatus::WORKING){
|
||||
if(detach_top)
|
||||
_algo().detach_child(me->legs[0].order_key);
|
||||
else
|
||||
_algo().cancel_child(me->legs[0].order_key);
|
||||
}
|
||||
}
|
||||
switch(exit_path){
|
||||
case PyramidAlgo::ExitPath::ERR:
|
||||
_algo().terminate_error(msg);
|
||||
break;
|
||||
case PyramidAlgo::ExitPath::CANCEL:
|
||||
_algo().cancel(msg);
|
||||
break;
|
||||
case PyramidAlgo::ExitPath::COMPLETE:
|
||||
_algo().complete();
|
||||
break;
|
||||
}
|
||||
_switch_state(PyramidAlgo::Status::DONE);
|
||||
}
|
||||
// true the out qty is changed it be below
|
||||
// the filled qty
|
||||
bool PyramidAlgo::_target_reached() const{
|
||||
return me->legs[0].order_state.total_fills.value() >=
|
||||
me->out_qty.value();
|
||||
}
|
||||
|
||||
bool PyramidAlgo::_md_recvd() const{
|
||||
for (const LegData& l : me->legs)
|
||||
if(!l.recv_book)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool gr_than_eq(double p1, double p2){
|
||||
return securities::test_price_equality(p1,p2) || p1 > p2;
|
||||
}
|
||||
|
||||
bool less_than_eq(double p1, double p2){
|
||||
return securities::test_price_equality(p1,p2) || p1 < p2;
|
||||
}
|
||||
|
||||
bool PyramidAlgo::_working_state_met() const{
|
||||
double cover_sum = 0.0;
|
||||
size_t i = 0;
|
||||
for (const auto& l : me->legs)
|
||||
if (i++ > 0)
|
||||
cover_sum += l.opp_md()[0].price();
|
||||
bool rv = !os::is_nan(cover_sum) &&
|
||||
(
|
||||
(
|
||||
me->side == api::Side::SIDE_BUY &&
|
||||
less_than_eq(me->out_price.value(),cover_sum)
|
||||
)
|
||||
||
|
||||
(
|
||||
me->side == api::Side::SIDE_SELL &&
|
||||
gr_than_eq(me->out_price.value(),cover_sum)
|
||||
)
|
||||
);
|
||||
if (!rv && !os::is_nan(cover_sum)){
|
||||
// price condition is not met, but if the
|
||||
// cum qty at the top leg price is > threashold
|
||||
// then the condition is met ()
|
||||
const auto& top_leg = me->legs[0];
|
||||
Quantity cumm_level_qty;
|
||||
SpreaderUtils::find_qty_upto_price_level(top_leg.book,
|
||||
top_leg.side,
|
||||
Price(me->out_price),
|
||||
0.0,
|
||||
cumm_level_qty);
|
||||
rv = cumm_level_qty.value() > top_leg.top_delete.value();
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -1,74 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
class ChildTemplate;
|
||||
struct LegData;
|
||||
|
||||
class PyramidAlgo : public IAlgoOrder {
|
||||
public:
|
||||
PyramidAlgo();
|
||||
~PyramidAlgo();
|
||||
struct PyramidAlgoParams{
|
||||
// first one is the top
|
||||
SecurityKey top_spread;
|
||||
Quantity top_delete;
|
||||
std::vector<SecurityKey> cover_spreads;
|
||||
std::vector<Quantity> cover_payups;
|
||||
};
|
||||
public:
|
||||
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_book(SecurityKey, Book const &) override;
|
||||
void on_child_change(OrderKey, ChildState const &) override;
|
||||
IAlgoOrder::OnCancelResponse on_cancel() override;
|
||||
|
||||
private:
|
||||
enum class Status;
|
||||
enum class ExitPath;
|
||||
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me;
|
||||
private:
|
||||
IAlgoServices& _algo() const ;
|
||||
// converts parameters from the CustomOrderDefinition::dynamic
|
||||
// to PyramidAlgoParams
|
||||
static PyramidAlgoParams _convert_params (
|
||||
const bts::api::orderman::CustomOrderDefinition&);
|
||||
static std::tuple<PyramidAlgoParams,bool,std::string> _get_creation_params(
|
||||
const bts::api::orderman::CustomOrderDefinition&,
|
||||
IBeforeLaunchAlgoServices&);
|
||||
static std::tuple<PyramidAlgoParams,bool,std::string> _get_update_params(
|
||||
const api::orderman::Update&,
|
||||
const PyramidAlgoParams& curr_params);
|
||||
ChildTemplate* _create_top_order_template (IBeforeLaunchAlgoServices&) const;
|
||||
ChildTemplate* _create_hedge_order_template (IBeforeLaunchAlgoServices&,
|
||||
size_t) const;
|
||||
// acts on md, cognizant of the state
|
||||
void _analyze_md_and_act();
|
||||
void _switch_state (Status new_status);
|
||||
void _send_hedges (Quantity filled_qty);
|
||||
void _modify_top_spread_order();
|
||||
// returns true is the price, qty changes
|
||||
bool _copy_update(const PyramidAlgoParams& algo_params,
|
||||
Price price, Quantity qty);
|
||||
void _check_for_completion();
|
||||
// cxl's the working top leg order (if any), detaches
|
||||
// all hedges. if is_error then terminates algo, is
|
||||
// is_cxl then cxl's algo, otherwise completes it
|
||||
void _cxl_and_end(ExitPath exit_path,
|
||||
const std::string& err_txt,
|
||||
bool detach_top = false);
|
||||
bool _target_reached() const;
|
||||
// true is all legs have recvd md
|
||||
bool _md_recvd() const;
|
||||
bool _working_state_met() const;
|
||||
};
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -1,253 +0,0 @@
|
||||
|
||||
#include "lib/plugin/SparkServices.h"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverAdapter.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/LimitCoverAlgo.h"
|
||||
#include "lib/config/protos/toggles.pb.h"
|
||||
#include "lib/config/config.h"
|
||||
#include "DormanAlgo.h"
|
||||
#include "PyramidAlgo.h"
|
||||
#include "TASAlgo.h"
|
||||
#include "TASCounterAlgo.h"
|
||||
#include "OnSignalAlgo.h"
|
||||
|
||||
using namespace bts;
|
||||
using namespace api::orderman;
|
||||
using namespace orderman::dorman;
|
||||
using namespace orderman;
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace dorman {
|
||||
|
||||
template <class OK,class ERR>
|
||||
using Result = std::variant<OK,ERR>;
|
||||
Result<AlgoParams,OrderOpResult> bad_definition(const char* msg) { return {OrderOpResult{ORDER_ERROR_INVALID_ORDER_DEFINITON, msg}}; }
|
||||
|
||||
// Validate DynamicOrderDefinition and construct parameter object
|
||||
Result<AlgoParams,OrderOpResult> to_dorman_params(const api::orderman::DynamicOrderDefinition& def) {
|
||||
const auto& params = def.order_params();
|
||||
AlgoParams res;
|
||||
|
||||
//
|
||||
// Step Table
|
||||
for (size_t x = 0; x < 5; ++x) {
|
||||
auto xs = std::to_string(x+1);
|
||||
auto mq = get_if<uint32_t>(params, "Market Quantity " + xs).value_or(0);
|
||||
auto oq = get_if<uint32_t>(params, "Order Quantity " + xs).value_or(0);
|
||||
if (mq && oq) {
|
||||
res.steps.step_table.push_back({ mq, oq });
|
||||
}
|
||||
}
|
||||
std::sort(res.steps.step_table.begin(), res.steps.step_table.end());
|
||||
res.steps.maximum = get_if<uint32_t>(params, "Max Size").value_or(-1);
|
||||
res.steps.trail_seconds = get_if<uint32_t>(params, "Trail Seconds").value_or(0);
|
||||
|
||||
//
|
||||
// Clip limit
|
||||
res.clip_limit = get_if<uint32_t>(params, "Clip Size").value_or(0);
|
||||
|
||||
//
|
||||
// scratch
|
||||
auto enable_scratch = get_if<bool>(params, "Enable Scratch");
|
||||
if (enable_scratch && enable_scratch.value()) {
|
||||
auto overfill = get_if<uint32_t>(params, "Max Overfill");
|
||||
if (!overfill) return bad_definition("Missing 'Max Overfill'");
|
||||
res.scratch = { overfill.value() };
|
||||
}
|
||||
|
||||
//
|
||||
// profit orders
|
||||
auto enable_profit_orders = get_if<bool>(params, "Profit Orders");
|
||||
if (enable_profit_orders && enable_profit_orders.value()) {
|
||||
auto ticks = get_if<uint32_t>(params, "Ticks Away");
|
||||
if (!ticks) return bad_definition("Missing 'Ticks Away'");
|
||||
res.profit_orders = { ticks.value() };
|
||||
}
|
||||
|
||||
//
|
||||
// trade accumulation
|
||||
auto trade_acc = get_if<bool>(params, "Trade Accumulation").value_or(false);
|
||||
if (trade_acc) {
|
||||
TradeAccumulationParams tap;
|
||||
|
||||
auto contracts = get_if<uint32_t>(params, "Trade Contracts");
|
||||
if (!contracts) return bad_definition("Missing 'Trade Accumulation # Contracts'");
|
||||
tap.contracts = contracts.value();
|
||||
|
||||
auto interval = get_if<uint32_t>(params, "Trade Interval (seconds)");
|
||||
if (!interval) return bad_definition("Missing 'Trade Accumulation Interval'");
|
||||
tap.seconds = interval.value();
|
||||
|
||||
auto sleep = get_if<uint32_t>(params, "Trade Sleep (seconds)");
|
||||
if (!sleep) return bad_definition("Missing 'Trade Accumulation Sleep'");
|
||||
tap.sleep_seconds = sleep.value();
|
||||
|
||||
res.trade_accumulation = {tap};
|
||||
}
|
||||
|
||||
return {res};
|
||||
}
|
||||
|
||||
extern "C" void init_dorman_plugin(bts::SparkServices &srv) {
|
||||
DynamicOrderDescriptor join_desc;
|
||||
|
||||
join_desc.set_dynamic_order_type("Dorman");
|
||||
|
||||
*join_desc.add_param_desc() = param_array("what", param_double("").param_type());
|
||||
|
||||
|
||||
*join_desc.add_param_desc() = param_uint32("Market Quantity 1");
|
||||
*join_desc.add_param_desc() = param_uint32("Order Quantity 1");
|
||||
*join_desc.add_param_desc() = param_uint32("Market Quantity 2");
|
||||
*join_desc.add_param_desc() = param_uint32("Order Quantity 2");
|
||||
*join_desc.add_param_desc() = param_uint32("Market Quantity 3");
|
||||
*join_desc.add_param_desc() = param_uint32("Order Quantity 3");
|
||||
*join_desc.add_param_desc() = param_uint32("Market Quantity 4");
|
||||
*join_desc.add_param_desc() = param_uint32("Order Quantity 4");
|
||||
*join_desc.add_param_desc() = param_uint32("Market Quantity 5");
|
||||
*join_desc.add_param_desc() = param_uint32("Order Quantity 5");
|
||||
|
||||
*join_desc.add_param_desc() = param_uint32("Max Size");
|
||||
*join_desc.add_param_desc() = param_uint32("Trail Seconds");
|
||||
*join_desc.add_param_desc() = param_uint32("Clip Size");
|
||||
*join_desc.add_param_desc() = param_bool("Enable Scratch");
|
||||
*join_desc.add_param_desc() = param_uint32("Max Overfill");
|
||||
*join_desc.add_param_desc() = param_bool("Profit Orders");
|
||||
*join_desc.add_param_desc() = param_uint32("Ticks Away");
|
||||
|
||||
*join_desc.add_param_desc() = param_bool("Trade Accumulation");
|
||||
*join_desc.add_param_desc() = param_uint32("Trade Contracts");
|
||||
*join_desc.add_param_desc() = param_uint32("Trade Interval (seconds)");
|
||||
*join_desc.add_param_desc() = param_uint32("Trade Sleep (seconds)");
|
||||
|
||||
srv.register_dynamic_algo_order(join_desc, []() { return std::make_unique<DormanAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_pyramid_plugin(bts::SparkServices& srv) {
|
||||
DynamicOrderDescriptor join_desc;
|
||||
|
||||
join_desc.set_dynamic_order_type("Pyramid");
|
||||
|
||||
*join_desc.add_param_desc() = param_security_key("Top Spread");
|
||||
*join_desc.add_param_desc() = param_uint32("Delete");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 1");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 1 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 1 Order");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 2");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 2 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 2 Order");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 3");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 3 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 3 Order");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 4");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 4 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 4 Order");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 5");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 5 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 5 Order");
|
||||
|
||||
srv.register_dynamic_algo_order(join_desc, []() { return std::make_unique<orderman::PyramidAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_tascounter_plugin(bts::SparkServices& srv) {
|
||||
DynamicOrderDescriptor join_desc;
|
||||
|
||||
join_desc.set_dynamic_order_type("TASCounter");
|
||||
// all 6 contracts. They can theoretically be inferred from the front month
|
||||
// since T1 are calendars 1 month apart starting from the front month
|
||||
// ex Mar-May, May-Jul, Jul-Sep being T1 front, second and third respectively
|
||||
// (assuming Mar is the front month)
|
||||
*join_desc.add_param_desc() = param_security_key("T1 Front");
|
||||
*join_desc.add_param_desc() = param_security_key("T1 Second");
|
||||
*join_desc.add_param_desc() = param_security_key("T1 Third");
|
||||
*join_desc.add_param_desc() = param_security_key("T2 Front");
|
||||
*join_desc.add_param_desc() = param_security_key("T2 Second");
|
||||
*join_desc.add_param_desc() = param_security_key("T3 Front");
|
||||
// tas contracts (could have been derived)
|
||||
*join_desc.add_param_desc() = param_security_key("T1 Front TAS");
|
||||
*join_desc.add_param_desc() = param_security_key("T1 Second TAS");
|
||||
*join_desc.add_param_desc() = param_security_key("T1 Third TAS");
|
||||
// automatic orders
|
||||
*join_desc.add_param_desc() = param_uint32("T1 Front Time");
|
||||
*join_desc.add_param_desc() = param_uint32("T1 Front Qty");
|
||||
*join_desc.add_param_desc() = param_bool("T1 Front Order Enabled");
|
||||
|
||||
*join_desc.add_param_desc() = param_uint32("T1 Second Time");
|
||||
*join_desc.add_param_desc() = param_uint32("T1 Second Qty");
|
||||
*join_desc.add_param_desc() = param_bool("T1 Second Order Enabled");
|
||||
|
||||
*join_desc.add_param_desc() = param_uint32("T1 Third Time");
|
||||
*join_desc.add_param_desc() = param_uint32("T1 Third Qty");
|
||||
*join_desc.add_param_desc() = param_bool("T1 Third Order Enabled");
|
||||
// settlement time
|
||||
*join_desc.add_param_desc() = param_uint32("Settlement Start Hour");
|
||||
*join_desc.add_param_desc() = param_uint32("Settlement Start Minutes");
|
||||
*join_desc.add_param_desc() = param_uint32("Settlement Start Seconds");
|
||||
*join_desc.add_param_desc() = param_uint32("Settlement Period (seconds)");
|
||||
|
||||
srv.register_dynamic_algo_order(join_desc, []() { return std::make_unique<orderman::TASCounterAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_tas_plugin(bts::SparkServices& srv) {
|
||||
DynamicOrderDescriptor tas_desc;
|
||||
|
||||
tas_desc.set_dynamic_order_type("TAS");
|
||||
|
||||
size_t n_child_ords = 8;
|
||||
for (size_t i=0; i< n_child_ords; i++){
|
||||
auto idx = std::to_string(i+1);
|
||||
*tas_desc.add_param_desc() = param_double("Order " + idx + " Price");
|
||||
*tas_desc.add_param_desc() = param_uint32("Order " + idx + " Quantity");
|
||||
*tas_desc.add_param_desc() = param_bool("Order " + idx + " Buy?");
|
||||
}
|
||||
srv.register_dynamic_algo_order(tas_desc, []() { return std::make_unique<orderman::TASAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_limit_cover(bts::SparkServices & srv) {
|
||||
DynamicOrderDescriptor lc_desc;
|
||||
lc_desc.set_dynamic_order_type("LimitCover");
|
||||
LimitCoverAdapter::define_proto(lc_desc);
|
||||
srv.register_dynamic_algo_order(lc_desc, []() { return std::make_unique<orderman::LimitCoverAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_on_signal_plugin(bts::SparkServices& srv) {
|
||||
DynamicOrderDescriptor on_signal;
|
||||
on_signal.set_dynamic_order_type("OnSignal");
|
||||
*on_signal.add_param_desc() = param_uint32("Time Hours");
|
||||
*on_signal.add_param_desc() = param_uint32("Time Minutes");
|
||||
*on_signal.add_param_desc() = param_uint32("Time Seconds");
|
||||
*on_signal.add_param_desc() = param_bool("Preopen");
|
||||
*on_signal.add_param_desc() = param_bool("Open");
|
||||
// contracts who's signals have to be received to send the algo
|
||||
*on_signal.add_param_desc() = param_security_key("Signal Contract 1");
|
||||
size_t n_child_ords = 10;
|
||||
for (size_t i=0; i< n_child_ords; i++){
|
||||
auto idx = std::to_string(i+1);
|
||||
*on_signal.add_param_desc() = param_security_key("Order " + idx + " Contract");
|
||||
*on_signal.add_param_desc() = param_order("Order " + idx);
|
||||
*on_signal.add_param_desc() = param_double("Order " + idx + " Price");
|
||||
*on_signal.add_param_desc() = param_int32("Order " + idx + " Quantity");
|
||||
}
|
||||
srv.register_dynamic_algo_order(on_signal, []() { return std::make_unique<orderman::OnSignalAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_spark_plugin(bts::SparkServices &srv) {
|
||||
auto& dyn_algo_config = config::get<config::toggles::DynamicAlgos>();
|
||||
if(dyn_algo_config.enable_dorman())
|
||||
init_dorman_plugin(srv);
|
||||
if(dyn_algo_config.enable_pyramid())
|
||||
init_pyramid_plugin(srv);
|
||||
if(dyn_algo_config.enable_tas())
|
||||
init_tas_plugin(srv);
|
||||
if(dyn_algo_config.enable_tascounter())
|
||||
init_tascounter_plugin(srv);
|
||||
if (dyn_algo_config.enable_limit_cover())
|
||||
init_limit_cover(srv);
|
||||
if (dyn_algo_config.enable_onsignal())
|
||||
init_on_signal_plugin(srv);
|
||||
}
|
||||
|
||||
}}}
|
||||
@@ -1,222 +0,0 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/TASAlgo.h"
|
||||
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoServices.h"
|
||||
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h"
|
||||
#include "lib/os/os.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include "lib/utils/datetime.h"
|
||||
#include <stdint.h>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
using namespace api::orderman;
|
||||
|
||||
enum class TASAlgo::Status { WAITING_FOR_SIGNAL, WORKING, DONE };
|
||||
|
||||
struct TASLegData{
|
||||
api::Side side; //trading side
|
||||
Price price;
|
||||
Quantity quantity;
|
||||
ChildTemplate* order_template = nullptr;
|
||||
OrderKey order_key;
|
||||
ChildState order_state;
|
||||
TASLegData (api::Side side,
|
||||
Price price,
|
||||
Quantity quantity,
|
||||
ChildTemplate* order_template)
|
||||
:
|
||||
side (side),
|
||||
price(price),
|
||||
quantity(quantity),
|
||||
order_template (order_template),
|
||||
order_key(null_order_key()),
|
||||
order_state(){
|
||||
order_state.status = ChildStatus::WORKING;
|
||||
}
|
||||
};
|
||||
|
||||
struct TASAlgo::PImpl {
|
||||
Status state = Status::WAITING_FOR_SIGNAL;
|
||||
IAlgoServices* algo_services = nullptr;
|
||||
SecurityKey sec_key = SecurityKey(0);
|
||||
TASAlgo::TASAlgoParams algo_params;
|
||||
std::vector<TASLegData> legs;
|
||||
bts::datetime::PTime start_time = bts::datetime::local_now();
|
||||
PImpl(){
|
||||
}
|
||||
TASLegData* find_leg_from_ord_key (OrderKey ord_key){
|
||||
for (auto& l : legs)
|
||||
if(l.order_key == ord_key)
|
||||
return &l;
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
TASAlgo::TASAlgo() : me(new PImpl()) {}
|
||||
TASAlgo::~TASAlgo() {}
|
||||
// initialization /////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
OrderOpResult TASAlgo::do_before_launch(IBeforeLaunchAlgoServices& serv) {
|
||||
auto& def = serv.definition();
|
||||
if (def.has_custom_def() && def.custom_def().has_dynamic()){
|
||||
const auto& sec = serv.security();
|
||||
me->sec_key = SecurityKey(sec.id());
|
||||
me->algo_params = _convert_params(def.custom_def());
|
||||
if (me->algo_params.child_orders.size() > 0){
|
||||
for (const auto& t : me->algo_params.child_orders){
|
||||
Price price = std::get<0>(t);
|
||||
Quantity quantity = std::get<1>(t);
|
||||
api::Side side = std::get<2>(t);
|
||||
auto* ordt = _create_order_template(
|
||||
serv,
|
||||
SecurityKey(sec.id()),
|
||||
side);
|
||||
me->legs.emplace_back(side,
|
||||
price,
|
||||
quantity,
|
||||
ordt);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {OrderError::ORDER_ERROR_INVALID_PARAMETER,
|
||||
std::string("Parameters are not well formed")};
|
||||
}
|
||||
void TASAlgo::do_launch(IAlgoServices& algo_services) {
|
||||
me->algo_services = &algo_services;
|
||||
_algo().subscribe_status(me->sec_key);
|
||||
//_algo().subscribe_timer();
|
||||
}
|
||||
void TASAlgo::do_launch_attached(IAlgoServices& algo_services,
|
||||
OrderKey,
|
||||
ChildState) {
|
||||
me->algo_services = &algo_services;
|
||||
_cxl_and_end(true, "TAS Algo can not be attached to working orders");
|
||||
}
|
||||
// updates ////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void TASAlgo::do_update(const api::orderman::Update&) {
|
||||
LOG(OrderMan, Warn, "TASAlgo::do_update order:{} state:{} update not allowed ",
|
||||
_algo().order_key().value(),
|
||||
static_cast<int>(me->state));
|
||||
}
|
||||
// callbacks //////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void TASAlgo::on_book(SecurityKey, const Book&) {
|
||||
}
|
||||
void TASAlgo::on_child_change(OrderKey key,
|
||||
ChildState const &st){
|
||||
LOG(OrderMan, Info,
|
||||
"TASAlgo::on_child_change key:{} child_key:{} state:{} status:{} "
|
||||
"total_fills:{} last_price:{}",
|
||||
_algo().order_key(),
|
||||
key,
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(st.status),
|
||||
st.total_fills,
|
||||
st.last_price);
|
||||
TASLegData* ld_ptr = me->find_leg_from_ord_key(key);
|
||||
if (ld_ptr != nullptr)
|
||||
ld_ptr->order_state = st;
|
||||
for (const auto& l : me->legs)
|
||||
if(l.order_state.status == ChildStatus::WORKING)
|
||||
return;
|
||||
_cxl_and_end(false,"");
|
||||
}
|
||||
void TASAlgo::on_status(SecurityKey key, co2::TradingStatus status){
|
||||
if(key == me->sec_key &&
|
||||
status == co2::TradingStatus::TRADINGSTATUS_PRE_OPEN){
|
||||
switch (me->state){
|
||||
case TASAlgo::Status::WAITING_FOR_SIGNAL:
|
||||
for (auto& l : me->legs){
|
||||
l.order_key = _algo().launch_child(
|
||||
*l.order_template,
|
||||
l.price,
|
||||
l.quantity);
|
||||
LOG(OrderMan, Info,"TASAlgo::on_status launching order key:{} "
|
||||
"child_key:{} price:{} qty:{}",
|
||||
_algo().order_key(),
|
||||
l.order_key,
|
||||
l.price,
|
||||
l.quantity);
|
||||
}
|
||||
_switch_state(TASAlgo::Status::WORKING);
|
||||
break;
|
||||
case TASAlgo::Status::WORKING:
|
||||
case TASAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
void TASAlgo::on_timer(){
|
||||
if (bts::datetime::msecs_since(me->start_time) > 10000)
|
||||
on_status(me->sec_key,co2::TradingStatus::TRADINGSTATUS_PRE_OPEN);
|
||||
}
|
||||
IAlgoServices& TASAlgo::_algo() const { return *me->algo_services; }
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// stateless conversion ///////////////////////////////////////////////////////
|
||||
TASAlgo::TASAlgoParams TASAlgo::_convert_params (
|
||||
const bts::api::orderman::CustomOrderDefinition& cod){
|
||||
const DynamicOrderDefinition& def = cod.dynamic();
|
||||
const auto& params = def.order_params();
|
||||
TASAlgo::TASAlgoParams rv;
|
||||
for (size_t i = 0; i < 1024; i++){
|
||||
auto idx = std::to_string(i+1);
|
||||
auto price = get_if<double>(params, "Order " + idx + " Price");
|
||||
auto qty = get_if<uint32_t>(params, "Order " + idx + " Quantity");
|
||||
auto is_buy = get_if<bool>(params, "Order " + idx + " Buy?");
|
||||
if(price.has_value() && qty.has_value() && qty.value() != 0){
|
||||
api::Side side = is_buy.has_value() ?
|
||||
(is_buy.value() ? api::Side::SIDE_BUY : api::Side::SIDE_SELL) :
|
||||
api::Side::SIDE_SELL;
|
||||
rv.child_orders.emplace_back(Price(price.value()), Quantity(qty.value()), side);
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
ChildTemplate* TASAlgo::_create_order_template (
|
||||
IBeforeLaunchAlgoServices& serv,
|
||||
SecurityKey key,
|
||||
api::Side side){
|
||||
OrderDefinition order_def = serv.definition();
|
||||
order_def.set_security_key (key.value());
|
||||
order_def.set_side(side);
|
||||
order_def.mutable_custom_def()->mutable_limit()->MergeFrom (
|
||||
api::orderman::LimitOrderDefinition());
|
||||
return &(serv.make_child_template(order_def));
|
||||
}
|
||||
void TASAlgo::_switch_state(TASAlgo::Status new_state){
|
||||
switch (new_state){
|
||||
case (TASAlgo::Status::WAITING_FOR_SIGNAL):
|
||||
bts_assert(false);
|
||||
break;
|
||||
case (TASAlgo::Status::WORKING):
|
||||
bts_assert(me->state == TASAlgo::Status::WAITING_FOR_SIGNAL);
|
||||
break;
|
||||
case (TASAlgo::Status::DONE):
|
||||
bts_assert(me->state != TASAlgo::Status::DONE);
|
||||
break;
|
||||
}
|
||||
LOG(OrderMan, Info,"TASAlgo::_switch_state key:{} {}->{} ",
|
||||
_algo().order_key(),
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(new_state));
|
||||
me->state = new_state;
|
||||
}
|
||||
|
||||
void TASAlgo::_cxl_and_end (bool is_error, const std::string& err_msg){
|
||||
if(is_error)
|
||||
_algo().terminate_error(err_msg);
|
||||
else
|
||||
_algo().complete();
|
||||
_switch_state(TASAlgo::Status::DONE);
|
||||
}
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -1,45 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
class ChildTemplate;
|
||||
struct LegData;
|
||||
|
||||
class TASAlgo : public IAlgoOrder {
|
||||
public:
|
||||
TASAlgo();
|
||||
~TASAlgo();
|
||||
struct TASAlgoParams{
|
||||
std::vector<std::tuple<Price, Quantity, api::Side>> child_orders;
|
||||
};
|
||||
public:
|
||||
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_book(SecurityKey, Book const &) override;
|
||||
void on_child_change(OrderKey, ChildState const &) override;
|
||||
void on_status(SecurityKey, co2::TradingStatus) override;
|
||||
void on_timer() override;
|
||||
|
||||
private:
|
||||
enum class Status;
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me;
|
||||
private:
|
||||
IAlgoServices& _algo() const ;
|
||||
static TASAlgoParams _convert_params (
|
||||
const bts::api::orderman::CustomOrderDefinition&);
|
||||
static ChildTemplate* _create_order_template (IBeforeLaunchAlgoServices&,
|
||||
SecurityKey,
|
||||
api::Side);
|
||||
// acts on md, cognizant of the state
|
||||
void _switch_state (Status new_status);
|
||||
void _cxl_and_end (bool is_error, const std::string& err_msg);
|
||||
};
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user