Compare commits
79
Commits
2.15.10_rc3
...
2.16.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c30bab899f | ||
|
|
46544f36f1 | ||
|
|
c7df3ac43c | ||
|
|
400143c84d | ||
|
|
479f3dccff | ||
|
|
c272d8a8c2 | ||
|
|
fbd8bbd4a8 | ||
|
|
3da52b350f | ||
|
|
a69118c5d3 | ||
|
|
5accd0e2b4 | ||
|
|
df5ad61cd2 | ||
|
|
4a7674eb29 | ||
|
|
5a458287f4 | ||
|
|
5921f9f6b5 | ||
|
|
4c73a63ae8 | ||
|
|
42b1c921a8 | ||
|
|
fab3334b1e | ||
|
|
7239dd3ade | ||
|
|
dd4c8eb649 | ||
|
|
76a29bdc8a | ||
|
|
06d708f3ea | ||
|
|
e2f70cbc19 | ||
|
|
9d54e47a03 | ||
|
|
faccdf581e | ||
|
|
1a64714a54 | ||
|
|
3ba6792a85 | ||
|
|
10e75f30e6 | ||
|
|
20eac19c4f | ||
|
|
8c0765a2c7 | ||
|
|
7a1f88e813 | ||
|
|
3420f14c8a | ||
|
|
86277dfbfe | ||
|
|
4839e464c6 | ||
|
|
dc2ad071a9 | ||
|
|
65e49992f9 | ||
|
|
afbb9ae92d | ||
|
|
ff088142d9 | ||
|
|
93b7cf346b | ||
|
|
8ee9b84488 | ||
|
|
e3648f5373 | ||
|
|
facd3a9d81 | ||
|
|
84969cb481 | ||
|
|
6d521e77db | ||
|
|
82c49f8f49 | ||
|
|
e074309da1 | ||
|
|
2d622a5f39 | ||
|
|
d51d20e84c | ||
|
|
2ccdef9dc5 | ||
|
|
42e62a30bf | ||
|
|
c233c607fa | ||
|
|
462cb930f7 | ||
|
|
550a4e8d50 | ||
|
|
99b8221efc | ||
|
|
19e74c30f8 | ||
|
|
26693a4c98 | ||
|
|
d8bdb69a7c | ||
|
|
6d357a74e1 | ||
|
|
73f4e4814c | ||
|
|
312bc4be15 | ||
|
|
a0c3ba449a | ||
|
|
d5a2a7f3f0 | ||
|
|
eda8c94def | ||
|
|
9a8e80e149 | ||
|
|
6a7e3d739d | ||
|
|
e6b8274813 | ||
|
|
9783520c62 | ||
|
|
7e7f0bc37f | ||
|
|
9d1b03d1e2 | ||
|
|
9a677cf980 | ||
|
|
25445d58d0 | ||
|
|
0b313a8aa8 | ||
|
|
54f954fd4b | ||
|
|
980f38ba03 | ||
|
|
9eada2724b | ||
|
|
b5f53ee918 | ||
|
|
efc175f654 | ||
|
|
6074afd999 | ||
|
|
219dff4055 | ||
|
|
9a3b07b622 |
No files matched your search
@@ -1,9 +1,22 @@
|
||||
+++
|
||||
+++
|
||||
title = "Release notes"
|
||||
weight = 3
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
## 2.16.2
|
||||
### User Visible Change
|
||||
- Synthetic creation dialog has a "fractional" checkbox next to the tick settings. Users may uncheck this box to enter integer tick sizes, so the synthetic security's prices will be displayed without decimals, or fractional units.
|
||||
## 2.16.1
|
||||
### Bug Fixes
|
||||
- OrderBook's algo state parameters (in the panel on the right side) are sorted alphabetically
|
||||
## 2.16.0
|
||||
### Bug Fixes
|
||||
- Solved an issue that could result in incorrect ladder levels
|
||||
- Solved an issue that could lead to incorrect routing of orders in the event of certain MSGW changes
|
||||
### API Changes
|
||||
- Add C# client side API samples for order entry, position monitoring and market data observation
|
||||
|
||||
## 2.15.10
|
||||
### Bug Fixes
|
||||
- Solved an issue that could cause the user interface to fail when playing some alert sounds
|
||||
@@ -715,3 +728,4 @@ insert_anchor_links = "right"
|
||||
- Start, Day and Total PnL columns are painted with a red or green background color based on if the pnl value is negative or positive respectively
|
||||
- Corrected cash per point values, used in pnl calculations, for a number of products
|
||||
- PnL report will now remember any customizations you make to filtering and/or sorting criteria
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "2.1.508",
|
||||
"rollForward": "latestMajor"
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@
|
||||
<GenDir>$(BaseOutputPath)\gen\$(MSBuildProjectName)</GenDir>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(VCTargetsPath)'==''">
|
||||
<VCTargetsPath>C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\IDE\VC\VCTargets</VCTargetsPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<Import Project="$(ComputerSpecificConfig)" Condition="Exists($(ComputerSpecificConfig))" />
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using bts.api;
|
||||
using bts.co2;
|
||||
using bts.utils;
|
||||
using bts.utils.csv;
|
||||
using mbts;
|
||||
using mbts.spark;
|
||||
using mbts.ui.controls;
|
||||
@@ -44,6 +45,18 @@ namespace bts.shell.time_and_sales {
|
||||
public void raise_invalidated() =>
|
||||
invalidated?.Invoke();
|
||||
|
||||
public static System.Collections.Specialized.OrderedDictionary csv_ordered_properties =>
|
||||
CsvHelper.create_ordering_maintainer(
|
||||
typeof(TimeAndSalesRowVM),
|
||||
new string[] {
|
||||
nameof(security),
|
||||
nameof(time),
|
||||
nameof(price),
|
||||
nameof(size),
|
||||
nameof(aggressor_side),
|
||||
nameof(security_key),
|
||||
});
|
||||
|
||||
public bool Equals(TimeAndSalesRowVM other) =>
|
||||
other != null &&
|
||||
row_key == other.row_key;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using bts.shell.fills;
|
||||
using bts.spark;
|
||||
using bts.view;
|
||||
using bts.utils.csv;
|
||||
using mbts;
|
||||
using mbts.co2;
|
||||
using mbts.persist;
|
||||
@@ -114,6 +115,8 @@ namespace bts.shell.time_and_sales {
|
||||
show_in_context_menu = _can_pause_updates
|
||||
};
|
||||
_commands = new SparkCommand[] {
|
||||
// csv export
|
||||
new SparkCommandCommand("Export to csv", Icons.Export, FileDialogCommand.export_csv(() => _vm.rows.ToCsv(TimeAndSalesRowVM.csv_ordered_properties), "TaS")),
|
||||
// ladder
|
||||
SparkCommands.ShowLadderAutoLookup(SparkCommands.KeyGestures.ShowLadder)
|
||||
.complete(current_sec_key_src, sec_keys => sec_keys.Count > 0),
|
||||
|
||||
@@ -549,24 +549,48 @@
|
||||
</u:BindingUpdateGroupCollection>
|
||||
</u:Behavior.BindingUpdateGroups>
|
||||
</TextBox>
|
||||
<TextBlock
|
||||
<StackPanel
|
||||
x:Name="PART_TickFactors_Label"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Text="Ticks"
|
||||
IsEnabled="True"
|
||||
Orientation="Horizontal"
|
||||
Visibility="{Binding LegTuples.Count, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IntNotZeroVisibilityConverter}}"
|
||||
/>
|
||||
<c:TickFactorsPicker
|
||||
>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Text="Ticks ("
|
||||
/>
|
||||
<CheckBox
|
||||
Margin="1,0,0,0"
|
||||
IsChecked="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
VerticalAlignment="Center"
|
||||
VerticalContentAlignment="Center"
|
||||
Padding="0"
|
||||
>
|
||||
fractional
|
||||
</CheckBox>
|
||||
<TextBlock VerticalAlignment="Center" Margin="1,0,0,0">)</TextBlock>
|
||||
</StackPanel>
|
||||
<Grid
|
||||
x:Name="PART_TickFactors_Picker"
|
||||
Grid.Column="3"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
HorizontalAlignment="Stretch"
|
||||
IsEnabled="True"
|
||||
SelectedTickFactors="{Binding TickFactors, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
|
||||
Margin="{StaticResource Margin.LHeaderPanelItem}"
|
||||
Visibility="{Binding LegTuples.Count, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource IntNotZeroVisibilityConverter}}"
|
||||
/>
|
||||
>
|
||||
<c:TickFactorsPicker
|
||||
HorizontalAlignment="Stretch"
|
||||
SelectedTickFactors="{Binding TickFactors, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}"
|
||||
Visibility="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}, Converter={x:Static cv:BooleanToVisibilityConverter.HiddenInstance}}"
|
||||
/>
|
||||
<c:TickUpDownInt
|
||||
HorizontalAlignment="Stretch"
|
||||
Minimum="1"
|
||||
Value="{Binding IntegerTickSize, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Visibility="{Binding UseFractionalTicks, RelativeSource={RelativeSource TemplatedParent}, Converter={StaticResource FalseVisibilityConverter}}"
|
||||
/>
|
||||
</Grid>
|
||||
<TextBlock
|
||||
x:Name="PART_Pricing_Label"
|
||||
Grid.Column="4"
|
||||
|
||||
@@ -52,6 +52,26 @@ namespace mbts.ui.spark {
|
||||
public TickFactors TickFactors { get => GetValue(TickFactorsProperty) as TickFactors; set => SetValue(TickFactorsProperty, value); }
|
||||
#endregion
|
||||
|
||||
#region UseFractionalTicks
|
||||
public static readonly DependencyProperty UseFractionalTicksProperty = WPFHelper.create_dp<bool, ExecutionStrategyEditor>(
|
||||
nameof(UseFractionalTicks), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, true);
|
||||
|
||||
public bool UseFractionalTicks {
|
||||
get => (bool)GetValue(UseFractionalTicksProperty);
|
||||
set => SetValue(UseFractionalTicksProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IntegerTickSize
|
||||
public static readonly DependencyProperty IntegerTickSizeProperty = WPFHelper.create_dp<int, ExecutionStrategyEditor>(
|
||||
nameof(IntegerTickSize), FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, 1);
|
||||
|
||||
public int IntegerTickSize {
|
||||
get => (int)GetValue(IntegerTickSizeProperty);
|
||||
set => SetValue(IntegerTickSizeProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region IsNew
|
||||
public static readonly DependencyProperty IsNewProperty =
|
||||
DependencyProperty.Register(
|
||||
|
||||
@@ -36,6 +36,8 @@
|
||||
ContractPickerModel="{Binding contract_picker_model}"
|
||||
HedgePricing="{Binding hedge_pricing}"
|
||||
HedgePricingItemsSource="{Binding hedge_pricing_options}"
|
||||
UseFractionalTicks="{Binding use_fractional_ticks}"
|
||||
IntegerTickSize="{Binding integer_tick_size}"
|
||||
/>
|
||||
<c:DialogSkirtOKCancel Grid.Row="1" OKCommand="{Binding commit}" CloseOnOK="False"/>
|
||||
</Grid>
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using bts.collections;
|
||||
using bts.shell;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.spark;
|
||||
@@ -30,8 +29,16 @@ namespace mbts.spark {
|
||||
readonly Property<bool> _has_working_orders, _can_add_leg;
|
||||
|
||||
Security _synthetic;
|
||||
bool _set_name_automatically, _set_ticks_automatically, _set_quantities_automatically, _set_price_mults_automatically, _is_setting_quantities, _is_setting_price_mults;
|
||||
bool
|
||||
_set_name_automatically,
|
||||
_set_ticks_automatically,
|
||||
_set_quantities_automatically,
|
||||
_set_price_mults_automatically,
|
||||
_is_setting_quantities,
|
||||
_is_setting_price_mults,
|
||||
_use_fractional_ticks = true;
|
||||
TickFactors _tick_factors;
|
||||
int _integer_tick_size = 1;
|
||||
ExecutionStrategyImmutable _strategy;
|
||||
HedgePricing _hedge_pricing;
|
||||
|
||||
@@ -44,6 +51,10 @@ namespace mbts.spark {
|
||||
: this(synthetic, strategy, false) {
|
||||
ExceptionAssert.InvalidOperation.That(synthetic.Id != 0, "This constructor may only be used for already-existing synthetic securities (id must be non-zero).");
|
||||
_tick_factors = TickUtils.get_regional_ticks(synthetic.ContractSpec).low_tick;
|
||||
if (TickUtils.is_integer_tick(synthetic.ContractSpec)) {
|
||||
_use_fractional_ticks = false;
|
||||
_integer_tick_size = DoubleHelper.to_optional_int32(synthetic.ContractSpec.LowTick) ?? 0;
|
||||
}
|
||||
title = $"Edit '{synthetic.Synthetic.Name}'";
|
||||
icon = Icons.ModifySynthetic;
|
||||
_has_working_orders = backing(nameof(has_working_orders), security_has_working_orders.OnDispatcher());
|
||||
@@ -131,14 +142,14 @@ namespace mbts.spark {
|
||||
flash_error_msg("Name already taken", SparkUtils.UI.MessageFlashDuration, nameof(name)).ignore();
|
||||
valid = false;
|
||||
}
|
||||
if (_tick_factors == null || !_tick_factors.is_valid()) {
|
||||
flash_error_msg("Please select ticks", SparkUtils.UI.MessageFlashDuration, nameof(tick_factors)).ignore();
|
||||
if (use_fractional_ticks ? (_tick_factors == null || !_tick_factors.is_valid()) : integer_tick_size == 0) {
|
||||
flash_error_msg("Please select ticks", SparkUtils.UI.MessageFlashDuration, use_fractional_ticks ? nameof(tick_factors) : nameof(integer_tick_size)).ignore();
|
||||
valid = false;
|
||||
}
|
||||
if (valid) {
|
||||
_synthetic.Synthetic.Legs.SetItems(leg_tuples.Select(l => l.leg));
|
||||
_synthetic.Symbol = _deduce_symbol(_synthetic);
|
||||
_synthetic.ContractSpec = _tick_factors.to_spec();
|
||||
_synthetic.ContractSpec = use_fractional_ticks ? _tick_factors.to_spec() : TickUtils.create_integer_tick_spec(integer_tick_size);
|
||||
uint? id = await model.create_synthetic(new SecurityImmutable(_synthetic));
|
||||
if (id.HasValue && id != 0) {
|
||||
if (await model.save_strategy(id.Value, get_strategy())) {
|
||||
@@ -295,6 +306,20 @@ namespace mbts.spark {
|
||||
public bool has_working_orders => _has_working_orders.Value;
|
||||
public bool is_new { get; }
|
||||
public bool is_new_leg_popup_open { get; set; }
|
||||
public bool use_fractional_ticks {
|
||||
get => _use_fractional_ticks;
|
||||
set {
|
||||
if (Set(ref _use_fractional_ticks, value))
|
||||
_set_ticks_automatically = false;
|
||||
}
|
||||
}
|
||||
public int integer_tick_size {
|
||||
get => _integer_tick_size;
|
||||
set {
|
||||
if (Set(ref _integer_tick_size, value))
|
||||
_set_ticks_automatically = false;
|
||||
}
|
||||
}
|
||||
public bool can_add_leg => _can_add_leg.Value;
|
||||
public SyntheticContract.Types.PriceCalc price_calc { get => _synthetic.Synthetic.PriceCalc; set => _synthetic.Synthetic.PriceCalc = value; }
|
||||
public ObservableCollection<LegSpreaderParamsTuple> leg_tuples { get; } = new ObservableCollection<LegSpreaderParamsTuple>();
|
||||
@@ -323,6 +348,16 @@ namespace mbts.spark {
|
||||
_strategy = strategy;
|
||||
hedge_pricing = strategy.HedgePricing;
|
||||
leg_tuples.SetItems(_get_legs(_synthetic, strategy));
|
||||
if (synthetic.HasContractSpec) {
|
||||
/// assign the fields <see cref="_use_fractional_ticks"/> and <see cref="_integer_tick_size"/>,
|
||||
/// rather than the properties <see cref="use_fractional_ticks"/> and <see cref="integer_tick_size"/>
|
||||
/// because the setters of the former flip <see cref="_set_ticks_automatically"/> to false if the value changes.
|
||||
/// This behavior causes the ticks to be automatically deduced as legs are added/removed,
|
||||
/// until the user explicitly enters a different value (as the properties are bound to tick input controls in the view).
|
||||
Set(ref _use_fractional_ticks, !TickUtils.is_integer_tick(synthetic.ContractSpec), nameof(use_fractional_ticks));
|
||||
if (!use_fractional_ticks)
|
||||
Set(ref _integer_tick_size, DoubleHelper.to_optional_int32(synthetic.ContractSpec.LowTick) ?? 0, nameof(integer_tick_size));
|
||||
}
|
||||
RaisePropertyChanged(nameof(name), nameof(tick_factors));
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ using bts.collections;
|
||||
using bts.shell.fills;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using bts.utils.csv;
|
||||
using mbts.collections;
|
||||
using mbts.persist;
|
||||
using mbts.spark.filters;
|
||||
@@ -330,6 +331,27 @@ namespace mbts.spark {
|
||||
public override bool Equals(object obj) => Equals(obj as Row);
|
||||
public override int GetHashCode() => _fill.FillKey.GetHashCode() ^ _order.Key.GetHashCode();
|
||||
|
||||
public static System.Collections.Specialized.OrderedDictionary csv_ordered_properties =>
|
||||
CsvHelper.create_ordering_maintainer(typeof(Row),
|
||||
new string[] {
|
||||
nameof(security),
|
||||
nameof(order_type),
|
||||
nameof(root_algo_display_name),
|
||||
nameof(side),
|
||||
nameof(fill_time),
|
||||
nameof(user),
|
||||
nameof(account),
|
||||
nameof(exchange),
|
||||
nameof(time_in_force),
|
||||
nameof(size),
|
||||
nameof(price),
|
||||
nameof(fill_kind),
|
||||
nameof(fill_status),
|
||||
nameof(fill_key),
|
||||
nameof(order_key),
|
||||
nameof(security_key)
|
||||
});
|
||||
|
||||
public event Action invalidated;
|
||||
void IRowVM.raise_invalidated() => invalidated?.Invoke();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using bts.shell;
|
||||
using bts.shell.fills;
|
||||
using bts.spark;
|
||||
using bts.view;
|
||||
using bts.utils.csv;
|
||||
using mbts.persist;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
@@ -61,6 +62,7 @@ namespace mbts.spark {
|
||||
_view = new FillsView { DataContext = _vm };
|
||||
|
||||
_commands = new List<SparkCommand> {
|
||||
new SparkCommandCommand("Export to csv", Icons.Export, FileDialogCommand.export_csv(() => _vm.rows.ToCsv(FillsVM.Row.csv_ordered_properties), "Fills")),
|
||||
// wrap the grid settings view factory in a dialogCommand here since the vm is destroyed/recreated when the security criteria changes
|
||||
SparkCommands.window_settings_command(new DialogCommand(() => {
|
||||
var vm = new ShellSettingsDialogVM<GridSettings, SimpleGridSettingsVM>(
|
||||
|
||||
@@ -143,7 +143,7 @@ namespace mbts.spark {
|
||||
string candidate = string.Empty;
|
||||
if (a.HasValue) {
|
||||
var algo_name = a.Value.name;
|
||||
var words = algo_name.Split(delim);
|
||||
var words = algo_name.Split(delim, StringSplitOptions.RemoveEmptyEntries);
|
||||
if (words.Length == 1) {
|
||||
var single_word = words[0];
|
||||
if (single_word.Length > 1) {
|
||||
@@ -152,7 +152,7 @@ namespace mbts.spark {
|
||||
candidate = single_word;
|
||||
}
|
||||
} else {
|
||||
candidate = words.Take(2).Select(w => w.Substring(0, 1)).list_string(string.Empty);
|
||||
candidate = string.Join(string.Empty, words.Take(2).Select(w => w.Substring(0, 1)));
|
||||
}
|
||||
}
|
||||
return candidate.ToUpper();
|
||||
|
||||
@@ -534,13 +534,44 @@ namespace mbts.spark {
|
||||
_size_from_row(row, _price_list, _asks, _vm.security.ContractSpec);
|
||||
|
||||
uint _volume_from_row(int row) {
|
||||
if (_price_list.SafeTryGetAt(row, out var price)) {
|
||||
var vapls = _volume_container.read_new_value();
|
||||
if (vapls != null) {
|
||||
return vapls.SelectFirstOrDefault(v => MathUtils.epsilon_equals(v.Price, price), v => v.Volume);
|
||||
}
|
||||
const int max_tries = 3;
|
||||
var errors = new List<string>(0);
|
||||
int num_tries = 0;
|
||||
for (; num_tries < max_tries; num_tries++) {
|
||||
try {
|
||||
var rv = impl();
|
||||
if (num_tries > 0) {
|
||||
log_errors(true);
|
||||
}
|
||||
return rv;
|
||||
} catch (Exception ex) {
|
||||
errors.Add(ex.ToString());
|
||||
}
|
||||
}
|
||||
log_errors(false);
|
||||
return 0;
|
||||
|
||||
void log_errors(bool succeeded) {
|
||||
// Only log distinct errors to avoid useless noise.
|
||||
// It's likely that the exception will always be the same (IndexOutOfRange),
|
||||
// however, this bug is not fully understood, so assume nothing.
|
||||
var error_msg = string.Join("; ", errors.Distinct());
|
||||
if (succeeded) {
|
||||
Log.Ui.error($"Ladder '{security}' got volume for row at index '{row}' after {num_tries + 1} attempts (price_list array length is {_price_list.Length}). The following exceptions were observed: {error_msg}");
|
||||
} else {
|
||||
Log.Ui.error($"Ladder '{security}' failed to get volume for row at index '{row}' after {max_tries} attempts (price_list array length is {_price_list.Length}): {error_msg}");
|
||||
}
|
||||
}
|
||||
|
||||
uint impl() {
|
||||
if (_price_list.SafeTryGetAt(row, out var price)) {
|
||||
var vapls = _volume_container.read_new_value();
|
||||
if (vapls != null) {
|
||||
return vapls.SelectFirstOrDefault(v => MathUtils.epsilon_equals(v.Price, price), v => v.Volume);
|
||||
}
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.spark;
|
||||
using bts.utils;
|
||||
using bts.utils.csv;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using System;
|
||||
@@ -11,6 +12,7 @@ using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Linq;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
using Columns = mbts.spark.OrderBookGridView.ColumnKeys;
|
||||
@@ -59,6 +61,35 @@ namespace mbts.spark {
|
||||
public event Action<(OrderSecurityRootPair oldval, OrderSecurityRootPair newval)> updated;
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
|
||||
public static System.Collections.Specialized.OrderedDictionary csv_ordered_properties =>
|
||||
CsvHelper.create_ordering_maintainer(
|
||||
typeof(OrderBookRowVM),
|
||||
new string[] {
|
||||
nameof(security_text),
|
||||
nameof(symbol),
|
||||
nameof(time_created),
|
||||
nameof(last_change),
|
||||
nameof(user),
|
||||
nameof(account),
|
||||
nameof(exchange),
|
||||
nameof(time_in_force),
|
||||
nameof(order_type),
|
||||
nameof(root_algo_display_name),
|
||||
nameof(order_sub_type),
|
||||
nameof(side),
|
||||
nameof(quantity),
|
||||
nameof(filled_quantity),
|
||||
nameof(price),
|
||||
nameof(avg_price),
|
||||
nameof(last_price),
|
||||
nameof(piq),
|
||||
nameof(status),
|
||||
// last message (could contains commas, so skipping)
|
||||
nameof(security_key),
|
||||
nameof(order_key),
|
||||
nameof(parent_order_key),
|
||||
});
|
||||
|
||||
// Core
|
||||
public OrderSecurityRootPair order_tuple { get; private set; }
|
||||
public OrderRecordImmutable order => order_tuple.order.order;
|
||||
|
||||
@@ -51,6 +51,7 @@ namespace mbts.spark {
|
||||
readonly OrderParamsAggregator _selected_params_mon;
|
||||
readonly CommonValueMonitor<SecurityImmutable?> _selected_security_mon = new CommonValueMonitor<SecurityImmutable?>();
|
||||
readonly RightPanelModel _right_panel = new RightPanelModel();
|
||||
readonly CustomStateSynchronizer _custom_state_sync;
|
||||
OrderBookFillsGridSettings _cached_fills_settings = OrderBookStartupData.DefaultFillsGrid;
|
||||
double
|
||||
_raw_right_panel_width = OrderBookStartupData.Default.right_panel_width,
|
||||
@@ -124,6 +125,8 @@ namespace mbts.spark {
|
||||
_row_collection.CollectionChanged += _row_collection_CollectionChanged;
|
||||
_current_row = backing<OrderBookRowVM>(nameof(current_row));
|
||||
_columns.filter_changed += _on_filter_changed;
|
||||
_custom_state_sync = new CustomStateSynchronizer(this);
|
||||
// commands
|
||||
collapse_all_rows_command = new DelegateCommand(_row_collection.collapse_all);
|
||||
expand_all_rows_command = new DelegateCommand(_row_collection.expand_all);
|
||||
var modify_selected_orders_async_cmd = new AsyncCommand(_modify_selected_orders_async, () => _selected_params_mon.all_active && order_mod_vm.is_dirty);
|
||||
@@ -188,56 +191,7 @@ namespace mbts.spark {
|
||||
security_plurality = p;
|
||||
RaisePropertyChanged(nameof(security_plurality));
|
||||
}),
|
||||
Observable.Create<OrderBookRowVM>(o => {
|
||||
OrderBookRowVM last_fired = null;
|
||||
selected_order_changed += on_selected_order_changed;
|
||||
var sub = _current_row.Subscribe(ch => notify());
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void on_selected_order_changed(OrderRecordImmutable? _) =>
|
||||
notify();
|
||||
|
||||
void notify() {
|
||||
var cr = current_row_if_distinct_root_selected();
|
||||
if (!cr.safe_equals(last_fired))
|
||||
o.OnNext(last_fired = cr);
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
Disposable.dispose(ref sub);
|
||||
selected_order_changed -= on_selected_order_changed;
|
||||
}
|
||||
})
|
||||
.SubscribeEach(r => {
|
||||
if (r == null) {
|
||||
current_algo_custom_state.Clear();
|
||||
return Disposable.empty;
|
||||
}
|
||||
r.updated += on_updated;
|
||||
refresh_custom_state();
|
||||
return Disposable.Create(() => r.updated -= on_updated);
|
||||
void on_updated((OrderSecurityRootPair oldval, OrderSecurityRootPair newval) _) =>
|
||||
refresh_custom_state();
|
||||
|
||||
void refresh_custom_state() {
|
||||
current_algo_custom_state.Clear();
|
||||
var dcs = r.order.dynamic_custom_state();
|
||||
// while dcs is not expected to be null, best to play it safe and double check.
|
||||
if (dcs.is_null_or_empty()) {
|
||||
var child_rows = _row_collection.get_descendent_rows(r.order_key);
|
||||
if (_row_collection.filter != null) {
|
||||
child_rows = child_rows.Where(cr => _row_collection.filter(cr.order_tuple));
|
||||
}
|
||||
foreach (var dr in child_rows) {
|
||||
dcs = dr.order.dynamic_custom_state();
|
||||
if (!dcs.is_null_or_empty())
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach (var p in dcs.empty_if_null().OrderBy(kv => kv.Key))
|
||||
current_algo_custom_state.Add(new NamedValueVM(p.Key, DynamicOrderHelper.fmt_param(p.Value)));
|
||||
}
|
||||
}),
|
||||
_current_row.Subscribe(_ => _custom_state_sync.sync()),
|
||||
orders_source.OnDispatcher().Subscribe(t => {
|
||||
if (_selected_rows.Count == 0) {
|
||||
_cached_orders = t.state;
|
||||
@@ -257,6 +211,8 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
}
|
||||
_custom_state_sync.sync(true);
|
||||
orders_changed?.Invoke();
|
||||
}),
|
||||
model.on_orders_can_attach.OnDispatcher().Subscribe(table => {
|
||||
_cached_attachable = new MappedDictionary<uint, KeyValuePair<uint, bool>, bool>(table.state, kv => kv.Value);
|
||||
@@ -402,15 +358,6 @@ namespace mbts.spark {
|
||||
}
|
||||
void on_end_attaching() => Disposable.dispose(ref attach_vm_hooks);
|
||||
|
||||
// the output grid (custom state) in the right panel
|
||||
// will only display info for the 'current_row'
|
||||
// if all selected rows are of the same root order.
|
||||
OrderBookRowVM current_row_if_distinct_root_selected() =>
|
||||
current_row != null &&
|
||||
selected_rows.Select(r => r.order_tuple.root.order.Key).Distinct().TryGetSingle(out uint droot) &&
|
||||
current_row.order_tuple.root.order.Key == droot ?
|
||||
current_row : null;
|
||||
|
||||
IDisposable connect_order_mod_vm() =>
|
||||
Disposable.Create(
|
||||
_selected_params_mon.subscribe_algos().OnDispatcher().Subscribe(order_mod_vm.algo_vm.load_many),
|
||||
@@ -434,7 +381,7 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public event Action<OrderRecordImmutable?> selected_order_changed;
|
||||
public event Action begin_attaching, end_attaching;
|
||||
public event Action begin_attaching, end_attaching, orders_changed;
|
||||
public event Action<(IEnumerable, bool)> request_selection;
|
||||
|
||||
public ObservableAppendCollection<DIYGrid.ColumnBase> columns { get; }
|
||||
@@ -534,6 +481,28 @@ namespace mbts.spark {
|
||||
public IObservable<bool> subscribe_show_fills() => _show_fills.observe_current(true);
|
||||
public IObservable<bool> subscribe_show_ladder() => _show_ladder.observe_current(true);
|
||||
public IObservable<bool> subscribe_show_order_params() => _show_order_params.observe_current(true);
|
||||
public IObservable<OrderSecurityAlgo?> subscribe_single_selected_order() =>
|
||||
Observable.Create<OrderSecurityAlgo?>(o => {
|
||||
OrderSecurityAlgo? last_fired = null;
|
||||
orders_changed += notify;
|
||||
_current_row.Changed += on_current_row_changed;
|
||||
notify();
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void notify() {
|
||||
var next = current_row == null ? (OrderSecurityAlgo?)null : current_row.order_tuple.order;
|
||||
if (next != last_fired)
|
||||
o.OnNext(last_fired = next);
|
||||
}
|
||||
|
||||
void on_current_row_changed(OrderBookRowVM oldval, OrderBookRowVM newval) =>
|
||||
notify();
|
||||
|
||||
void dispose() {
|
||||
orders_changed -= notify;
|
||||
_current_row.Changed -= on_current_row_changed;
|
||||
}
|
||||
});
|
||||
|
||||
public void row_selection_changed(SelectionChangedEventArgs e) {
|
||||
try {
|
||||
@@ -563,6 +532,7 @@ namespace mbts.spark {
|
||||
_can_attach_selected_orders.refresh();
|
||||
_can_detach_selected_orders.refresh();
|
||||
selected_order_changed?.Invoke(current_order);
|
||||
_custom_state_sync.sync();
|
||||
} catch (Exception ex) {
|
||||
// [John]I observed a strange null-reference exception in this callback when the launcher was shutting down (see https://bluetradesys.atlassian.net/browse/SPARK-2089)
|
||||
Log.Ui.error($"[{nameof(OrderBookVM)}] Error handling selection change: {ex}");
|
||||
@@ -783,6 +753,45 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
class CustomStateSynchronizer {
|
||||
readonly OrderBookVM _vm;
|
||||
OrderBookRowVM _synced_row;
|
||||
|
||||
public CustomStateSynchronizer(OrderBookVM vm) =>
|
||||
_vm = vm;
|
||||
|
||||
public void sync(bool force_refresh = false) {
|
||||
var row = _vm.current_row != null &&
|
||||
_vm.selected_rows.Select(r => r.order_tuple.root.order.Key).Distinct().TryGetSingle(out uint droot) &&
|
||||
_vm.current_row.order_tuple.root.order.Key == droot ?
|
||||
_vm.current_row : null;
|
||||
if (force_refresh || row != _synced_row) {
|
||||
_synced_row = row;
|
||||
_refresh();
|
||||
}
|
||||
}
|
||||
|
||||
void _refresh() {
|
||||
_vm.current_algo_custom_state.Clear();
|
||||
if (_synced_row == null)
|
||||
return;
|
||||
var dcs = _synced_row.order.dynamic_custom_state();
|
||||
// while dcs is not expected to be null, best to play it safe and double check.
|
||||
if (dcs.is_null_or_empty()) {
|
||||
var descendent_rows = _vm._row_collection.get_descendent_rows(_synced_row.order_key);
|
||||
if (_vm._row_collection.filter != null)
|
||||
descendent_rows = descendent_rows.Where(child => _vm._row_collection.filter(child.order_tuple));
|
||||
foreach (var dr in descendent_rows) {
|
||||
dcs = dr.order.dynamic_custom_state();
|
||||
if (!dcs.is_null_or_empty())
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach (var p in dcs.empty_if_null().OrderBy(kv => kv.Key))
|
||||
_vm.current_algo_custom_state.Add(new NamedValueVM(p.Key, DynamicOrderHelper.fmt_param(p.Value)));
|
||||
}
|
||||
}
|
||||
|
||||
public class OrderParamsVM : DisposableViewModelBase {
|
||||
readonly SelectorProperty<AccountTuple, AccountTuple?> _accounts;
|
||||
readonly OrderBookVM _owner;
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
using bts.api;
|
||||
using bts;
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.shell;
|
||||
using bts.shell.fills;
|
||||
using bts.shell.orderbook;
|
||||
using bts.spark;
|
||||
using bts.view;
|
||||
using bts.utils.csv;
|
||||
using mbts.persist;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.ui.spark;
|
||||
using mbts.ui.utilities;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
@@ -24,7 +28,10 @@ namespace mbts.spark {
|
||||
const string _window_type = "OrderBook", _default_title = "Order Book";
|
||||
readonly INotificationService _notification_service;
|
||||
readonly IWindowSettingsManager<OrderBookGridSettings> _settings_manager;
|
||||
readonly bool _can_edit_specific_algo_params;
|
||||
IDisposable _attachment_exit_sub;
|
||||
// initialized in thread_init
|
||||
List<IDisposable> _thread_init_resources;
|
||||
OrderBookStartupData _state;
|
||||
OrdersModel _model;
|
||||
OrderBookVM _vm;
|
||||
@@ -34,13 +41,14 @@ namespace mbts.spark {
|
||||
SparkCommand[] _commands;
|
||||
|
||||
public OrderBookWindow(OrdersModel model, OrderBookWindowFactory factory, IBus bus, INotificationService notification_service,
|
||||
OrderBookStartupData state, ImageSource icon, IWindowSettingsManager<OrderBookGridSettings> settings_manager)
|
||||
OrderBookStartupData state, ImageSource icon, IWindowSettingsManager<OrderBookGridSettings> settings_manager, bool can_edit_specific_algo_params)
|
||||
: base(_default_title, bus, factory, SparkCommands.ShowOrders.icon) {
|
||||
_model = model;
|
||||
_factory = factory;
|
||||
_notification_service = notification_service;
|
||||
_state = state;
|
||||
_settings_manager = settings_manager;
|
||||
_can_edit_specific_algo_params = can_edit_specific_algo_params;
|
||||
thread_id = config.Configuration.get<bts.config.MultiThreadedWindows>().order_book ? "OrderBook" : null;
|
||||
}
|
||||
|
||||
@@ -64,12 +72,14 @@ namespace mbts.spark {
|
||||
public string thread_id { get; }
|
||||
|
||||
public override void Dispose() {
|
||||
_commands.OfType<IDisposable>().DisposeAll();
|
||||
Disposable.dispose(_thread_init_resources);
|
||||
Disposable.dispose(ref _attachment_exit_sub);
|
||||
Disposable.dispose(ref _vm);
|
||||
}
|
||||
|
||||
public void thread_init() {
|
||||
Log.assert(_thread_init_resources == null, $"{nameof(_thread_init_resources)} must be initialized in thread_init.");
|
||||
_thread_init_resources = new List<IDisposable>();
|
||||
_menu_display_vm = new SparkMenuDisplayVM();
|
||||
if (_state != null)
|
||||
_menu_display_vm.load_from(_state.menu_display);
|
||||
@@ -147,6 +157,15 @@ namespace mbts.spark {
|
||||
can_execute: () => _vm.selected_orders.Any(x => passes_recreate_search_criteria(x))
|
||||
);
|
||||
recreate_selected_orders_command.group = CommandGroups.Modify;
|
||||
ICommand edit_algo_params_cmd;
|
||||
if (_can_edit_specific_algo_params) {
|
||||
_thread_init_resources.Add(CommandHelper.bind_param_source(
|
||||
new DialogCommand<OrderSecurityAlgo?, Window>(mk_algo_params_dialog, has_algo_params),
|
||||
_vm.subscribe_single_selected_order(),
|
||||
out edit_algo_params_cmd, true));
|
||||
} else {
|
||||
edit_algo_params_cmd = DelegateCommand.empty;
|
||||
}
|
||||
|
||||
var attach_selected_orders_command = new SparkCommandCommand(
|
||||
"Attach To Selected",
|
||||
@@ -194,25 +213,37 @@ namespace mbts.spark {
|
||||
show_grid_settings_command.group = CommandGroups.Auxiliary;
|
||||
|
||||
_commands = new SparkCommand[] {
|
||||
// modify commands
|
||||
toggle_show_right_panel_command,
|
||||
attach_selected_orders_command,
|
||||
detach_selected_orders_command,
|
||||
recreate_selected_orders_command,
|
||||
new SparkCommandCommand("Algo Params...", Icons.Algo, edit_algo_params_cmd) {
|
||||
group = CommandGroups.Modify,
|
||||
show_in_toolbar = false,
|
||||
show_in_context_menu = _can_edit_specific_algo_params
|
||||
},
|
||||
// cancel commands
|
||||
cancel_selected_orders_command,
|
||||
cancel_buy_orders_command,
|
||||
cancel_sell_orders_command,
|
||||
cancel_all_orders_command,
|
||||
|
||||
toggle_collapse_all_command,
|
||||
toggle_expand_all_command,
|
||||
show_grid_settings_command,
|
||||
new SparkCommandCommand("Export to csv", Icons.Export, FileDialogCommand.export_csv(() => _vm.rows.ToCsv(OrderBookRowVM.csv_ordered_properties), "OrderBook")),
|
||||
// hidden cmds (i.e., keyboard shortcuts at window level)
|
||||
hidden_cmd(new SparkCommandCommand(OrderBookView.CommandDescriptions.ModifyOrders, null, _vm.modify_selected_orders_cmd, OrderBookView.KeyboardShortcuts.ModifyOrders)),
|
||||
hidden_cmd(new SparkCommandCommand(OrderBookView.CommandDescriptions.RevertOrders, null, _vm.revert_selected_orders_cmd, OrderBookView.KeyboardShortcuts.RevertOrders)),
|
||||
_menu_display_vm.spark_cmd
|
||||
};
|
||||
_vm.as_demuxer().subscribe<bool>(nameof(OrderBookVM.show_right_panel))
|
||||
.Subscribe(_ => toggle_show_right_panel_command.title = get_toggle_show_right_panel_cmd_title());
|
||||
_thread_init_resources.AddRange(_commands.OfType<IDisposable>());
|
||||
_thread_init_resources.Add(_vm.as_demuxer()
|
||||
.subscribe<bool>(nameof(OrderBookVM.show_right_panel))
|
||||
.Subscribe(_ => toggle_show_right_panel_command.title = get_toggle_show_right_panel_cmd_title()));
|
||||
_vm.selected_order_changed += vm_selected_order_changed;
|
||||
_thread_init_resources.Add(Disposable.Create(() => _vm.selected_order_changed -= vm_selected_order_changed));
|
||||
// local methods
|
||||
string get_toggle_show_right_panel_cmd_title() =>
|
||||
_vm.show_right_panel ? OrderBookView.CommandDescriptions.HideRightPanel : OrderBookView.CommandDescriptions.ShowRightPanel;
|
||||
@@ -234,6 +265,36 @@ namespace mbts.spark {
|
||||
attach_selected_orders_command.raiseCanExecuteChanged();
|
||||
detach_selected_orders_command.raiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
bool has_algo_params(OrderSecurityAlgo? order) =>
|
||||
order.HasValue && !OrderDataHelper.get_metadata(order.Value.algo.value.CustomCase).NoProps;
|
||||
|
||||
Window mk_algo_params_dialog(OrderSecurityAlgo? order) {
|
||||
if (!has_algo_params(order))
|
||||
return null;
|
||||
var algo_def = AlgoDefinitionVM.create(_model, order.Value.algo, editing_permissions: AlgoEditingPermissions.AllowEditValues);
|
||||
var algo_params_vm = new OrderBookAlgoParamsVM(algo_def, order.Value.order.is_non_terminal());
|
||||
var dialog = new OrderBookAlgoParamsDialog {
|
||||
Content = algo_params_vm
|
||||
};
|
||||
ui.WindowStyle.SetTitleContent(dialog, algo_def);
|
||||
dialog.Closed += (s, e) => {
|
||||
Disposable.dispose(ref algo_params_vm);
|
||||
dialog = null;
|
||||
};
|
||||
algo_params_vm.RequestCommit += commit;
|
||||
algo_params_vm.RequestClose += (s, e) => close_dialog();
|
||||
return dialog;
|
||||
|
||||
void close_dialog() {
|
||||
dialog?.Close();
|
||||
dialog = null;
|
||||
}
|
||||
void commit(CustomOrderDefinitionImmutable value) {
|
||||
_model.update_order(order.Value.order, new_algo: value).ignore();
|
||||
close_dialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnParentViewCreated(SparkWindowView parent_view) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using bts.api;
|
||||
using bts.shell.orderbook;
|
||||
using bts.spark;
|
||||
using mbts.config;
|
||||
using mbts.persist;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
@@ -15,8 +16,11 @@ namespace bts.shell {
|
||||
readonly INotificationServiceDataCache _notification_service_data_cache;
|
||||
readonly OrdersModel _model;
|
||||
readonly UserSettingsHub _settings_hub;
|
||||
readonly OrderBookConfig _config;
|
||||
|
||||
public OrderBookWindowFactory(IShellModule owner, IBus bus, INotificationService notification_service, INotificationServiceDataCache notification_service_data_cache, OrdersModel model, UserSettingsHub settings_hub) : base(owner, "SparkOrderBookWindow") {
|
||||
var raw_config = Configuration.get<bts.config.OrderBookOptions>();
|
||||
_config = new OrderBookConfig(raw_config.can_edit_specific_algo_params);
|
||||
_bus = bus;
|
||||
_notification_service = notification_service;
|
||||
_notification_service_data_cache = notification_service_data_cache;
|
||||
@@ -29,7 +33,8 @@ namespace bts.shell {
|
||||
var settings_manager = new WindowSettingsManager<OrderBookGridSettings>(_settings_hub, state.grid_settings_wrapper(),
|
||||
us => us.order_book_grid ?? OrderBookStartupData.DefaultGrid,
|
||||
(us, s) => us.with(order_book_grid: s));
|
||||
var w = new OrderBookWindow(_model, this, _bus, _notification_service, state, SparkCommands.ShowOrders.icon, settings_manager.as_non_shared());
|
||||
var w = new OrderBookWindow(_model, this, _bus, _notification_service, state,
|
||||
SparkCommands.ShowOrders.icon, settings_manager.as_non_shared(), _config.can_edit_specific_algo_params);
|
||||
return Task.FromResult<IShellWindow>(w);
|
||||
}
|
||||
|
||||
@@ -37,5 +42,12 @@ namespace bts.shell {
|
||||
var w = await load(startup_data);
|
||||
_bus.post(SparkTags.ShowWindow, w);
|
||||
}
|
||||
|
||||
readonly struct OrderBookConfig {
|
||||
public readonly bool can_edit_specific_algo_params;
|
||||
|
||||
public OrderBookConfig(bool can_edit_specific_algo_params) =>
|
||||
this.can_edit_specific_algo_params = can_edit_specific_algo_params;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<Window
|
||||
x:Class="bts.shell.orderbook.OrderBookAlgoParamsDialog"
|
||||
x:ClassModifier="internal"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:sp="clr-namespace:mbts.spark"
|
||||
xmlns:ob="clr-namespace:bts.shell.orderbook"
|
||||
xmlns:usp="clr-namespace:mbts.ui.spark"
|
||||
xmlns:u="clr-namespace:mbts.ui"
|
||||
xmlns:c="clr-namespace:mbts.ui.controls"
|
||||
u:WindowStyle.SizeToContent="WidthAndHeight"
|
||||
Margin="0"
|
||||
Padding="0"
|
||||
Style="{StaticResource BTSWindowTitleTemplate}"
|
||||
>
|
||||
<u:WindowStyle.TitleContentTemplate>
|
||||
<DataTemplate DataType="{x:Type sp:AlgoDefinitionVM}">
|
||||
<StackPanel HorizontalAlignment="Left" Orientation="Horizontal">
|
||||
<usp:OrderTypeItem Margin="4,0" DisplayName="{Binding name, StringFormat='{}{0} Params'}" BadgeFill="{Binding badge_fill}"/>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</u:WindowStyle.TitleContentTemplate>
|
||||
<Window.ContentTemplate>
|
||||
<DataTemplate DataType="{x:Type ob:OrderBookAlgoParamsVM}">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<usp:AlgoEditorGridView
|
||||
Width="390"
|
||||
Margin="5"
|
||||
AlgoParamRows="{Binding param_rows}"
|
||||
CanUserChangeAlgoRoot="False"
|
||||
/>
|
||||
<c:DialogSkirt Grid.Row="1" RightItems="{Binding dialog_skirt}"/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</Window.ContentTemplate>
|
||||
</Window>
|
||||
@@ -0,0 +1,49 @@
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace bts.shell.orderbook {
|
||||
/// <summary>
|
||||
/// Interaction logic for OrderBookAlgoParamsDialog.xaml
|
||||
/// </summary>
|
||||
partial class OrderBookAlgoParamsDialog : Window {
|
||||
public OrderBookAlgoParamsDialog() =>
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
class OrderBookAlgoParamsVM : DisposableViewModelBase, IRequestViewClose {
|
||||
readonly ObservableAggregateCollection<AlgoParamRowVM> _param_rows = new ObservableAggregateCollection<AlgoParamRowVM>();
|
||||
readonly DialogSkirtItem _commit, _cancel;
|
||||
|
||||
public OrderBookAlgoParamsVM(AlgoDefinitionVM algo_definition, bool can_user_edit) {
|
||||
this.algo_definition = algo_definition;
|
||||
_dispose = AlgoParamRowsHelper.connect(algo_definition, _param_rows, false);
|
||||
_commit = new DialogSkirtItem("OK", new DelegateCommand(commit, () => can_user_edit)) { is_default = true };
|
||||
_cancel = new DialogSkirtItem("Cancel", new DelegateCommand(close)) { is_cancel = true };
|
||||
// local methods
|
||||
void commit() {
|
||||
RequestCommit?.Invoke(new CustomOrderDefinitionImmutable(algo_definition.algo_root.created_assigned()));
|
||||
close();
|
||||
}
|
||||
void close() => RequestClose?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
public event EventHandler RequestClose;
|
||||
public event Action<CustomOrderDefinitionImmutable> RequestCommit;
|
||||
|
||||
public AlgoDefinitionVM algo_definition { get; }
|
||||
public IReadOnlyObservableCollection<AlgoParamRowVM> param_rows => _param_rows;
|
||||
public IEnumerable<DialogSkirtItem> dialog_skirt {
|
||||
get {
|
||||
yield return _commit;
|
||||
yield return _cancel;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ namespace mbts.spark
|
||||
{
|
||||
return;
|
||||
}
|
||||
var ev = fd.EnumType.Values.First(x => x.Name == value)?.Number;
|
||||
var ev = fd.EnumType.Values.FirstOrDefault(x => x.Name == value)?.Number;
|
||||
if(ev.HasValue)
|
||||
fd.Accessor.SetValue(message, ev.Value);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.collections;
|
||||
using bts.spark;
|
||||
using Google.Protobuf;
|
||||
using Google.Protobuf.Reflection;
|
||||
using mbts.collections;
|
||||
@@ -15,21 +14,19 @@ using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reflection;
|
||||
using AlgoType = bts.api.orderman.CustomOrderDefinition.CustomOneofCase;
|
||||
|
||||
namespace mbts.spark {
|
||||
static partial class OrderTypeVMFactory
|
||||
{
|
||||
internal class GenericOrderFactory
|
||||
{
|
||||
static public IAlgoVM create_vm(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions)
|
||||
{
|
||||
static partial class OrderTypeVMFactory {
|
||||
internal class GenericOrderFactory {
|
||||
static public IAlgoVM create_vm(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions) {
|
||||
var prop = typeof(CustomOrderDefinition).GetProperty(algo_type.ToString(),
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
var rt = prop.PropertyType;
|
||||
|
||||
var propi = typeof(CustomOrderDefinitionImmutable).GetProperty(algo_type.ToString(),
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
var rti = propi.PropertyType;
|
||||
|
||||
var vmt = typeof(GenericOrderVM<,>);
|
||||
@@ -38,38 +35,33 @@ namespace mbts.spark {
|
||||
object vm = Activator.CreateInstance(makevm, key, algo_type, display_name, editing_permissions);
|
||||
return (IAlgoVM)vm;
|
||||
}
|
||||
static public void setup_generic_custom_order(CustomOrderDefinition custom, CustomOrderDefinition.CustomOneofCase type)
|
||||
{
|
||||
static public void setup_generic_custom_order(CustomOrderDefinition custom, CustomOrderDefinition.CustomOneofCase type) {
|
||||
var prop = typeof(CustomOrderDefinition).GetProperty(type.ToString(),
|
||||
System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public);
|
||||
BindingFlags.Instance | BindingFlags.Public);
|
||||
var rt = prop.PropertyType;
|
||||
var rtt = Activator.CreateInstance(rt);
|
||||
|
||||
|
||||
var ci = rt.GetProperties().Where(x => x.PropertyType == typeof(CustomOrderDefinition));
|
||||
foreach(var cid in ci)
|
||||
foreach (var cid in ci)
|
||||
cid.SetValue(rtt, new CustomOrderDefinition { Limit = new LimitOrderDefinition() });
|
||||
prop.SetValue(custom, rtt);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class GenericOrderVM<TMutable, TImmutable> : OrderTypeVMBase<TMutable, TImmutable>
|
||||
where TMutable : class, IMessage, new() where TImmutable : struct, IImmutableMessage
|
||||
{
|
||||
where TMutable : class, IMessage, new() where TImmutable : struct, IImmutableMessage {
|
||||
readonly ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string> _parameters =
|
||||
new ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string>(vm => vm.descriptor.Name);
|
||||
readonly AlgoType _algo_type;
|
||||
|
||||
public GenericOrderVM(AlgoKey key, AlgoType algo_type, string display_name, AlgoEditingPermissions editing_permissions)
|
||||
: base(key, display_name, editing_permissions)
|
||||
{
|
||||
: base(key, display_name, editing_permissions) {
|
||||
_algo_type = algo_type;
|
||||
_parameters = new ObservableKeyedCollection<GenericOrderParamVM<TMutable>, string>(vm => vm.descriptor.Name);
|
||||
_resources.AddRange(
|
||||
ObservableAutoWrapper.connect_item_hooks(_parameters, p =>
|
||||
{
|
||||
ObservableAutoWrapper.connect_item_hooks(_parameters, p => {
|
||||
p.PropertyChanged += on_param_prop_changed;
|
||||
return Disposable.Create(() =>
|
||||
{
|
||||
return Disposable.Create(() => {
|
||||
p.PropertyChanged -= on_param_prop_changed;
|
||||
remove_property(p.descriptor.Name);
|
||||
p.Dispose();
|
||||
@@ -80,84 +72,74 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
public string algo_type => _algo_type.ToString();
|
||||
public override string abbreviation =>
|
||||
public override string abbreviation =>
|
||||
model == null ? display_name : model.factory.abbreviate_algo(new OneOf<CustomOrderDefinition.CustomOneofCase, string>(_algo_type));
|
||||
public override AlgoDisplayGroup group => AlgoDisplayGroup.API;
|
||||
public ObservableCollection<GenericOrderParamVM<TMutable>> parameters => _parameters;
|
||||
|
||||
protected override void on_model_internal()
|
||||
{
|
||||
protected override void on_model_internal() =>
|
||||
_sync_generic_params();
|
||||
}
|
||||
|
||||
void _sync_generic_params()
|
||||
{
|
||||
if (model != null)
|
||||
{
|
||||
Google.Protobuf.Reflection.MessageDescriptor desc = new TMutable().Descriptor;
|
||||
void _sync_generic_params() {
|
||||
if (model != null) {
|
||||
MessageDescriptor desc = new TMutable().Descriptor;
|
||||
_parameters.SyncWithDictionary(
|
||||
desc.Fields.InDeclarationOrder().Where(_is_value_param_desc).ToDictionary(p => p.Name), _mk_param_vm);
|
||||
}
|
||||
}
|
||||
static bool _is_value_param_desc(FieldDescriptor desc)
|
||||
{
|
||||
return (desc.FieldType != Google.Protobuf.Reflection.FieldType.Message ||
|
||||
|
||||
static bool _is_value_param_desc(FieldDescriptor desc) =>
|
||||
(desc.FieldType != FieldType.Message ||
|
||||
_is_quantity_measure_desc(desc));
|
||||
}
|
||||
|
||||
static bool _is_quantity_measure_desc(FieldDescriptor desc)
|
||||
{
|
||||
return desc.FieldType == Google.Protobuf.Reflection.FieldType.Message &&
|
||||
static bool _is_quantity_measure_desc(FieldDescriptor desc) =>
|
||||
desc.FieldType == FieldType.Message &&
|
||||
desc.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName;
|
||||
}
|
||||
|
||||
protected override IEnumerable<ChildAlgoData> get_selector_data()
|
||||
{
|
||||
var v = OrderDataHelper.get_child_algo_data(_algo_type);
|
||||
return v;
|
||||
}
|
||||
protected override IEnumerable<ChildAlgoData> get_selector_data() =>
|
||||
OrderDataHelper.get_child_algo_data(_algo_type);
|
||||
|
||||
GenericOrderParamVM<TMutable> _mk_param_vm(FieldDescriptor descriptor)
|
||||
{
|
||||
GenericOrderParamVM<TMutable> _mk_param_vm(FieldDescriptor descriptor) {
|
||||
ExceptionAssert.Argument.NotNull(descriptor, nameof(descriptor));
|
||||
|
||||
if (descriptor.FieldType == FieldType.Message &&
|
||||
descriptor.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName)
|
||||
{
|
||||
if (descriptor.FieldType == FieldType.Message &&
|
||||
descriptor.MessageType.FullName == typeof(bts.spark.QuantityMeasure).FullName) {
|
||||
var qm = new GenericOrderQuantityVM<TMutable>(descriptor);
|
||||
return attached(qm, GenericOrderHelper.get_quantity_value, GenericOrderHelper.set_quantity_value);
|
||||
}
|
||||
else if(descriptor.FieldType == FieldType.Enum)
|
||||
{
|
||||
var pvm = new GenericOrderEnumeratedVM<TMutable>(descriptor);
|
||||
attached(pvm, GenericOrderHelper.get_enumerated_value, GenericOrderHelper.set_enumerated_value);
|
||||
return pvm;
|
||||
}
|
||||
else if (descriptor.FieldType == FieldType.Bool)
|
||||
} else if (descriptor.FieldType == FieldType.Enum) {
|
||||
return EnumParamFactory.create(this, descriptor);
|
||||
} else if (descriptor.FieldType == FieldType.Bool) {
|
||||
return mk<bool?, bool>(descriptor, v => v, v => v.GetValueOrDefault());
|
||||
else if (descriptor.FieldType == FieldType.Double || descriptor.FieldType == FieldType.Float)
|
||||
} else if (descriptor.FieldType == FieldType.Double || descriptor.FieldType == FieldType.Float) {
|
||||
return mk<double, double>(descriptor, v => v, v => v, double.NaN);
|
||||
else if (descriptor.FieldType == FieldType.Int32)
|
||||
} else if (descriptor.FieldType == FieldType.Int32) {
|
||||
return mk<int?, int>(descriptor, v => v, v => v.GetValueOrDefault());
|
||||
else if (descriptor.FieldType == FieldType.UInt32)
|
||||
} else if (descriptor.FieldType == FieldType.UInt32) {
|
||||
return mk<uint?, uint>(descriptor, v => v, v => v.GetValueOrDefault());
|
||||
else if (descriptor.FieldType == FieldType.String)
|
||||
} else if (descriptor.FieldType == FieldType.String) {
|
||||
return mk<string, string>(descriptor, v => v, v => v);
|
||||
else
|
||||
return null;
|
||||
} else return null;
|
||||
}
|
||||
GenericOrderParamVM<TMutable, TView, TModel> mk<TView, TModel>(FieldDescriptor fdesc, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default)
|
||||
{
|
||||
var pvm = new GenericOrderParamVM<TMutable, TView, TModel>(fdesc, convert, convert_back);
|
||||
return attached<TView, GenericOrderParamVM<TMutable, TView, TModel>>(pvm, get_from_param, assign_to_param, default_value);
|
||||
|
||||
GenericOrderParamVM<TMutable, TView, TModel> mk<TView, TModel>(
|
||||
FieldDescriptor fdesc,
|
||||
Func<TModel, TView> convert,
|
||||
Func<TView, TModel> convert_back,
|
||||
TView default_value = default) {
|
||||
//
|
||||
var pvm = new GenericOrderParamVM<TMutable, TView, TModel>(fdesc, convert, convert_back);
|
||||
return attached(pvm, get_from_param, assign_to_param, default_value);
|
||||
TView get_from_param(IMessage param, FieldDescriptor fd) => convert(ConvertUtils.try_convert<TModel>(fdesc.Accessor.GetValue(param)));
|
||||
void assign_to_param(IMessage param, FieldDescriptor fd, TView value) => fdesc.Accessor.SetValue(param, convert_back(value));
|
||||
}
|
||||
|
||||
TParam attached<T, TParam>(TParam pvm, Func<IMessage, FieldDescriptor, T> selector, Action<IMessage, FieldDescriptor, T> assign_to_param, T default_value = default) where TParam : GenericOrderParamVM<TMutable>, IGenericOrderParamVM<T>
|
||||
{
|
||||
add_property<T>(
|
||||
TParam attached<T, TParam>(
|
||||
TParam pvm,
|
||||
Func<IMessage, FieldDescriptor, T>
|
||||
selector, Action<IMessage, FieldDescriptor, T> assign_to_param,
|
||||
T default_value = default)
|
||||
where TParam : GenericOrderParamVM<TMutable>, IGenericOrderParamVM<T> {
|
||||
//
|
||||
add_property(
|
||||
pvm.descriptor.Name,
|
||||
mdef => mdef != null ? selector(mdef, pvm.descriptor) : default_value,
|
||||
(mdef, v) => assign_to_param(mdef, pvm.descriptor, v),
|
||||
@@ -166,7 +148,37 @@ namespace mbts.spark {
|
||||
pvm.subscribe_value(),
|
||||
default_value);
|
||||
return pvm;
|
||||
}
|
||||
}
|
||||
|
||||
static class EnumParamFactory {
|
||||
[ThreadStatic]
|
||||
static Dictionary<Type, IFactory> _factories_backing;
|
||||
static Dictionary<Type, IFactory> _factories =>
|
||||
_factories_backing ?? (_factories_backing = new Dictionary<Type, IFactory>());
|
||||
|
||||
public static GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor) {
|
||||
if (descriptor.FieldType != FieldType.Enum)
|
||||
throw ExceptionBuilder.Argument.unexpected_case(descriptor.FieldType, $"{nameof(descriptor)}.{nameof(FieldDescriptor.FieldType)}");
|
||||
if (!_factories.TryGetValue(descriptor.EnumType.ClrType, out var f)) {
|
||||
// Impl<T> is a nested type, so parent class's generic type args must be included.
|
||||
_factories[descriptor.EnumType.ClrType] = f = (IFactory)Activator.CreateInstance(typeof(Impl<>)
|
||||
.MakeGenericType(typeof(TMutable), typeof(TImmutable), descriptor.EnumType.ClrType));
|
||||
}
|
||||
return f.create(owner, descriptor);
|
||||
}
|
||||
|
||||
interface IFactory {
|
||||
GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor);
|
||||
}
|
||||
|
||||
class Impl<T> : IFactory where T : struct {
|
||||
static readonly Func<T, T?> _convert = v => v;
|
||||
static readonly Func<T?, T> _convert_back = v => v ?? default;
|
||||
|
||||
public GenericOrderParamVM<TMutable> create(GenericOrderVM<TMutable, TImmutable> owner, FieldDescriptor descriptor) =>
|
||||
owner.mk(descriptor, _convert, _convert_back, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
<RuntimeIdentifiers>win7-x64</RuntimeIdentifiers>
|
||||
<PlatformTarget>AnyCPU</PlatformTarget>
|
||||
<LanguageTargets>$(MSBuildExtensionsPath)\$(VisualStudioVersion)\Bin\Microsoft.CSharp.targets</LanguageTargets>
|
||||
<LanguageTargets Condition="'$(VisualStudioVersion)'=='16.0'">$(MSBuildToolsPath)\Microsoft.CSharp.targets</LanguageTargets>
|
||||
<ApplicationIcon>logo.ico</ApplicationIcon>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<StartupObject>mbts.ui.shell.App</StartupObject>
|
||||
|
||||
@@ -63,5 +63,20 @@ namespace mbts {
|
||||
|
||||
public static double? nan_nullable(double value) =>
|
||||
double.IsNaN(value) ? (double?)null : value;
|
||||
|
||||
public static int? to_optional_int32(double value) {
|
||||
if (double.IsNaN(value) || double.IsInfinity(value))
|
||||
return null;
|
||||
if (value > 0) {
|
||||
value = Math.Floor(value);
|
||||
if (value <= int.MaxValue)
|
||||
return (int)value;
|
||||
} else if (value < 0) {
|
||||
value = Math.Ceiling(value);
|
||||
if (value >= int.MinValue)
|
||||
return (int)value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1005,6 +1005,8 @@
|
||||
ContractPickerModel="{Binding contract_picker_model}"
|
||||
HedgePricing="{Binding hedge_pricing}"
|
||||
HedgePricingItemsSource="{Binding hedge_pricing_options}"
|
||||
UseFractionalTicks="{Binding use_fractional_ticks}"
|
||||
IntegerTickSize="{Binding integer_tick_size}"
|
||||
/>
|
||||
<c:DialogSkirt Grid.Row="1" Margin="-4" VerticalAlignment="Top" RightItems="{Binding skirt_items}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -12,6 +12,9 @@ namespace mbts.ui.converters
|
||||
public Visibility VisibilityForFalse { get { return _visfalse; } set { _visfalse = value; } }
|
||||
|
||||
public static BooleanToVisibilityConverter Instance => new BooleanToVisibilityConverter();
|
||||
public static BooleanToVisibilityConverter HiddenInstance => new BooleanToVisibilityConverter {
|
||||
VisibilityForFalse = Visibility.Hidden
|
||||
};
|
||||
|
||||
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
|
||||
bool flag = false;
|
||||
|
||||
@@ -64,6 +64,25 @@ namespace mbts.ui {
|
||||
}
|
||||
}, can_execute == null ? null : new Func<object, bool>(_ => can_execute()));
|
||||
|
||||
public static IDialogCommand export_csv(
|
||||
Func<string> get_file_content,
|
||||
string source_window_name) =>
|
||||
new AnonymousFileDialogCommand<object>(_ =>
|
||||
_create_save_file_dialog(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
|
||||
$"BTS_Spark_{source_window_name}_{DateTime.Now.ToString("yyyyMMdd_HHmm")}.csv",
|
||||
new[] { ("CSV (comma delimited)", new string[] { "csv" }) }
|
||||
),
|
||||
(_, file) => {
|
||||
try {
|
||||
File.WriteAllText(file, get_file_content.Invoke());
|
||||
} catch (Exception ex) {
|
||||
BTSMessageBox.Show($"Error exporting data to csv: {ex}.", "CSV Export Error", BTSMessageBoxButtons.OK, System.Windows.MessageBoxImage.Error, true, false, true);
|
||||
}
|
||||
},
|
||||
// we are always able to perform an export, even when no rows (it will just be a header)
|
||||
(_) => true);
|
||||
|
||||
static TDialog _create_file_dialog<TDialog>(string initial_dir = null, IReadOnlyList<(string name, string[] extensions)> filter_items = null) where TDialog : FileDialog, new() {
|
||||
var rv = new TDialog();
|
||||
if (!string.IsNullOrEmpty(initial_dir))
|
||||
|
||||
@@ -49,6 +49,28 @@ namespace mbts.spark {
|
||||
public static double parse(this ContractSpecificationImmutable? spec, string text, double display_multiplier, string nan_text) =>
|
||||
spec.spec_or_default().parse(text, display_multiplier, nan_text);
|
||||
|
||||
public static bool is_integer_tick(ContractSpecificationImmutable spec) =>
|
||||
spec.MainFraction == 0 &&
|
||||
spec.PriceDisplayFormat == 0 &&
|
||||
spec.LowTick > 0 &&
|
||||
MathUtils.is_int(spec.LowTick) &&
|
||||
spec.HighTick > 0 &&
|
||||
MathUtils.is_int(spec.HighTick);
|
||||
|
||||
public static ContractSpecification create_integer_tick_spec(int low_tick) =>
|
||||
create_integer_tick_spec(low_tick, low_tick);
|
||||
|
||||
public static ContractSpecification create_integer_tick_spec(int low_tick, int high_tick) {
|
||||
var rv = SecurityDataHelper.Defaults.ContractSpecification;
|
||||
rv.MainFraction = 0;
|
||||
rv.SubFraction = 0;
|
||||
rv.PriceDisplayFormat = 0;
|
||||
rv.PriceBreak = 0;
|
||||
rv.LowTick = low_tick;
|
||||
rv.HighTick = high_tick;
|
||||
return rv;
|
||||
}
|
||||
|
||||
public static double low_tick(NativePriceSize[] bids, NativePriceSize[] asks) {
|
||||
double low = bids[bids.Length - 1].price;
|
||||
if (double.IsNaN(low))
|
||||
@@ -326,7 +348,7 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
static bool _check_tick_size(double tick_size) =>
|
||||
(tick_size > 0 && tick_size <= 0.5) || tick_size == 1;
|
||||
tick_size > 0 && (tick_size <= 0.5 || MathUtils.is_int(tick_size));
|
||||
|
||||
struct SpecKey : IEquatable<SpecKey> {
|
||||
public readonly uint denominator, display_digits;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using mbts.ui.converters;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
@@ -12,6 +14,31 @@ namespace mbts.ui.utilities {
|
||||
public static string fmt(string title, KeyGesture key_gesture) =>
|
||||
key_gesture == null ? title : $"{title} ({key_gesture.GetDisplayStringForCulture(CultureInfo.CurrentCulture)})";
|
||||
|
||||
public static IDisposable bind_param_source<T>(ICommand command, IObservable<T> param_source, out ICommand bound_cmd, bool dispose_command_on_dispose = false) {
|
||||
bool initialized = false;
|
||||
bool can_execute = false;
|
||||
T param = default;
|
||||
var bcmd = new DelegateCommand(() => command.Execute(param), () => can_execute);
|
||||
bound_cmd = bcmd;
|
||||
var sub = param_source.Subscribe(p => {
|
||||
param = p;
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
can_execute = command.CanExecute(p);
|
||||
bcmd.RaiseCanExecuteChanged();
|
||||
} else if (can_execute != command.CanExecute(p)) {
|
||||
can_execute = !can_execute;
|
||||
bcmd.RaiseCanExecuteChanged();
|
||||
}
|
||||
});
|
||||
return dispose_command_on_dispose ?
|
||||
Disposable.Create(() => {
|
||||
Disposable.dispose(ref sub);
|
||||
(command as IDisposable)?.Dispose();
|
||||
command = null;
|
||||
}) : sub;
|
||||
}
|
||||
|
||||
#region Paraphrased from https://referencesource.microsoft.com/#PresentationFramework/src/Framework/MS/Internal/Commands/CommandHelpers.cs
|
||||
|
||||
public static bool CanExecuteCommandSource(ICommandSource commandSource) {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
|
||||
namespace bts.utils.csv {
|
||||
public static class CsvHelper {
|
||||
|
||||
public static string ToCsv<T>(this IEnumerable<T> items, OrderedDictionary properties = null) where T : class {
|
||||
var output = "";
|
||||
var delimiter = ",";
|
||||
if (properties == null) {
|
||||
properties = new OrderedDictionary();
|
||||
foreach (var pi in typeof(T).GetProperties()) {
|
||||
properties.Add(pi.Name, pi);
|
||||
}
|
||||
}
|
||||
|
||||
using (var sw = new StringWriter()) {
|
||||
var dict_values = properties.Values.OfType<PropertyInfo>();
|
||||
var header = dict_values.Select(n => n.Name).Aggregate((first, second) => first + delimiter + second);
|
||||
sw.WriteLine(header);
|
||||
foreach (var i in items) {
|
||||
var row = dict_values.Select(x => x.GetValue(i, null))
|
||||
.Select(x => x == null ? "null" : x.ToString())
|
||||
.Aggregate((first, second) => first + delimiter + second);
|
||||
sw.WriteLine(row);
|
||||
}
|
||||
output = sw.ToString();
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
public static OrderedDictionary create_ordering_maintainer(Type t, string[] property_names_in_order) {
|
||||
OrderedDictionary od = new OrderedDictionary();
|
||||
var props = t.GetProperties();
|
||||
foreach (var n in property_names_in_order) {
|
||||
var query = props.Where(pi => pi.Name == n);
|
||||
if (query.Any()) {
|
||||
var property_info = query.First();
|
||||
if (!od.Contains(property_info)) {
|
||||
od.Add(n, property_info);
|
||||
}
|
||||
}
|
||||
}
|
||||
return od;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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/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.cpp;metrics/metrics.cpp;metricsLine 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/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.cpp;metrics/metrics.cpp;metricsLine truncated
|
||||
@@ -104,6 +104,7 @@ message CustomOrderDefinition
|
||||
SplitterOrderDefinition splitter = 29;
|
||||
ScratchOrderDefinition scratch = 31;
|
||||
ProRataOrderDefinition prorata = 32;
|
||||
DeleteAndCoverDefinition delete_and_cover = 34;
|
||||
|
||||
DynamicOrderDefinition dynamic = 1000;
|
||||
}
|
||||
@@ -144,6 +145,13 @@ message TickOrderDefinition {
|
||||
bool detach_on_toward = 5;
|
||||
CustomOrderDefinition child = 6;
|
||||
}
|
||||
enum LeanOnToward
|
||||
{
|
||||
NO_CHANGE = 0; //continue to lean on initial inside market
|
||||
CANCEL = 1; //cancel algo and order
|
||||
DETACH = 2; //cancel algo but leave order working
|
||||
REPRICE = 3; //lean on new inside market
|
||||
}
|
||||
message LeanOrderDefinition {
|
||||
spark.QuantityMeasure cross_threshold = 1;
|
||||
uint32 max_chase_ticks = 2;
|
||||
@@ -179,6 +187,21 @@ message CoverOrderDefinition {
|
||||
CustomOrderDefinition child = 3;
|
||||
bool cancel_on_external_hedge_cancel = 4;
|
||||
bool cancel_hedges_on_cancel = 5;
|
||||
bool detach_hedges = 6;
|
||||
}
|
||||
message DeleteAndCoverDefinition{
|
||||
// cover
|
||||
uint32 hedge_ticks = 1;
|
||||
bool cancel_on_external_hedge_cancel = 2;
|
||||
bool cancel_hedges_on_cancel = 3;
|
||||
bool detach_hedges = 4;
|
||||
// main child = top
|
||||
spark.QuantityMeasure top_threshold = 10;
|
||||
uint32 ticks_off = 11;
|
||||
// hedge child = Lean
|
||||
spark.QuantityMeasure cross_threshold = 20;
|
||||
uint32 max_chase_ticks = 21;
|
||||
LeanOnToward trade_toward_behavior = 22;
|
||||
}
|
||||
message OffsetOrderDefinition {
|
||||
uint32 tick_offset_on_deploy = 1;
|
||||
|
||||
@@ -76,6 +76,7 @@ message LadderKnobs {
|
||||
message OrderBookOptions {
|
||||
optional bool animate_child_order_rows = 1 [default = false];
|
||||
optional double status_color_opacity_when_selected = 2 [default = 0.35];
|
||||
optional bool can_edit_specific_algo_params = 3 [default = false];
|
||||
}
|
||||
|
||||
message OrderTicketOptions {
|
||||
|
||||
@@ -68,4 +68,6 @@ message DynamicAlgos{
|
||||
optional bool enable_pyramid = 2 [default = false];
|
||||
optional bool enable_tas = 3 [default = false];
|
||||
optional bool enable_tascounter = 4 [default = false];
|
||||
optional bool enable_limit_cover = 5 [default = false];
|
||||
optional bool enable_onsignal = 6 [default = false];
|
||||
}
|
||||
@@ -41,16 +41,16 @@ inline api::Side other_side(api::Side side) {
|
||||
}
|
||||
}
|
||||
|
||||
inline double market_epislon() { return 1e-10; }
|
||||
inline double market_epsilon() { return 1e-10; }
|
||||
|
||||
// better == p1 more aggressive than p1 ie for a buy order p1 is higher
|
||||
// than p2, and for a sell order p1 is lower than p2
|
||||
inline bool is_better(api::Side side, Price p1, Price p2) {
|
||||
switch (side) {
|
||||
case api::SIDE_BUY:
|
||||
return p1.value() - p2.value() > market_epislon();
|
||||
return p1.value() - p2.value() > market_epsilon();
|
||||
case api::SIDE_SELL:
|
||||
return p2.value() - p1.value() > market_epislon();
|
||||
return p2.value() - p1.value() > market_epsilon();
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
@@ -76,7 +76,7 @@ inline Price worsen_price(api::Side side, Price p, Price offset) {
|
||||
}
|
||||
|
||||
inline bool is_same(Price p1, Price p2) {
|
||||
return std::abs(p1.value() - p2.value()) < market_epislon();
|
||||
return std::abs(p1.value() - p2.value()) < market_epsilon();
|
||||
}
|
||||
|
||||
inline bool is_equal_or_better(api::Side side, Price p1, Price p2) {
|
||||
@@ -211,6 +211,9 @@ inline bool gr_than(double p1, double p2, double tick_size) {
|
||||
return gr_than_eq(p1, p2, tick_size) &&
|
||||
!test_price_equality(p1, p2, tick_size);
|
||||
}
|
||||
inline bool test_price_equality(double price_1, double price_2) {
|
||||
return std::fabs(price_2 - price_1) < market_epsilon();
|
||||
}
|
||||
|
||||
|
||||
}}
|
||||
} // namespace securities
|
||||
} // namespace bts
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/md/IntPrice.h"
|
||||
#include "lib/md/MDConsumer.h"
|
||||
@@ -10,12 +11,11 @@
|
||||
#include "lib/mercury/events/TimerEvent.h"
|
||||
#include "lib/orderman/events/BookUpdateEvent.h"
|
||||
#include "lib/orderman/events/MDRequestEvent.h"
|
||||
#include "lib/orderman/events/TickerUpdateEvent.h"
|
||||
#include "lib/orderman/events/StatusUpdateEvent.h"
|
||||
#include "lib/orderman/events/TickerUpdateEvent.h"
|
||||
#include "lib/tracing/PerfLog.h"
|
||||
#include "lib/tracing/bts_trace.h"
|
||||
#include "lib/utils/TimeStamp.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
|
||||
#include <date/date.h>
|
||||
|
||||
@@ -31,9 +31,9 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
public:
|
||||
MDSource(md::MDConsumer &consumer, metrics::MetricsContainer &stats,
|
||||
trace::PerfLog &log)
|
||||
: _handles(), _pending_handles(), _status_subs(), _md_consumer(consumer),
|
||||
_book_update(), _ticker_update(), _status_update(),
|
||||
_perf_log(log), _stats(stats), _loop_timer(), _counter(0) {
|
||||
: _handles(), _pending_handles(), _status_subs(), _md_consumer(consumer),
|
||||
_book_update(), _ticker_update(), _status_update(), _perf_log(log),
|
||||
_stats(stats), _loop_timer(), _counter(0) {
|
||||
|
||||
_trades_buffer.Reserve(1000);
|
||||
auto cme_channels = md::get_configured_cme_channels();
|
||||
@@ -67,9 +67,8 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
auto ticker_channel_cout_metric =
|
||||
std::make_shared<metrics::CounterMetric<int>>(
|
||||
"spark_mdsource_ticker_channel_count");
|
||||
auto status_count_metric =
|
||||
std::make_shared<metrics::CounterMetric<int>>(
|
||||
"spark_mdsource_status_count");
|
||||
auto status_count_metric = std::make_shared<metrics::CounterMetric<int>>(
|
||||
"spark_mdsource_status_count");
|
||||
_stats.publish(security_count_metric);
|
||||
_stats.publish(synthetic_count_metric);
|
||||
_stats.publish(ticker_channel_cout_metric);
|
||||
@@ -78,37 +77,40 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
_book_update = make_stream<BookUpdateEvent>();
|
||||
_ticker_update = make_stream<TickerUpdateEvent>();
|
||||
_status_update = make_stream<StatusUpdateEvent>();
|
||||
subscribe<MDRequestEvent>(
|
||||
[this, security_count_metric, synthetic_count_metric,
|
||||
ticker_channel_cout_metric, status_count_metric](auto const &update) {
|
||||
if (update.events) {
|
||||
for (auto const &ev : *update.events) {
|
||||
auto k = ev.security_key;
|
||||
subscribe<MDRequestEvent>([this, security_count_metric,
|
||||
synthetic_count_metric,
|
||||
ticker_channel_cout_metric,
|
||||
status_count_metric](auto const &update) {
|
||||
if (update.events) {
|
||||
for (auto const &ev : *update.events) {
|
||||
auto k = ev.security_key;
|
||||
|
||||
switch (ev.md_type) {
|
||||
case MDRequestType::BOOK:
|
||||
on_book_sub(k, *synthetic_count_metric);
|
||||
security_count_metric->set(_handles.size());
|
||||
break;
|
||||
case MDRequestType::TICKER:
|
||||
on_ticker_sub(k, *ticker_channel_cout_metric);
|
||||
break;
|
||||
case MDRequestType::STATUS:
|
||||
on_status_sub(k);
|
||||
status_count_metric->set(_status_subs.size());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
switch (ev.md_type) {
|
||||
case MDRequestType::BOOK:
|
||||
on_book_sub(k, *synthetic_count_metric);
|
||||
security_count_metric->set(_handles.size());
|
||||
break;
|
||||
case MDRequestType::TICKER:
|
||||
on_ticker_sub(k, *ticker_channel_cout_metric);
|
||||
break;
|
||||
case MDRequestType::STATUS:
|
||||
on_status_sub(k);
|
||||
status_count_metric->set(_status_subs.size());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
security_count_metric->set(_handles.size());
|
||||
}
|
||||
|
||||
private:
|
||||
void on_poll(trace::PerfLog& log) override {
|
||||
void on_poll(trace::PerfLog &log) override {
|
||||
|
||||
auto scoped_do_work = trace::log_scope(log, {}, "MDSource::on_poll", utils::TimeStamp::Interval::from_microseconds(2));
|
||||
auto scoped_do_work =
|
||||
trace::log_scope(log, {}, "MDSource::on_poll",
|
||||
utils::TimeStamp::Interval::from_microseconds(2));
|
||||
|
||||
// Book Updates
|
||||
Book book;
|
||||
@@ -158,11 +160,11 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
}
|
||||
|
||||
// Status updates
|
||||
for (auto& s : _status_subs) {
|
||||
for (auto &s : _status_subs) {
|
||||
auto status = s.handle.trading_status();
|
||||
if (status != s.status) {
|
||||
s.status = status;
|
||||
publish(_status_update, SecurityKey { s.handle.key() }, status);
|
||||
publish(_status_update, SecurityKey{s.handle.key()}, status);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +173,7 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
_loop_timer->observe((start - stop).nanoseconds());
|
||||
|
||||
_counter += 1;
|
||||
if (_counter % (2<<20) == 0) {
|
||||
if (_counter % (2 << 20) == 0) {
|
||||
// Invoke periodic work every 2M calls on on_poll
|
||||
// This is approximately 20-30 seconds
|
||||
do_periodic_work();
|
||||
@@ -180,35 +182,39 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
|
||||
void do_periodic_work() {
|
||||
if (!_pending_handles.empty()) {
|
||||
for (auto iter = _pending_handles.begin(); iter != _pending_handles.end(); ++iter) {
|
||||
for (auto iter = _pending_handles.begin();
|
||||
iter != _pending_handles.end(); ++iter) {
|
||||
if (iter->handle->is_initialized()) {
|
||||
auto handle = *(iter->handle);
|
||||
LOG(OrderMan, Info, "Found md for {}. Pending handle removed.", handle.key());
|
||||
switch(iter->type) {
|
||||
case MDRequestType::BOOK:
|
||||
_handles.push_back({utils::TimeStamp(), handle,
|
||||
get_external_event_channel(handle.get_usersymbol()),
|
||||
std::make_unique<std::string>(securities::descriptor(
|
||||
handle.get_normalized_security()))});
|
||||
break;
|
||||
case MDRequestType::STATUS:
|
||||
for (auto& h : _status_subs) {
|
||||
// no need to reset status
|
||||
if (h.handle.key() == handle.key()) break;
|
||||
}
|
||||
_status_subs.push_back(
|
||||
StatusRecord{ handle, co2::TRADINGSTATUS_UNKNOWN });
|
||||
case MDRequestType::TICKER:
|
||||
LOG(OrderMan, Info, "Found md for {}. Pending handle removed.",
|
||||
handle.key());
|
||||
switch (iter->type) {
|
||||
case MDRequestType::BOOK:
|
||||
_handles.push_back(
|
||||
{utils::TimeStamp(), handle,
|
||||
get_external_event_channel(handle.get_usersymbol()),
|
||||
std::make_unique<std::string>(securities::descriptor(
|
||||
handle.get_normalized_security()))});
|
||||
break;
|
||||
case MDRequestType::STATUS:
|
||||
for (auto &h : _status_subs) {
|
||||
// no need to reset status
|
||||
if (h.handle.key() == handle.key())
|
||||
break;
|
||||
}
|
||||
_status_subs.push_back(
|
||||
StatusRecord{handle, co2::TRADINGSTATUS_UNKNOWN});
|
||||
case MDRequestType::TICKER:
|
||||
// We aren't currently monitoring for pending ticker requests
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
_pending_handles.erase(
|
||||
std::remove_if(_pending_handles.begin(),
|
||||
_pending_handles.end(),
|
||||
[](const auto& h) { return h.handle->is_initialized(); }),
|
||||
_pending_handles.end());
|
||||
std::remove_if(
|
||||
_pending_handles.begin(), _pending_handles.end(),
|
||||
[](const auto &h) { return h.handle->is_initialized(); }),
|
||||
_pending_handles.end());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,18 +235,18 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
auto mhandle = _md_consumer.get_managed_handle(k);
|
||||
if (mhandle->is_initialized()) {
|
||||
auto handle = *mhandle;
|
||||
_handles.push_back({utils::TimeStamp(), handle,
|
||||
get_external_event_channel(handle.get_usersymbol()),
|
||||
std::make_unique<std::string>(securities::descriptor(
|
||||
handle.get_normalized_security()))});
|
||||
_handles.push_back(
|
||||
{utils::TimeStamp(), handle,
|
||||
get_external_event_channel(handle.get_usersymbol()),
|
||||
std::make_unique<std::string>(
|
||||
securities::descriptor(handle.get_normalized_security()))});
|
||||
if (handle.is_synthetic())
|
||||
synthetic_count_metric.increment();
|
||||
}
|
||||
else {
|
||||
// A pending handle is a handle for security we are reasonbly sure exists
|
||||
// but we haven't yet found live market data for.
|
||||
} else {
|
||||
// A pending handle is a handle for security we are reasonbly sure
|
||||
// exists but we haven't yet found live market data for.
|
||||
LOG(OrderMan, Warn, "No market data for {}. Pending handle added.", k);
|
||||
_pending_handles.push_back({ mhandle, MDRequestType::BOOK });
|
||||
_pending_handles.push_back({mhandle, MDRequestType::BOOK});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,8 +259,17 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
|
||||
void on_ticker_sub(SecurityKey sk,
|
||||
metrics::CounterMetric<int> &channel_count_metric) {
|
||||
auto handle = _md_consumer.get_managed_handle(sk.value());
|
||||
|
||||
auto user_symbol = _md_consumer.get_handle(sk.value()).get_usersymbol();
|
||||
if (!handle->is_initialized()) {
|
||||
LOG(OrderMan, Warn,
|
||||
"Received ticker subscription for security {} which does not have "
|
||||
"valid market data. Ignoring",
|
||||
sk.value());
|
||||
return;
|
||||
}
|
||||
|
||||
auto user_symbol = handle->get_usersymbol();
|
||||
for (auto &ch : _channels) {
|
||||
if (ch.user_symbol == user_symbol) {
|
||||
auto it =
|
||||
@@ -262,7 +277,7 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
[sk](auto const &ev) { return ev.key() == sk; });
|
||||
if (it == ch.securities.end()) {
|
||||
// first time we are asked for this security, we will
|
||||
// publish the trades for it next time next round
|
||||
// publish the trades for it next time around
|
||||
LOG(OrderMan, Warn,
|
||||
"Starting to publish ticker updates on user symbol {} for "
|
||||
"security key: {} (sec count for the symbol: {} )",
|
||||
@@ -294,7 +309,7 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
|
||||
void on_status_sub(SecurityKey sk) {
|
||||
auto k = sk.value();
|
||||
for (auto& h : _status_subs) {
|
||||
for (auto &h : _status_subs) {
|
||||
if (h.handle.key() == k) {
|
||||
// reset status so it get resent next on_poll
|
||||
h.status = co2::TRADINGSTATUS_UNKNOWN;
|
||||
@@ -303,11 +318,11 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
}
|
||||
auto handle = _md_consumer.get_managed_handle(k);
|
||||
if (handle->is_initialized()) {
|
||||
_status_subs.push_back(StatusRecord{ *handle, co2::TRADINGSTATUS_UNKNOWN });
|
||||
}
|
||||
else {
|
||||
_status_subs.push_back(
|
||||
StatusRecord{*handle, co2::TRADINGSTATUS_UNKNOWN});
|
||||
} else {
|
||||
LOG(OrderMan, Warn, "No market data for {}. Pending handle added.", k);
|
||||
_pending_handles.push_back({ handle, MDRequestType::STATUS });
|
||||
_pending_handles.push_back({handle, MDRequestType::STATUS});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -337,7 +352,7 @@ class MDSource : public mercury::Actor, public mercury::IPollableActor {
|
||||
md::MDConsumerHandle handle;
|
||||
co2::TradingStatus status;
|
||||
};
|
||||
boost::container::small_vector<StatusRecord,20> _status_subs;
|
||||
boost::container::small_vector<StatusRecord, 20> _status_subs;
|
||||
|
||||
md::MDConsumer _md_consumer;
|
||||
mercury::Stream<BookUpdateEvent> _book_update;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/OffsetAlgo.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/ScratchAlgo.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/ProRataAlgo.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/DeleteAndCoverAlgo.h"
|
||||
#include "lib/orderman/orders/book_algos/OCOOrder.h"
|
||||
#include "lib/orderman/orders/book_algos/PricerOrder.h"
|
||||
#include "lib/orderman/orders/spreader_orders/SpreaderOrder.h"
|
||||
@@ -242,6 +243,10 @@ Order &OrderFactory::allocate(OrderKey k, OrderKey pk,
|
||||
order_ptr.reset(
|
||||
new orderman::AlgoWrapperOrder(std::make_unique<splitter::SplitterAlgo>()));
|
||||
break;
|
||||
case CustomOrderDefinition::kDeleteAndCover:
|
||||
order_ptr.reset(
|
||||
new orderman::AlgoWrapperOrder(std::make_unique<DeleteAndCoverAlgo>()));
|
||||
break;
|
||||
case CustomOrderDefinition::CUSTOM_NOT_SET:
|
||||
bts_assert_msg(false, "Cannot create order with unknown type");
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
#include "BookUtils.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
using Size = bts::md::Size;
|
||||
using Side = bts::api::Side;
|
||||
|
||||
Size BookUtils::volume_at_or_better(const Book& book, Side side, Price price) {
|
||||
return volume_at_or_better(book, side, price.value());
|
||||
}
|
||||
Size BookUtils::volume_at_or_better(const Book& book, Side side, double price) {
|
||||
Size volume = 0;
|
||||
SidePriceComp spc(side);
|
||||
for (const auto& plevel : book.side(side)) {
|
||||
if (spc(price, plevel.price()))
|
||||
break;
|
||||
volume += plevel.size();
|
||||
}
|
||||
return volume;
|
||||
}
|
||||
|
||||
SidePriceComp BookUtils::price_comp(Side side) {
|
||||
return SidePriceComp(side);
|
||||
}
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/config/protos/MarketData.pb.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/domain_model/common.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/orderman/orderman_utils/PriceComp.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
struct BookUtils {
|
||||
static md::Size volume_at_or_better(const Book& book, api::Side side, Price price);
|
||||
static md::Size volume_at_or_better(const Book& book, api::Side side, double price);
|
||||
static SidePriceComp price_comp(api::Side side);
|
||||
};
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,283 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/config/protos/MarketData.pb.h"
|
||||
#include "lib/domain_model/common.h"
|
||||
#include "lib/md/PriceSize.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <sstream>
|
||||
#include <fmt/format.h>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
struct Formatter {
|
||||
using Side = bts::api::Side;
|
||||
// this is a placeholder for other book formats down the road.
|
||||
enum class BookFormat { BBO = 1 };
|
||||
enum class Tense {
|
||||
Present,
|
||||
Preterite
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
inline static void concat(std::ostream& ostr, const T& v) {
|
||||
ostr << v;
|
||||
}
|
||||
|
||||
template<typename T, typename... Args>
|
||||
inline static void concat(std::ostream& ostr, const T& first, Args... args) {
|
||||
ostr << first;
|
||||
(concat(ostr, args), ...);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline static std::string fmt(const T& curr) {
|
||||
std::stringstream str;
|
||||
str << curr;
|
||||
return str.str();
|
||||
}
|
||||
|
||||
template<typename T, typename... Args>
|
||||
inline static std::string fmt(const T& curr, Args... next) {
|
||||
std::stringstream str;
|
||||
concat(str, curr, next...);
|
||||
return str.str();
|
||||
}
|
||||
|
||||
template <class TKey, class TValue>
|
||||
static std::string fmt_values(const std::map<TKey, TValue>& map) {
|
||||
if (map.empty())
|
||||
return "None";
|
||||
std::stringstream str;
|
||||
auto it = map.begin();
|
||||
str << it->second;
|
||||
while (++it != map.end()) {
|
||||
str << ", " << it->second;
|
||||
}
|
||||
return str.str();
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static std::string fmt_values(const std::vector<T>& values) {
|
||||
if (values.empty())
|
||||
return "None";
|
||||
std::stringstream str;
|
||||
auto it = values.begin();
|
||||
str << *it;
|
||||
while (++it != values.end()) {
|
||||
str << ", " << *it;
|
||||
}
|
||||
return str.str();
|
||||
}
|
||||
|
||||
static std::string fmt_fill(Side side, uint32_t qty, double price) {
|
||||
std::stringstream ostr;
|
||||
ostr << to_string(side, Tense::Preterite) << " " << qty << " @ " << price;
|
||||
return ostr.str();
|
||||
}
|
||||
|
||||
static std::string fmt_order(Side side, uint32_t qty, double price) {
|
||||
std::stringstream ostr;
|
||||
ostr << to_string(side) << " " << qty << " @ " << price;
|
||||
return ostr.str();
|
||||
}
|
||||
|
||||
static std::string fmt_order(OrderKey key, uint32_t qty, double price, const api::orderman::OrderDefinition& def) {
|
||||
std::stringstream ostr;
|
||||
ostr << key << " " << to_string(def.side()) << " " << qty << " @ "
|
||||
<< price << std::endl << "[Def]" << utils::pb::to_string(def);
|
||||
return ostr.str();
|
||||
}
|
||||
|
||||
static std::string fmt_order(OrderKey key, Side side, uint32_t qty, double price, ChildState state) {
|
||||
std::stringstream ostr;
|
||||
ostr << key << " " << to_string(side) << " " << qty << " @ " << price
|
||||
<< " " << to_string(state);
|
||||
return ostr.str();
|
||||
}
|
||||
|
||||
private:
|
||||
template<typename T, typename... Args>
|
||||
inline static void join_next(std::stringstream&, const std::string&) { }
|
||||
|
||||
template<typename T, typename... Args>
|
||||
inline static void join_next(std::stringstream&, std::function<std::string()>) { }
|
||||
|
||||
template<typename T, typename... Args>
|
||||
inline static void join_next(std::stringstream& str, const std::string& separator, const T& curr) {
|
||||
str << separator << curr;
|
||||
}
|
||||
|
||||
template<typename T, typename... Args>
|
||||
inline static void join_next(std::stringstream& str, std::function<std::string()> get_separator, const T& curr) {
|
||||
str << get_separator() << curr;
|
||||
}
|
||||
|
||||
template<typename T, typename... Args>
|
||||
inline static void join_next(std::stringstream& str, const std::string& separator, const T& curr, Args... next) {
|
||||
str << separator << curr;
|
||||
(join_next(str, separator, next), ...);
|
||||
}
|
||||
|
||||
template<typename T, typename... Args>
|
||||
inline static void join_next(std::stringstream& str, std::function<std::string()> get_separator, const T& curr, Args... next) {
|
||||
str << get_separator() << curr;
|
||||
(join_next(str, get_separator, next), ...);
|
||||
}
|
||||
|
||||
public:
|
||||
template<typename T, typename... Next>
|
||||
inline static std::string join(const std::string&, const T& curr) {
|
||||
std::stringstream str;
|
||||
str << curr;
|
||||
return str.str();
|
||||
}
|
||||
|
||||
template<typename T, typename... Next>
|
||||
inline static std::string join(const std::string& separator, const T& curr, Next... next) {
|
||||
std::stringstream str;
|
||||
str << curr;
|
||||
join_next(str, separator, next...);
|
||||
return str.str();
|
||||
}
|
||||
|
||||
template<typename T, typename... Next>
|
||||
inline static std::string join(std::function<std::string()> get_separator, const T& curr, Next... next) {
|
||||
std::stringstream str;
|
||||
str << curr;
|
||||
join_next(str, get_separator, next...);
|
||||
return str.str();
|
||||
}
|
||||
|
||||
template <class T>
|
||||
static std::string to_string(const T& value) {
|
||||
std::stringstream str;
|
||||
str << value;
|
||||
return str.str();
|
||||
}
|
||||
|
||||
static std::string to_string(bool value) {
|
||||
return value ? "true" : "false";
|
||||
}
|
||||
|
||||
static std::string to_string(Side side, Tense tense = Tense::Present) {
|
||||
switch (side) {
|
||||
case bts::api::SIDE_BUY:
|
||||
switch (tense) {
|
||||
case Tense::Preterite:
|
||||
return "Bought";
|
||||
case Tense::Present:
|
||||
default:
|
||||
return "Buy";
|
||||
}
|
||||
case bts::api::SIDE_SELL:
|
||||
switch (tense) {
|
||||
case Tense::Preterite:
|
||||
return "Sold";
|
||||
case Tense::Present:
|
||||
default:
|
||||
return "Sell";
|
||||
}
|
||||
default:
|
||||
return fmt("Side_", static_cast<int>(side));
|
||||
}
|
||||
}
|
||||
|
||||
static std::string to_string(ChildStatus value) {
|
||||
switch (value) {
|
||||
case ChildStatus::WORKING: return "Working";
|
||||
case ChildStatus::COMPLETED: return "Completed";
|
||||
case ChildStatus::CANCELED: return "Canceled";
|
||||
default: return fmt("ChildStatus_", static_cast<int>(value));
|
||||
}
|
||||
}
|
||||
|
||||
static std::string to_string(ChildState value) {
|
||||
std::stringstream ostr;
|
||||
ostr << "[" << to_string(value.status) << "] "
|
||||
<< value.quantity.value() << " @ " << value.price.value()
|
||||
<< "(total filled " << value.total_fills.value() << " @[avg] "
|
||||
<< value.avg_price.value() << ", last filled "
|
||||
<< value.last_fills.value() << " @ "
|
||||
<< value.last_price.value() << ")";
|
||||
return ostr.str();
|
||||
}
|
||||
|
||||
static std::string to_string(const Book &book) {
|
||||
return to_string(book, BookFormat::BBO);
|
||||
}
|
||||
|
||||
static std::string to_string(const Book &book, BookFormat format) {
|
||||
// add other impls as needed
|
||||
std::stringstream ostr;
|
||||
switch (format) {
|
||||
case BookFormat::BBO:
|
||||
if (book.bid().size() > 0 && !book.bid().at(0).is_nan()) {
|
||||
auto best_bid = book.bid().at(0);
|
||||
ostr << best_bid.size() << "@" << best_bid.price();
|
||||
} else {
|
||||
ostr << "N/A";
|
||||
}
|
||||
ostr << " <> ";
|
||||
if (book.ask().size() > 0 && !book.ask().at(0).is_nan()) {
|
||||
auto best_ask = book.ask().at(0);
|
||||
ostr << best_ask.size() << "@" << best_ask.price();
|
||||
} else {
|
||||
ostr << "N/A";
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return ostr.str();
|
||||
}
|
||||
|
||||
static std::string to_string(api::orderman::OrderError error) {
|
||||
return api::orderman::OrderError_Name(error);
|
||||
}
|
||||
|
||||
static std::string to_string(api::orderman::CustomOrderDefinition::CustomCase algo_type) {
|
||||
switch (algo_type) {
|
||||
case api::orderman::CustomOrderDefinition::CustomCase::kLimit: return "Limit";
|
||||
case api::orderman::CustomOrderDefinition::CustomCase::kOffset: return "Offset";
|
||||
case api::orderman::CustomOrderDefinition::CustomCase::kCover: return "Cover";
|
||||
case api::orderman::CustomOrderDefinition::CustomCase::kIceberg: return "Iceberg";
|
||||
default: return fmt("AlgoType_", static_cast<int>(algo_type));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// << operators
|
||||
inline std::ostream &operator<<(std::ostream &ostr, const api::Side &side) {
|
||||
ostr << Formatter::to_string(side);
|
||||
return ostr;
|
||||
}
|
||||
|
||||
inline std::ostream &operator<<(std::ostream &ostr, const ChildStatus &child_status) {
|
||||
ostr << Formatter::to_string(child_status);
|
||||
return ostr;
|
||||
}
|
||||
|
||||
inline std::ostream &operator<<(std::ostream &ostr, const ChildState &child_state) {
|
||||
ostr << "[" << child_state.status << "] " << child_state.quantity.value()
|
||||
<< " @ " << child_state.price.value() << "(filled "
|
||||
<< child_state.total_fills.value() << " @avg "
|
||||
<< child_state.avg_price.value() << ")";
|
||||
return ostr;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& ostr, const bts::Price& price) {
|
||||
ostr << price.value();
|
||||
return ostr;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& ostr, const bts::Quantity& quantity) {
|
||||
ostr << quantity.value();
|
||||
return ostr;
|
||||
}
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
#include "lib/domain_model/Security.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
struct PriceComp {
|
||||
bool operator()(const Price& lhs, const Price& rhs) { return lhs.value() > rhs.value(); }
|
||||
bool operator()(const double& lhs, const double& rhs) { return lhs > rhs; }
|
||||
};
|
||||
|
||||
struct ReversePriceComp {
|
||||
bool operator()(const Price& lhs, const Price& rhs) { return lhs.value() < rhs.value(); }
|
||||
bool operator()(const double& lhs, const double& rhs) { return lhs < rhs; }
|
||||
};
|
||||
|
||||
struct SidePriceComp {
|
||||
SidePriceComp(api::Side side) : _side(side) {}
|
||||
|
||||
api::Side side() const { return _side; }
|
||||
|
||||
bool operator()(const Price& lhs, const Price& rhs) {
|
||||
return _side == api::SIDE_SELL ? lhs.value() < rhs.value() : lhs.value() > rhs.value();
|
||||
}
|
||||
|
||||
bool operator()(const double& lhs, const double& rhs) {
|
||||
return _side == api::SIDE_SELL ? lhs < rhs : lhs > rhs;
|
||||
}
|
||||
private:
|
||||
api::Side _side;
|
||||
};
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -1,3 +1,4 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
|
||||
@@ -18,6 +18,7 @@ struct CoverAlgo::PImpl {
|
||||
Price price_improvement_delta;
|
||||
bool cancel_on_external_hedge_cancel = false;
|
||||
bool cancel_hedges_on_cancel = false;
|
||||
bool detach_hedges = false;
|
||||
ChildTemplate *child_template = nullptr;
|
||||
ChildTemplate *hedge_template = nullptr;
|
||||
Quantity hedged_fills = Quantity(0);
|
||||
@@ -101,8 +102,12 @@ void CoverAlgo::on_covered_change(ChildState const &st) {
|
||||
me->price_improvement_delta);
|
||||
|
||||
auto hedge =
|
||||
algo().launch_child(*me->hedge_template, hedge_price, hedge_qty);
|
||||
me->hedges.push_back(hedge);
|
||||
algo().launch_child(*me->hedge_template, hedge_price, hedge_qty,
|
||||
!me->detach_hedges);
|
||||
if (me->detach_hedges)
|
||||
algo().detach_child(hedge);
|
||||
else
|
||||
me->hedges.push_back(hedge);
|
||||
|
||||
me->hedged_fills =
|
||||
Quantity(hedge_qty.value() + me->hedged_fills.value());
|
||||
@@ -247,6 +252,7 @@ void CoverAlgo::update_definition(
|
||||
me->price_improvement_delta = Price(
|
||||
securities::tick_size(serv.security()).value() * cover.hedge_ticks());
|
||||
me->cancel_hedges_on_cancel = cover.cancel_hedges_on_cancel();
|
||||
me->detach_hedges = cover.detach_hedges();
|
||||
me->cancel_on_external_hedge_cancel =
|
||||
cover.cancel_on_external_hedge_cancel();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/DeleteAndCoverAlgo.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/utils/PBUtils.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
using namespace api::orderman;
|
||||
|
||||
struct DeleteAndCoverAlgo::PImpl{
|
||||
IAlgoServices* algo_services = nullptr;
|
||||
OrderDefinition order_def;
|
||||
ChildTemplate* child_template = nullptr;
|
||||
OrderKey child_key;
|
||||
};
|
||||
|
||||
void copy_lean (LeanOrderDefinition& lean,
|
||||
const DeleteAndCoverDefinition& dc){
|
||||
lean.mutable_cross_threshold()->CopyFrom(dc.cross_threshold());
|
||||
lean.set_max_chase_ticks(dc.max_chase_ticks());
|
||||
switch(dc.trade_toward_behavior()){
|
||||
case LeanOnToward::NO_CHANGE:
|
||||
lean.set_trade_toward_behavior(LeanOrderDefinition::NO_CHANGE);
|
||||
break;
|
||||
case LeanOnToward::CANCEL:
|
||||
lean.set_trade_toward_behavior(LeanOrderDefinition::CANCEL);
|
||||
break;
|
||||
case LeanOnToward::DETACH:
|
||||
lean.set_trade_toward_behavior(LeanOrderDefinition::DETACH);
|
||||
break;
|
||||
case LeanOnToward::REPRICE:
|
||||
lean.set_trade_toward_behavior(LeanOrderDefinition::REPRICE);
|
||||
break;
|
||||
default:
|
||||
bts_assert(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void copy_top(TopOrderDefinition& top,
|
||||
const DeleteAndCoverDefinition& dc){
|
||||
top.mutable_top_threshold()->CopyFrom(dc.top_threshold());
|
||||
top.set_ticks_off(dc.ticks_off());
|
||||
}
|
||||
|
||||
void copy_cover(CoverOrderDefinition& cover,
|
||||
const DeleteAndCoverDefinition& dc){
|
||||
cover.set_detach_hedges(true);
|
||||
cover.set_hedge_ticks(dc.hedge_ticks());
|
||||
cover.set_cancel_on_external_hedge_cancel(dc.cancel_on_external_hedge_cancel());
|
||||
cover.set_cancel_hedges_on_cancel(dc.cancel_hedges_on_cancel());
|
||||
}
|
||||
|
||||
DeleteAndCoverAlgo::DeleteAndCoverAlgo() : me(new PImpl()) {}
|
||||
DeleteAndCoverAlgo::~DeleteAndCoverAlgo() {}
|
||||
IAlgoServices &DeleteAndCoverAlgo::algo() const { return *me->algo_services; }
|
||||
|
||||
// initialization /////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
OrderOpResult DeleteAndCoverAlgo::do_before_launch(IBeforeLaunchAlgoServices& serv) {
|
||||
auto &def = serv.definition();
|
||||
if(def.has_custom_def() && def.custom_def().has_delete_and_cover()){
|
||||
const auto& dc = def.custom_def().delete_and_cover();
|
||||
LOG(OrderMan, Info, "DeleteAndCoverAlgo do_before_launch args: {}",
|
||||
utils::pb::to_string(dc));
|
||||
// init child temp
|
||||
me->order_def = serv.definition();
|
||||
me->order_def.set_attach_target_order_key(null_order_key().value());
|
||||
// Lean
|
||||
LeanOrderDefinition lean;
|
||||
copy_lean(lean,dc);
|
||||
lean.mutable_child()->mutable_limit()->MergeFrom (
|
||||
LimitOrderDefinition());
|
||||
// Top
|
||||
TopOrderDefinition top;
|
||||
copy_top(top,dc);
|
||||
top.mutable_child()->mutable_limit()->MergeFrom (
|
||||
LimitOrderDefinition());
|
||||
// Cover
|
||||
CoverOrderDefinition cover;
|
||||
copy_cover(cover,dc);
|
||||
cover.mutable_child()->mutable_top()->MergeFrom (top);
|
||||
cover.mutable_hedge()->mutable_lean()->CopyFrom(lean);
|
||||
// put it all together
|
||||
me->order_def.mutable_custom_def()->mutable_cover()->MergeFrom (cover);
|
||||
me->child_template = &(serv.make_child_template(me->order_def));
|
||||
return {};
|
||||
}
|
||||
else
|
||||
return {OrderError::ORDER_ERROR_INVALID_PARAMETER,
|
||||
std::string("Parameters are not well formed")};
|
||||
}
|
||||
void DeleteAndCoverAlgo::do_launch(IAlgoServices& algo) {
|
||||
me->algo_services = &algo;
|
||||
bts_assert(me->child_template);
|
||||
me->child_key = algo.launch_child(*me->child_template, algo.price(), algo.quantity());
|
||||
}
|
||||
void DeleteAndCoverAlgo::do_launch_attached(IAlgoServices& algo,
|
||||
OrderKey k,
|
||||
ChildState st) {
|
||||
me->algo_services = &algo;
|
||||
me->child_key = k;
|
||||
on_child_change(k, st);
|
||||
}
|
||||
// updates ////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void DeleteAndCoverAlgo::do_update(const api::orderman::Update& u) {
|
||||
LOG(OrderMan, Info, "DeleteAndCoverAlgo order {} receiving update: {}",
|
||||
algo().order_key().value(), utils::pb::to_string(u));
|
||||
if (u.has_custom() && u.custom().has_delete_and_cover()){
|
||||
const auto& dc = u.custom().delete_and_cover();
|
||||
copy_cover(*me->order_def.mutable_custom_def()->mutable_cover(),dc);
|
||||
copy_lean(*me->order_def.mutable_custom_def()->mutable_cover()->mutable_hedge()->mutable_lean(),dc);
|
||||
copy_top(*me->order_def.mutable_custom_def()->mutable_cover()->mutable_child()->mutable_top(),dc);
|
||||
Update child_u = u;
|
||||
child_u.mutable_custom()->CopyFrom(me->order_def.custom_def());
|
||||
child_u.set_order_key (me->child_key.value());// is this needed?
|
||||
algo().update_child(me->child_key,child_u);
|
||||
}
|
||||
else
|
||||
LOG(OrderMan, Error, "DeleteAndCoverAlgo order {} Invalid update: {}",
|
||||
algo().order_key().value(), utils::pb::to_string(u));
|
||||
}
|
||||
// callbacks //////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
void DeleteAndCoverAlgo::on_child_change(OrderKey, const ChildState &st) {
|
||||
algo().update_fill_state(st.total_fills,st.last_price);
|
||||
switch (st.status){
|
||||
case ChildStatus::WORKING:
|
||||
break;
|
||||
case ChildStatus::CANCELED:
|
||||
algo().cancel();
|
||||
break;
|
||||
case ChildStatus::COMPLETED:
|
||||
algo().complete();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,28 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
class ChildTemplate;
|
||||
|
||||
class DeleteAndCoverAlgo : public IAlgoOrder {
|
||||
public:
|
||||
DeleteAndCoverAlgo();
|
||||
~DeleteAndCoverAlgo();
|
||||
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_child_change(OrderKey, ChildState const &) override;
|
||||
private:
|
||||
IAlgoServices &algo() const ;
|
||||
private:
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me;
|
||||
};
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,401 @@
|
||||
#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
|
||||
@@ -0,0 +1,94 @@
|
||||
#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
|
||||
@@ -0,0 +1,247 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/OnSignalAlgo.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/orderman/actors/SODPosition.h"
|
||||
#include "lib/os/os.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include "lib/utils/datetime.h"
|
||||
#include <google/protobuf/util/time_util.h>
|
||||
#include <boost/tokenizer.hpp>
|
||||
#include <stdint.h>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
using namespace api::orderman;
|
||||
using namespace google::protobuf;
|
||||
using namespace boost;
|
||||
|
||||
struct OnSignalLegData{
|
||||
const SecurityKey sec_key;
|
||||
ChildTemplate* order_template = nullptr;
|
||||
const Price price;
|
||||
const Quantity quantity;
|
||||
const api::Side side;
|
||||
OrderKey order_key;
|
||||
ChildState order_state;
|
||||
};
|
||||
|
||||
enum class OnSignalAlgo::State { WAITING_FOR_SIGNAL, WORKING, DONE };
|
||||
|
||||
struct OnSignalAlgoParams{
|
||||
std::optional<SecurityKey> signal_key_1 = std::nullopt;
|
||||
std::optional<google::protobuf::Timestamp> time = std::nullopt;
|
||||
bool preopen = false;
|
||||
bool open = false;
|
||||
std::vector<OnSignalLegData> legs;
|
||||
OnSignalLegData* find_leg_from_ord_key (OrderKey ord_key){
|
||||
for (auto& l : legs)
|
||||
if(l.order_key == ord_key)
|
||||
return &l;
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
struct OnSignalAlgo::PImpl {
|
||||
IAlgoServices* algo_services = nullptr;
|
||||
State state = State::WAITING_FOR_SIGNAL;
|
||||
OnSignalAlgoParams algo_params;
|
||||
PImpl(){
|
||||
}
|
||||
};
|
||||
|
||||
ChildTemplate* _get_template (IBeforeLaunchAlgoServices& serv,
|
||||
const CustomOrderDefinition& cod,
|
||||
SecurityKey key,
|
||||
api::Side side){
|
||||
OrderDefinition rv = serv.definition();
|
||||
rv.set_security_key(key.value());
|
||||
rv.set_side(side);
|
||||
rv.mutable_custom_def()->CopyFrom(cod);
|
||||
return &(serv.make_child_template(rv));
|
||||
}
|
||||
|
||||
bool _convert_params (IBeforeLaunchAlgoServices& serv,
|
||||
const CustomOrderDefinition& cod,
|
||||
OnSignalAlgoParams& out_p){
|
||||
const DynamicOrderDefinition& def = cod.dynamic();
|
||||
const auto& params = def.order_params();
|
||||
auto hh = get_if<uint32_t>(params, "Time Hours");
|
||||
auto mm = get_if<uint32_t>(params, "Time Minutes");
|
||||
auto ss = get_if<uint32_t>(params, "Time Seconds");
|
||||
if(hh.has_value() && hh.value() <= 23 &&
|
||||
mm.has_value() && mm.value() <= 59 &&
|
||||
ss.has_value() && ss.value() <= 59)
|
||||
out_p.time = SODPosition::today_local_time_to_utc_timestamp (
|
||||
hh.value(),mm.value(),ss.value(),true,false);
|
||||
auto preopen = get_if<bool>(params, "Preopen");
|
||||
out_p.preopen = preopen.has_value() ? preopen.value() : false;
|
||||
auto open = get_if<bool>(params, "Open");
|
||||
out_p.open = open.has_value() ? open.value() : false;
|
||||
auto k1 = get_if<uint32_t>(params, "Signal Contract 1");
|
||||
if(k1.has_value())
|
||||
out_p.signal_key_1 = SecurityKey(k1.value());
|
||||
for (int i=0;;i++){
|
||||
auto idx = std::to_string(i+1);
|
||||
auto contract = get_if<uint32_t>(params, "Order " + idx + " Contract");
|
||||
auto custom_ord = get_if<CustomOrderDefinition>(params, "Order " + idx);
|
||||
auto price = get_if<double>(params, "Order " + idx + " Price");
|
||||
auto qty = get_if<int32_t>(params, "Order " + idx + " Quantity");
|
||||
if(contract.has_value() && custom_ord.has_value() && price.has_value() &&
|
||||
qty.has_value() && qty.value() != 0){
|
||||
auto side = qty > 0 ? api::Side::SIDE_BUY : api::Side::SIDE_SELL;
|
||||
uint32_t eff_qty = std::abs(qty.value());
|
||||
auto key = SecurityKey(contract.value());
|
||||
ChildTemplate* child_template = _get_template(serv,custom_ord.value(),key,side);
|
||||
OnSignalLegData leg =
|
||||
{
|
||||
key,
|
||||
child_template,
|
||||
Price(price.value()),
|
||||
Quantity(eff_qty),
|
||||
side,
|
||||
OrderKey(0),
|
||||
ChildState()
|
||||
};
|
||||
out_p.legs.push_back(leg);
|
||||
}
|
||||
else
|
||||
break;
|
||||
}
|
||||
bool valid = out_p.legs.size() > 0 &&
|
||||
(out_p.time.has_value() ||
|
||||
(out_p.signal_key_1.has_value() && (out_p.preopen || out_p.open)));
|
||||
return valid;
|
||||
}
|
||||
|
||||
OnSignalAlgo::OnSignalAlgo() : me (new PImpl()){}
|
||||
OnSignalAlgo::~OnSignalAlgo() {}
|
||||
|
||||
OrderOpResult OnSignalAlgo::do_before_launch(IBeforeLaunchAlgoServices& serv){
|
||||
auto& def = serv.definition();
|
||||
if (def.has_custom_def() &&
|
||||
def.custom_def().has_dynamic() &&
|
||||
_convert_params(serv,def.custom_def(),me->algo_params))
|
||||
return {};
|
||||
return {OrderError::ORDER_ERROR_INVALID_PARAMETER,
|
||||
std::string("Parameters are not well formed")};
|
||||
}
|
||||
void OnSignalAlgo::do_launch(IAlgoServices& algo_services){
|
||||
me->algo_services = &algo_services;
|
||||
me->algo_services->subscribe_timer();
|
||||
if(me->algo_params.signal_key_1.has_value())
|
||||
me->algo_services->subscribe_status(me->algo_params.signal_key_1.value());
|
||||
}
|
||||
void OnSignalAlgo::do_launch_attached(IAlgoServices& algo_services,
|
||||
OrderKey,
|
||||
ChildState){
|
||||
me->algo_services = &algo_services;
|
||||
_cxl_and_end(true, "OnSignal Algo can not be attached to working orders");
|
||||
}
|
||||
void OnSignalAlgo::do_update(api::orderman::Update const &){
|
||||
LOG(OrderMan, Warn, "OnSignalAlgo::do_update order:{} state:{} update not allowed, please cxl and re-enter ",
|
||||
me->algo_services->order_key().value(),
|
||||
static_cast<int>(me->state));
|
||||
}
|
||||
void OnSignalAlgo::on_child_change(OrderKey key, const ChildState& st){
|
||||
LOG(OrderMan, Info,
|
||||
"OnSignalAlgo::on_child_change key:{} child_key:{} state:{} status:{} "
|
||||
"total_fills:{} last_price:{}",
|
||||
me->algo_services->order_key(),
|
||||
key,
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(st.status),
|
||||
st.total_fills,
|
||||
st.last_price);
|
||||
switch (me->state){
|
||||
case (OnSignalAlgo::State::WAITING_FOR_SIGNAL):
|
||||
LOG(OrderMan, Error,
|
||||
"OnSignalAlgo::on_child_change key:{} in WAITING_FOR_SIGNAL",
|
||||
me->algo_services->order_key());
|
||||
break;
|
||||
case (OnSignalAlgo::State::WORKING):
|
||||
{
|
||||
auto ld_ptr = me->algo_params.find_leg_from_ord_key(key);
|
||||
if (ld_ptr != nullptr)
|
||||
ld_ptr->order_state = st;
|
||||
}
|
||||
for (const auto& l : me->algo_params.legs)
|
||||
if(l.order_state.status == ChildStatus::WORKING)
|
||||
return;
|
||||
_cxl_and_end(false,"");
|
||||
break;
|
||||
case (OnSignalAlgo::State::DONE):
|
||||
break;
|
||||
}
|
||||
}
|
||||
void OnSignalAlgo::on_status(SecurityKey key, co2::TradingStatus s){
|
||||
if(me->algo_params.signal_key_1.has_value() && me->algo_params.signal_key_1 == key)
|
||||
switch (me->state){
|
||||
case (OnSignalAlgo::State::WAITING_FOR_SIGNAL):
|
||||
if(me->algo_params.preopen && s == co2::TradingStatus::TRADINGSTATUS_PRE_OPEN)
|
||||
_fire_orders();
|
||||
else if(me->algo_params.open && s == co2::TradingStatus::TRADINGSTATUS_NORMAL)
|
||||
_fire_orders();
|
||||
break;
|
||||
case (OnSignalAlgo::State::WORKING):
|
||||
case (OnSignalAlgo::State::DONE):
|
||||
break;
|
||||
}
|
||||
}
|
||||
void OnSignalAlgo::on_timer(){
|
||||
if(me->algo_params.time.has_value())
|
||||
switch (me->state){
|
||||
case (OnSignalAlgo::State::WAITING_FOR_SIGNAL):
|
||||
{
|
||||
auto thresh = me->algo_params.time.value();
|
||||
auto now = util::TimeUtil::GetCurrentTime();
|
||||
auto diff = util::TimeUtil::TimestampToMicroseconds(now) -
|
||||
util::TimeUtil::TimestampToMicroseconds(thresh);
|
||||
if(diff >= 0)
|
||||
_fire_orders();
|
||||
break;
|
||||
}
|
||||
case (OnSignalAlgo::State::WORKING):
|
||||
case (OnSignalAlgo::State::DONE):
|
||||
break;
|
||||
}
|
||||
}
|
||||
void OnSignalAlgo::_fire_orders(){
|
||||
for (auto& l : me->algo_params.legs)
|
||||
l.order_key = me->algo_services->launch_child(*l.order_template,l.price,l.quantity);
|
||||
_switch_state(State::WORKING);
|
||||
}
|
||||
void OnSignalAlgo::_cxl_and_end (bool is_error, const std::string& err_msg){
|
||||
if(is_error)
|
||||
me->algo_services->terminate_error(err_msg);
|
||||
else
|
||||
me->algo_services->complete();
|
||||
_switch_state(OnSignalAlgo::State::DONE);
|
||||
}
|
||||
void OnSignalAlgo::_switch_state (State new_state){
|
||||
switch (new_state){
|
||||
case (OnSignalAlgo::State::WAITING_FOR_SIGNAL):
|
||||
bts_assert(false);
|
||||
break;
|
||||
case (OnSignalAlgo::State::WORKING):
|
||||
bts_assert(me->state == OnSignalAlgo::State::WAITING_FOR_SIGNAL);
|
||||
break;
|
||||
case (OnSignalAlgo::State::DONE):
|
||||
bts_assert(me->state != OnSignalAlgo::State::DONE);
|
||||
break;
|
||||
}
|
||||
LOG(OrderMan, Info,"OnSignalAlgo::_switch_state key:{} {}->{} ",
|
||||
me->algo_services->order_key(),
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(new_state));
|
||||
me->state = new_state;
|
||||
}
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,36 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
class ChildTemplate;
|
||||
struct LegData;
|
||||
|
||||
class OnSignalAlgo : public IAlgoOrder {
|
||||
public:
|
||||
OnSignalAlgo();
|
||||
~OnSignalAlgo();
|
||||
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_status(SecurityKey, co2::TradingStatus) override;
|
||||
void on_timer() override;
|
||||
|
||||
private:
|
||||
enum class State;
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me;
|
||||
private:
|
||||
IAlgoServices& _algo() const ;
|
||||
// acts on md, cognizant of the state
|
||||
void _switch_state (State new_state);
|
||||
void _fire_orders();
|
||||
void _cxl_and_end (bool is_error, const std::string& err_msg);
|
||||
};
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -16,7 +16,7 @@ namespace orderman {
|
||||
|
||||
using namespace api::orderman;
|
||||
|
||||
enum class PyramidAlgo::Status { WAITING_FOR_MD, WAITING_FOR_MD_ATTACH, WORKING, DONE };
|
||||
enum class PyramidAlgo::Status { WAITING_FOR_MD, WAITING_FOR_MD_ATTACH, WORKING, WAITING_FOR_CXL_ACK, DONE };
|
||||
enum class PyramidAlgo::ExitPath { CANCEL, ERR, COMPLETE };
|
||||
|
||||
struct LegData{
|
||||
@@ -192,6 +192,7 @@ void PyramidAlgo::do_update(const api::orderman::Update& update) {
|
||||
"Order cancelled (Cover prices not met)");
|
||||
break;
|
||||
}
|
||||
case PyramidAlgo::Status::WAITING_FOR_CXL_ACK:
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
@@ -215,6 +216,7 @@ IAlgoOrder::OnCancelResponse PyramidAlgo::on_cancel(){
|
||||
case PyramidAlgo::Status::WORKING:
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,"Algo cancelled");
|
||||
break;
|
||||
case (PyramidAlgo::Status::WAITING_FOR_CXL_ACK):
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
@@ -242,6 +244,7 @@ void PyramidAlgo::on_child_change(OrderKey key, const ChildState &st) {
|
||||
_check_for_completion();
|
||||
break;
|
||||
case PyramidAlgo::Status::WORKING:
|
||||
case (PyramidAlgo::Status::WAITING_FOR_CXL_ACK):
|
||||
leg->order_state = st;
|
||||
_algo().update_fill_state(st.total_fills, st.avg_price);
|
||||
_send_hedges(st.last_fills);
|
||||
@@ -388,6 +391,7 @@ void PyramidAlgo::_analyze_md_and_act(){
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order cancelled (Cover prices not met)");
|
||||
break;
|
||||
case PyramidAlgo::Status::WAITING_FOR_CXL_ACK:
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
@@ -405,14 +409,19 @@ void PyramidAlgo::_switch_state(Status new_state){
|
||||
bts_assert(me->state == PyramidAlgo::Status::WAITING_FOR_MD ||
|
||||
me->state == PyramidAlgo::Status::WAITING_FOR_MD_ATTACH);
|
||||
break;
|
||||
case (PyramidAlgo::Status::WAITING_FOR_CXL_ACK):
|
||||
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);
|
||||
// when the child order enters a terminal state, while the algo
|
||||
// is in a working state (external cxl, target reached)
|
||||
me->state == PyramidAlgo::Status::WORKING ||
|
||||
// the usual controlled cxl, ie working->waiting_for_cxl_ack->done
|
||||
me->state == PyramidAlgo::Status::WAITING_FOR_CXL_ACK);
|
||||
break;
|
||||
}
|
||||
LOG(OrderMan, Info,"PyramidAlgo::_switch_state key:{} {}->{} ",
|
||||
@@ -494,8 +503,8 @@ void PyramidAlgo::_check_for_completion(){
|
||||
break;
|
||||
case (ChildStatus::CANCELED):
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::ERR,
|
||||
"Top spread unexpectedly cancelled");
|
||||
break;
|
||||
"Top spread cancelled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -507,8 +516,11 @@ void PyramidAlgo::_cxl_and_end(PyramidAlgo::ExitPath exit_path,
|
||||
if(top_ord_state.status == ChildStatus::WORKING){
|
||||
if(detach_top)
|
||||
_algo().detach_child(me->legs[0].order_key);
|
||||
else
|
||||
else{
|
||||
_algo().cancel_child(me->legs[0].order_key);
|
||||
_switch_state(PyramidAlgo::Status::WAITING_FOR_CXL_ACK);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
switch(exit_path){
|
||||
@@ -524,6 +536,7 @@ void PyramidAlgo::_cxl_and_end(PyramidAlgo::ExitPath exit_path,
|
||||
}
|
||||
_switch_state(PyramidAlgo::Status::DONE);
|
||||
}
|
||||
|
||||
// true the out qty is changed it be below
|
||||
// the filled qty
|
||||
bool PyramidAlgo::_target_reached() const{
|
||||
@@ -538,6 +551,14 @@ bool PyramidAlgo::_md_recvd() const{
|
||||
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;
|
||||
@@ -545,10 +566,17 @@ bool PyramidAlgo::_working_state_met() const{
|
||||
if (i++ > 0)
|
||||
cover_sum += l.opp_md()[0].price();
|
||||
bool rv = !os::is_nan(cover_sum) &&
|
||||
((me->side == api::Side::SIDE_BUY &&
|
||||
me->out_price.value() <= cover_sum) ||
|
||||
(me->side == api::Side::SIDE_SELL &&
|
||||
me->out_price.value() >= 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
|
||||
|
||||
@@ -3,12 +3,15 @@
|
||||
#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;
|
||||
@@ -198,11 +201,39 @@ extern "C" void init_tas_plugin(bts::SparkServices& srv) {
|
||||
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?");
|
||||
*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())
|
||||
@@ -213,6 +244,10 @@ extern "C" void init_spark_plugin(bts::SparkServices &srv) {
|
||||
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,16 +1,4 @@
|
||||
// TODO
|
||||
// put settle time data in the input (X)
|
||||
// better names (X)
|
||||
// at end of settle, order should dettach and complete (X)
|
||||
// order going out logic (X)
|
||||
// settlement compute logic (X)
|
||||
// actual publish (X)
|
||||
// no PTime
|
||||
// check settle calc (X)
|
||||
// check order shooting logic
|
||||
|
||||
#include "lib/orderman/orders/algo/algo_orders/TASCounterAlgo.h"
|
||||
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
@@ -38,17 +26,21 @@ enum class TASCounterAlgo::Status { WORKING, DONE };
|
||||
// (the vwap of the TAS contracts is not used, but for uniformity is kept in the
|
||||
// the same structure)
|
||||
struct ContractMarketData{
|
||||
private:
|
||||
uint32_t tot_vol = 0;
|
||||
double running_sum = 0;
|
||||
public:
|
||||
Book book;
|
||||
double estimated_settle = 0.0;
|
||||
void on_trade (const TickerUpdateEvent::Trade& t){
|
||||
tot_vol += t.quantity.value();
|
||||
running_sum += t.price.value()*t.quantity.value();
|
||||
}
|
||||
const double vwap(){
|
||||
double vwap() const{
|
||||
return tot_vol == 0 ? os::nan() : running_sum/tot_vol;
|
||||
}
|
||||
uint32_t traded_qty() const {return tot_vol;}
|
||||
double traded_value() const {return running_sum;}
|
||||
};
|
||||
// the input for the t1 front, second and third orders, as well as the template
|
||||
// and working order key are kept here
|
||||
@@ -112,6 +104,47 @@ bool trade_within_time_win (const Timestamp&, const Timestamp&,
|
||||
const TickerUpdateEvent::Trade& t);
|
||||
bool now_within_time_win (const Timestamp&, const Timestamp&);
|
||||
|
||||
double compute_t1_front_settle (const AlgoData& d){
|
||||
double rv = d.t1_front.vwap();
|
||||
rv = round_to_nearest(rv,d.tick_size);
|
||||
return rv;
|
||||
}
|
||||
|
||||
double compute_t1_second_settle (const AlgoData& d){
|
||||
double p_t1_front = compute_t1_front_settle(d);
|
||||
double part_1 = d.t1_second.traded_value();
|
||||
double part_2 = d.t2_front.traded_value() -
|
||||
(d.t2_front.traded_qty() * p_t1_front);
|
||||
uint32_t traded_qty_sum = d.t2_front.traded_qty() +
|
||||
d.t1_second.traded_qty();
|
||||
double rv = traded_qty_sum == 0 ? os::nan() :
|
||||
(part_1+part_2)/traded_qty_sum;
|
||||
LOG(OrderMan, Diag, "TASCounterAlgo::compute_t1_second_settle "
|
||||
"p_t1_front:{} part_1:{} part_2:{} traded_qty_sum:{} rv:{}",
|
||||
p_t1_front, part_1, part_2, traded_qty_sum, rv);
|
||||
return round_to_nearest(rv,d.tick_size);
|
||||
}
|
||||
|
||||
double compute_t1_third_settle (const AlgoData& d){
|
||||
double p_t1_front = compute_t1_front_settle(d);
|
||||
double p_t1_second = compute_t1_second_settle(d);
|
||||
double part_1 = d.t1_third.traded_value();
|
||||
double part_2 = d.t2_second.traded_value() -
|
||||
(d.t2_second.traded_qty()*p_t1_second);
|
||||
double part_3 = d.t3_front.traded_value() -
|
||||
(d.t3_front.traded_qty()*p_t1_front) -
|
||||
(d.t3_front.traded_qty()*p_t1_second);
|
||||
double traded_qty_sum = d.t3_front.traded_qty() +
|
||||
d.t2_second.traded_qty() +
|
||||
d.t1_third.traded_qty();
|
||||
double rv = traded_qty_sum == 0 ? os::nan() :
|
||||
(part_1+part_2+part_3)/traded_qty_sum;
|
||||
LOG(OrderMan, Diag, "TASCounterAlgo::compute_t1_third_settle "
|
||||
"p_t1_front:{} p_t1_second:{} part_1:{} part_2:{} part_3:{} "
|
||||
"traded_qty_sum:{} rv:{}",
|
||||
p_t1_front, p_t1_second, part_1, part_2, part_3, traded_qty_sum, rv);
|
||||
return round_to_nearest(rv,d.tick_size);
|
||||
}
|
||||
|
||||
TASCounterAlgo::TASCounterAlgo() : me(new PImpl()) {}
|
||||
TASCounterAlgo::~TASCounterAlgo() {}
|
||||
@@ -226,17 +259,9 @@ void TASCounterAlgo::_cxl_and_end (bool is_error, const std::string& err_msg){
|
||||
|
||||
void TASCounterAlgo::_compute_settles(){
|
||||
AlgoData& d = me->data;
|
||||
d.t1_front.estimated_settle = d.t1_front.vwap();
|
||||
d.t1_second.estimated_settle = d.t2_front.vwap() - d.t1_front.vwap();
|
||||
d.t1_third.estimated_settle = d.t3_front.vwap() - d.t1_front.vwap() -
|
||||
d.t1_second.vwap();
|
||||
// round to closest value
|
||||
d.t1_front.estimated_settle = round_to_nearest(
|
||||
d.t1_front.estimated_settle,me->data.tick_size);
|
||||
d.t1_second.estimated_settle = round_to_nearest(
|
||||
d.t1_second.estimated_settle,me->data.tick_size);
|
||||
d.t1_third.estimated_settle = round_to_nearest(
|
||||
d.t1_third.estimated_settle,me->data.tick_size);
|
||||
d.t1_front.estimated_settle = compute_t1_front_settle(d);
|
||||
d.t1_second.estimated_settle = compute_t1_second_settle(d);
|
||||
d.t1_third.estimated_settle = compute_t1_third_settle(d);
|
||||
}
|
||||
|
||||
// the decision to send an order can be caused by
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverAdapter.h"
|
||||
#include "lib/orderman/orders/OrderOpResult.h"
|
||||
#include "lib/orderman/orders/algo/utils/DynamicOrderAdapter.h"
|
||||
#include <utility>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
template<typename T>
|
||||
using Param = bts::orderman::dynamic_adapter::ParamKey<bts::orderman::LimitCoverDefinition, T>;
|
||||
using DynamicOrderEnumerated = bts::api::orderman::DynamicOrderEnumerated;
|
||||
using OnToward = bts::api::orderman::LeanOnToward;
|
||||
using LimitCoverDefinition = bts::orderman::LimitCoverDefinition;
|
||||
using namespace bts::api::orderman;
|
||||
#define ON_TOWARD_NAME_PAIR(x) std::make_pair(x, #x)
|
||||
|
||||
template<uint32_t gate_num>
|
||||
std::string gate_param_name(std::string name) {
|
||||
return Formatter::fmt("Gate ", gate_num, " ", name);
|
||||
}
|
||||
|
||||
template<>
|
||||
std::string gate_param_name<0>(std::string name) {
|
||||
return Formatter::fmt("Initial ", name);
|
||||
}
|
||||
|
||||
struct PriceOffset : public Param<double> {
|
||||
PriceOffset() : Param<double>(std::string("Limit"),
|
||||
[](const LimitCoverDefinition& def) { return def.price_offset(); },
|
||||
[](LimitCoverDefinition& def, double v) { def.price_offset(v); }) {}
|
||||
};
|
||||
|
||||
template<uint32_t gate_num>
|
||||
struct ActivationThreshold : public Param<uint32_t> {
|
||||
ActivationThreshold() : Param<uint32_t>(gate_param_name<gate_num>("Activation Threshold"),
|
||||
[](const LimitCoverDefinition& def) { return def.gate(gate_num).activation_threshold(); },
|
||||
[](LimitCoverDefinition& def, uint32_t v) { def.mgate(gate_num).activation_threshold(v); }) { }
|
||||
};
|
||||
|
||||
template<uint32_t gate_num>
|
||||
struct CoverThreshold : public Param<uint32_t> {
|
||||
CoverThreshold() : Param<uint32_t>(gate_param_name<gate_num>("Cover Threshold"),
|
||||
[](const LimitCoverDefinition& def) { return def.gate(gate_num).cover_threshold(); },
|
||||
[](LimitCoverDefinition& def, uint32_t v) { def.mgate(gate_num).cover_threshold(v); }) { }
|
||||
};
|
||||
|
||||
template<uint32_t gate_num>
|
||||
struct DeleteThreshold : public Param<uint32_t> {
|
||||
DeleteThreshold() : Param<uint32_t>(gate_param_name<gate_num>("Delete Threshold"),
|
||||
[](const LimitCoverDefinition& def) { return def.gate(gate_num).delete_threshold(); },
|
||||
[](LimitCoverDefinition& def, uint32_t v) { def.mgate(gate_num).delete_threshold(v); }) { }
|
||||
};
|
||||
|
||||
template<uint32_t gate_num>
|
||||
struct HedgeTicks : public Param<uint32_t> {
|
||||
HedgeTicks() : Param<uint32_t>(gate_param_name<gate_num>("Hedge Ticks"),
|
||||
[](const LimitCoverDefinition& def) { return def.gate(gate_num).hedge_ticks(); },
|
||||
[](LimitCoverDefinition& def, uint32_t v) { def.mgate(gate_num).hedge_ticks(v); }) { }
|
||||
};
|
||||
|
||||
template<uint32_t gate_num>
|
||||
struct TopTicksOff : public Param<uint32_t> {
|
||||
TopTicksOff() : Param<uint32_t>(gate_param_name<gate_num>("Top Ticks Offset"),
|
||||
[](const LimitCoverDefinition& def) { return def.gate(gate_num).top_ticks_offset(); },
|
||||
[](LimitCoverDefinition& def, uint32_t v) { def.mgate(gate_num).top_ticks_offset(v); }) { }
|
||||
};
|
||||
|
||||
template<uint32_t gate_num>
|
||||
struct MaxChaseTicks : public Param<uint32_t> {
|
||||
MaxChaseTicks() : Param<uint32_t>(gate_param_name<gate_num>("Max Chase Ticks"),
|
||||
[](const LimitCoverDefinition& def) { return def.gate(gate_num).max_chase_ticks(); },
|
||||
[](LimitCoverDefinition& def, uint32_t v) { def.mgate(gate_num).max_chase_ticks(v); }) { }
|
||||
};
|
||||
|
||||
template<uint32_t gate_num>
|
||||
struct TradeTowardBehavior : public dynamic_adapter::EnumParamKey<LimitCoverDefinition, OnToward> {
|
||||
TradeTowardBehavior() : EnumParamKey<LimitCoverDefinition, OnToward>(
|
||||
gate_param_name<gate_num>("Trade Toward Behavior"), {
|
||||
ON_TOWARD_NAME_PAIR(NO_CHANGE),
|
||||
ON_TOWARD_NAME_PAIR(CANCEL),
|
||||
ON_TOWARD_NAME_PAIR(DETACH),
|
||||
ON_TOWARD_NAME_PAIR(REPRICE)
|
||||
},
|
||||
[](const LimitCoverDefinition& def) { return def.gate(gate_num).on_toward(); },
|
||||
[](LimitCoverDefinition& def, OnToward v) { def.mgate(gate_num).on_toward(v); })
|
||||
{}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct ActivationThreshold<0> : public Param<uint32_t> {
|
||||
ActivationThreshold() : Param<uint32_t>(std::string("Entry Threshold"),
|
||||
[](const LimitCoverDefinition& def) { return def.gate(0).activation_threshold(); },
|
||||
[](LimitCoverDefinition& def, uint32_t v) {
|
||||
auto& g = def.mgate(0);
|
||||
g.activation_threshold(v);
|
||||
g.cover_threshold(v);
|
||||
g.delete_threshold(v);
|
||||
}) {}
|
||||
};
|
||||
|
||||
using Adapter = dynamic_adapter::DynamicOrderAdapter<LimitCoverDefinition,
|
||||
PriceOffset,
|
||||
// [gate 0]
|
||||
ActivationThreshold<0>,
|
||||
HedgeTicks<0>,
|
||||
TopTicksOff<0>,
|
||||
MaxChaseTicks<0>,
|
||||
TradeTowardBehavior<0>,
|
||||
// gate 1
|
||||
ActivationThreshold<1>,
|
||||
CoverThreshold<1>,
|
||||
DeleteThreshold<1>,
|
||||
HedgeTicks<1>,
|
||||
TopTicksOff<1>,
|
||||
MaxChaseTicks<1>,
|
||||
TradeTowardBehavior<1>,
|
||||
// gate 2
|
||||
ActivationThreshold<2>,
|
||||
CoverThreshold<2>,
|
||||
DeleteThreshold<2>,
|
||||
HedgeTicks<2>,
|
||||
TopTicksOff<2>,
|
||||
MaxChaseTicks<2>,
|
||||
TradeTowardBehavior<2>>;
|
||||
|
||||
LimitCoverDefinition LimitCoverAdapter::from_proto(const api::orderman::OrderDefinition& order) {
|
||||
if (order.has_custom_def())
|
||||
return from_proto(order.custom_def());
|
||||
return LimitCoverDefinition();
|
||||
}
|
||||
LimitCoverDefinition LimitCoverAdapter::from_proto(const api::orderman::CustomOrderDefinition& custom_def) {
|
||||
LimitCoverDefinition rv;
|
||||
if (custom_def.has_dynamic())
|
||||
Adapter().from_proto(custom_def.dynamic(), rv);
|
||||
return rv;
|
||||
}
|
||||
|
||||
#define INVALID_DEF(x) OrderOpResult(api::orderman::ORDER_ERROR_INVALID_ORDER_DEFINITON, x)
|
||||
|
||||
OrderOpResult LimitCoverAdapter::validate_proto(const api::orderman::DynamicOrderDefinition& dyn) {
|
||||
return Adapter().validate_proto(dyn) ?
|
||||
OrderOpResult() :
|
||||
INVALID_DEF("One or more dynamic parameters are missing or invalid.");
|
||||
}
|
||||
void LimitCoverAdapter::init_proto(const LimitCoverDefinition& def, api::orderman::DynamicOrderDefinition& dyn) {
|
||||
Adapter().to_proto(dyn, def);
|
||||
}
|
||||
|
||||
void LimitCoverAdapter::define_proto(api::orderman::DynamicOrderDescriptor& desc) {
|
||||
Adapter().define_proto(desc);
|
||||
}
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,15 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverDefinition.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
struct LimitCoverAdapter {
|
||||
static LimitCoverDefinition from_proto(const api::orderman::OrderDefinition& order);
|
||||
static LimitCoverDefinition from_proto(const api::orderman::CustomOrderDefinition& custom_def);
|
||||
static OrderOpResult validate_proto(const api::orderman::DynamicOrderDefinition& order);
|
||||
static void init_proto(const LimitCoverDefinition& def, api::orderman::DynamicOrderDefinition& dyn);
|
||||
static void define_proto(api::orderman::DynamicOrderDescriptor& desc);
|
||||
};
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverGate.h"
|
||||
#include "lib/orderman/orderman_utils/Formatter.h"
|
||||
#include <ostream>
|
||||
#include <map>
|
||||
#include <vector>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace limit_cover {
|
||||
// The exclusive maximum number of gates.
|
||||
// Note: the limit cover spec called for 3 gates (2 gates explicitly,
|
||||
// plus an initial activation threshold, which is treated as another gate).
|
||||
const uint32_t max_num_gates = 4;
|
||||
}
|
||||
struct LimitCoverDefinition {
|
||||
// price offset
|
||||
double price_offset() const {
|
||||
return _price_offset;
|
||||
}
|
||||
void price_offset(double value) {
|
||||
_price_offset = value;
|
||||
}
|
||||
|
||||
LimitCoverGate gate(uint32_t index) const {
|
||||
return _gate_or_default(index);
|
||||
}
|
||||
LimitCoverGate& mgate(uint32_t index) {
|
||||
return _ensure_gate(index);
|
||||
}
|
||||
void gate(uint32_t index, LimitCoverGate value) {
|
||||
_ensure_gate(index) = value;
|
||||
}
|
||||
|
||||
// The original LimitAlgo spec called for an initial activation thresh, and 2 gates
|
||||
// (fyi a gate has 3 members: activation thresh, cover thresh, and delete thresh).
|
||||
// The initial activation thresh serves as the initial cover, and delete threshes as well.
|
||||
// In this sense, the activation threshold can be considered as an 'initial gate' itself.
|
||||
// 'normal_gates' does just that: returns a map of both gates plus the 'initial gate.'
|
||||
std::map<uint32_t, LimitCoverGate> normal_gates() const {
|
||||
std::map<uint32_t, LimitCoverGate> m;
|
||||
for (auto g : _gates)
|
||||
m.insert(std::make_pair(g.activation_threshold(), g));
|
||||
return m;
|
||||
}
|
||||
private:
|
||||
double _price_offset;
|
||||
std::vector<LimitCoverGate> _gates;
|
||||
|
||||
LimitCoverGate& _ensure_gate(uint32_t index) {
|
||||
bts_assert(index < limit_cover::max_num_gates);
|
||||
while (_gates.size() <= index)
|
||||
_gates.push_back(LimitCoverGate());
|
||||
return _gates[index];
|
||||
}
|
||||
|
||||
LimitCoverGate _gate_or_default(uint32_t index) const {
|
||||
return _gates.size() > index ? _gates[index] : LimitCoverGate();
|
||||
}
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& ostr, const LimitCoverDefinition& lcd) {
|
||||
auto ngates = lcd.normal_gates();
|
||||
ostr << "Price Offset: " << lcd.price_offset()
|
||||
<< ", Activation Thresh: " << lcd.gate(0).activation_threshold()
|
||||
<< ", Gates: " << Formatter::fmt_values(ngates);
|
||||
return ostr;
|
||||
}
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,87 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/sparkproto/SparkProtos.pb.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include <ostream>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
struct LimitCoverGate {
|
||||
LimitCoverGate() : LimitCoverGate(0, 0, 0) {}
|
||||
|
||||
LimitCoverGate(const LimitCoverGate& other)
|
||||
: _activation_threshold(other._activation_threshold), _definition(other._definition) {}
|
||||
|
||||
LimitCoverGate(uint32_t activation_thresh, uint32_t cover_thresh, uint32_t delete_thresh)
|
||||
: _activation_threshold(activation_thresh), _definition() {
|
||||
cover_threshold(cover_thresh);
|
||||
delete_threshold(delete_thresh);
|
||||
}
|
||||
|
||||
// activation threshold
|
||||
uint32_t activation_threshold() const { return _activation_threshold; }
|
||||
void activation_threshold(uint32_t value) { _activation_threshold = value; }
|
||||
|
||||
// cover threshold
|
||||
uint32_t cover_threshold() const {
|
||||
return _definition.has_cross_threshold() ? _get_absolute(_definition.cross_threshold()) : 0;
|
||||
}
|
||||
void cover_threshold(uint32_t value) {
|
||||
if (!_definition.has_cross_threshold()) {
|
||||
*_definition.mutable_cross_threshold() = bts::spark::QuantityMeasure();
|
||||
}
|
||||
_definition.mutable_cross_threshold()->set_absolute(value);
|
||||
}
|
||||
|
||||
// delete threshold
|
||||
uint32_t delete_threshold() const {
|
||||
return _definition.has_top_threshold() ? _get_absolute(_definition.top_threshold()) : 0;
|
||||
}
|
||||
void delete_threshold(uint32_t value) {
|
||||
if (!_definition.has_top_threshold()) {
|
||||
*_definition.mutable_top_threshold() = bts::spark::QuantityMeasure();
|
||||
}
|
||||
_definition.mutable_top_threshold()->set_absolute(value);
|
||||
}
|
||||
|
||||
// hedge ticks
|
||||
uint32_t hedge_ticks() const { return _definition.hedge_ticks(); }
|
||||
void hedge_ticks(uint32_t value) { _definition.set_hedge_ticks(value); }
|
||||
// top ticks offset
|
||||
uint32_t top_ticks_offset() const { return _definition.ticks_off(); }
|
||||
void top_ticks_offset(uint32_t value) { _definition.set_ticks_off(value); }
|
||||
// max chase ticks
|
||||
uint32_t max_chase_ticks() const { return _definition.max_chase_ticks(); }
|
||||
void max_chase_ticks(uint32_t value) { _definition.set_max_chase_ticks(value); }
|
||||
// on toward
|
||||
api::orderman::LeanOnToward on_toward() const { return _definition.trade_toward_behavior(); }
|
||||
void on_toward(api::orderman::LeanOnToward value) { _definition.set_trade_toward_behavior(value); }
|
||||
// definition
|
||||
const api::orderman::DeleteAndCoverDefinition& definition() const { return _definition; }
|
||||
api::orderman::DeleteAndCoverDefinition& mdefinition() { return _definition; }
|
||||
void definition(api::orderman::DeleteAndCoverDefinition value) { _definition = value; }
|
||||
|
||||
api::orderman::CustomOrderDefinition custom_def() const {
|
||||
api::orderman::CustomOrderDefinition cod;
|
||||
*(cod.mutable_delete_and_cover()) = definition();
|
||||
return cod;
|
||||
}
|
||||
void custom_def(const api::orderman::CustomOrderDefinition &cod) {
|
||||
if (cod.has_delete_and_cover())
|
||||
definition(cod.delete_and_cover());
|
||||
}
|
||||
private:
|
||||
uint32_t _activation_threshold;
|
||||
api::orderman::DeleteAndCoverDefinition _definition;
|
||||
|
||||
static inline uint32_t _get_absolute(spark::QuantityMeasure qty_measure, uint32_t fallback_value = 0) {
|
||||
return qty_measure.quantity_case() == spark::QuantityMeasure::kAbsolute ? qty_measure.absolute() : fallback_value;
|
||||
}
|
||||
};
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& ostr, const LimitCoverGate& g) {
|
||||
ostr << "Activation Thresh: " << g.activation_threshold() << ", Definition: " << utils::pb::to_string(g.custom_def());
|
||||
return ostr;
|
||||
}
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -71,9 +71,11 @@ struct AlgoTestDriverBase::Impl : public RecursionCounter {
|
||||
bool sub_position = false;
|
||||
|
||||
// Callbacks
|
||||
std::vector<std::function<void(OrderKey)>> on_child_launching;
|
||||
std::vector<std::function<void(OrderKey, Quantity, Price)>> on_child_launching;
|
||||
std::vector<std::function<void(const ChildOrder&)>> on_child_changing;
|
||||
std::vector<std::function<void(const ChildOrder&, const ChildOrder&)>> on_child_changed;
|
||||
// runs after the child change has been forwarded to the underlying algo.
|
||||
std::vector<std::function<void(const ChildOrder&, const ChildOrder&)>> after_child_changed;
|
||||
bool allow_overfill = false;
|
||||
|
||||
Impl(IAlgoOrder& o) : algo(o) {}
|
||||
@@ -206,6 +208,20 @@ const std::vector<ChildOrder>& AlgoTestDriverBase::child_orders() const {
|
||||
return me->orders;
|
||||
}
|
||||
|
||||
// This method asserts that the child exists. If the need arises for an
|
||||
// optional return, leave this method as is, and add a 'try_find_child' method.
|
||||
// These are tests, so it's reasonable to assume the caller is certain about
|
||||
// the existence of the child it seeks.
|
||||
const ChildOrder& AlgoTestDriverBase::find_child(OrderKey key) const {
|
||||
for (auto& c : me->orders) {
|
||||
if (c.key == key)
|
||||
return c;
|
||||
}
|
||||
REQUIRE(false);
|
||||
bts_assert(false);
|
||||
return me->orders.front();
|
||||
}
|
||||
|
||||
const std::vector<ChildOrder>& AlgoTestDriverBase::detached_orders() const {
|
||||
return me->detached_orders;
|
||||
}
|
||||
@@ -237,6 +253,7 @@ void AlgoTestDriverBase::fill(OrderKey k, Price price, Quantity qty) {
|
||||
for (const auto& cb : me->on_child_changed) cb(old, child);
|
||||
// No defer because this is called from outside
|
||||
if (child.subscribed) me->algo.on_child_change(k, child.cstate);
|
||||
for (const auto& cb : me->after_child_changed) cb(old, child);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -273,7 +290,7 @@ void AlgoTestDriverBase::fill(Price p, Quantity q) {
|
||||
}
|
||||
}
|
||||
|
||||
void AlgoTestDriverBase::on_child_launching(std::function<void(OrderKey)>&& cb) {
|
||||
void AlgoTestDriverBase::on_child_launching(std::function<void(OrderKey, Quantity, Price)>&& cb) {
|
||||
me->on_child_launching.push_back(cb);
|
||||
}
|
||||
void AlgoTestDriverBase::on_child_changing(std::function<void(const ChildOrder&)>&& cb) {
|
||||
@@ -282,18 +299,21 @@ void AlgoTestDriverBase::on_child_changing(std::function<void(const ChildOrder&)
|
||||
void AlgoTestDriverBase::on_child_changed(std::function<void(const ChildOrder&, const ChildOrder&)>&& cb) {
|
||||
me->on_child_changed.push_back(cb);
|
||||
}
|
||||
|
||||
void AlgoTestDriverBase::after_child_changed(std::function<void(const ChildOrder&, const ChildOrder&)>&& cb) {
|
||||
me->after_child_changed.push_back(cb);
|
||||
}
|
||||
bool AlgoTestDriverBase::allow_overfill() const { return me->allow_overfill; }
|
||||
void AlgoTestDriverBase::allow_overfill(bool value) { me->allow_overfill = value; }
|
||||
|
||||
//
|
||||
// Utility
|
||||
|
||||
ChildOrder mkchild(OrderKey key, Price price, Quantity qty, bool sub) {
|
||||
ChildOrder mkchild(OrderKey key, Price price, Quantity qty, bool sub, ChildStatus status = ChildStatus::WORKING) {
|
||||
ChildOrder result = {};
|
||||
result.key = key;
|
||||
result.cstate.price = price;
|
||||
result.cstate.quantity = qty;
|
||||
result.cstate.status = status;
|
||||
result.subscribed = sub;
|
||||
return result;
|
||||
}
|
||||
@@ -407,7 +427,7 @@ OrderKey AlgoTestDriverBase::launch_child(ChildTemplate&, Price price, Quantity
|
||||
OrderKey key = me->next_order();
|
||||
me->orders.push_back(
|
||||
mkchild(key, price, qty, sub));
|
||||
for(const auto& cb : me->on_child_launching) cb(key);
|
||||
for(const auto& cb : me->on_child_launching) cb(key, qty, price);
|
||||
return key;
|
||||
}
|
||||
OrderKey AlgoTestDriverBase::launch_child(api::orderman::OrderDefinition const & def, Price price, Quantity qty, bool sub) {
|
||||
@@ -415,7 +435,7 @@ OrderKey AlgoTestDriverBase::launch_child(api::orderman::OrderDefinition const &
|
||||
me->orders.push_back(
|
||||
mkchild(key, price, qty, sub));
|
||||
me->orders.back().def = {def};
|
||||
for(const auto& cb : me->on_child_launching) cb(key);
|
||||
for(const auto& cb : me->on_child_launching) cb(key, qty, price);
|
||||
return key;
|
||||
}
|
||||
void AlgoTestDriverBase::update_child(OrderKey key, api::orderman::Update const & update) {
|
||||
|
||||
@@ -61,6 +61,8 @@ class AlgoTestDriverBase : private IAlgoServices {
|
||||
Quantity total_fills() const override;
|
||||
bool is_shutting_down() const override;
|
||||
std::vector<PositionUpdate> const & current_positions() const override;
|
||||
void update_child(OrderKey , api::orderman::Update const &) override;
|
||||
void cancel_child(OrderKey) override;
|
||||
//
|
||||
// Change state
|
||||
|
||||
@@ -79,6 +81,7 @@ class AlgoTestDriverBase : private IAlgoServices {
|
||||
//
|
||||
// Inspection methods
|
||||
const std::vector<ChildOrder>& child_orders() const;
|
||||
const ChildOrder& find_child(OrderKey) const;
|
||||
const std::vector<ChildOrder>& detached_orders() const;
|
||||
AlgoState state() const;
|
||||
|
||||
@@ -87,10 +90,11 @@ class AlgoTestDriverBase : private IAlgoServices {
|
||||
// Fill this quantity, start with the largest child order
|
||||
void fill(Price, Quantity);
|
||||
|
||||
void on_child_launching(std::function<void(OrderKey)>&&);
|
||||
void on_child_launching(std::function<void(OrderKey, Quantity, Price)>&&);
|
||||
void on_child_changing(std::function<void(const ChildOrder&)>&&);
|
||||
void on_child_changed(std::function<void(const ChildOrder&, const ChildOrder&)>&&);
|
||||
|
||||
void after_child_changed(std::function<void(const ChildOrder&, const ChildOrder&)>&&);
|
||||
|
||||
//
|
||||
// Tweak internal REQUIRE
|
||||
bool allow_overfill() const;
|
||||
@@ -130,8 +134,6 @@ private:
|
||||
OrderKey launch_child(ChildTemplate &, Price, Quantity, bool) override;
|
||||
OrderKey launch_child(api::orderman::OrderDefinition const &, Price,
|
||||
Quantity, bool) override;
|
||||
void update_child(OrderKey , api::orderman::Update const &) override;
|
||||
void cancel_child(OrderKey) override;
|
||||
void detach_child(OrderKey) override;
|
||||
void detach_all_children() override;
|
||||
|
||||
@@ -157,4 +159,17 @@ struct AlgoTestDriver : public AlgoTestDriverBase
|
||||
const Algo& algo() const { return _algo; }
|
||||
};
|
||||
|
||||
inline std::ostream& operator <<(std::ostream& ostr, const AlgoState& state) {
|
||||
switch (state) {
|
||||
case AlgoState::CREATED: ostr << "CREATED"; break;
|
||||
case AlgoState::READY: ostr << "READY"; break;
|
||||
case AlgoState::RUNNING: ostr << "RUNNING"; break;
|
||||
case AlgoState::CANCELED: ostr << "CANCELED"; break;
|
||||
case AlgoState::TERMINATED: ostr << "TERMINATED"; break;
|
||||
case AlgoState::COMPLETED: ostr << "COMPLETED"; break;
|
||||
default: ostr << "AlgoState_" << static_cast<int>(state); break;
|
||||
}
|
||||
return ostr;
|
||||
}
|
||||
|
||||
}}}//namespace
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/LimitCoverAlgo.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/AlgoTestDriver.h"
|
||||
#include "catch/catch.hpp"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/orderman/orderman_utils/BookUtils.h"
|
||||
#include "lib/orderman/orderman_utils/Formatter.h"
|
||||
#include "lib/orderman/orderman_utils/PriceComp.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/tests/limit_cover/LimitCoverState.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/limit_cover/LimitCoverTestUtils.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/limit_cover/TestSteps.h"
|
||||
#include "lib/sparkproto/SparkProtos.pb.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include <iostream>
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <array>
|
||||
#include <random>
|
||||
#include <variant>
|
||||
#include <cmath>
|
||||
|
||||
// for some reason, intellisense poops its pants without 'using namespace std.'
|
||||
// std::optional<T>, and std::unordered_map<T>, in particular, break intellisense for the whole document.
|
||||
using namespace std;
|
||||
using namespace bts::api::orderman;
|
||||
using namespace bts::orderman::limit_cover::utils;
|
||||
//#define PRINT_SUMMARIES
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
namespace limit_cover {
|
||||
// Glossary:
|
||||
// * limit-price:
|
||||
// The price at which the main child is deployed (computed as a tick offset from the settlement).
|
||||
// * limit-volume:
|
||||
// The same-side market volume at or better than the limit price.
|
||||
// This is recomputed whenever the book changes (obviously).
|
||||
// * volume-gate:
|
||||
// * volume-gate: a simple struct with 3 fields:
|
||||
// 1) activation_threshold (uint)
|
||||
// 2) cover_threshold (uint) must be <= activation_thresh
|
||||
// 3) delete_threshold (uint) must be <= activation_thresh
|
||||
// * current-volume-gate:
|
||||
// The volume gate with the highest activation threshold that is still
|
||||
// <= the limit-volume. This is recomputed whenever the book changes
|
||||
// (obviously).
|
||||
// * volume-gates:
|
||||
// A series of volume gates, sorted by 'activation_threshold.'
|
||||
// Initially, the current-volume-gate is one with the lowest activation threshold.
|
||||
// The main child is not deployed until the limit-volume reaches that threshold.
|
||||
// After that, the algo continues to observe the limit-volume, removing gates with lower
|
||||
// thresholds when the volume breeches the next gate's thresh.
|
||||
|
||||
// Test Assertions:
|
||||
// * child is not launched until the limit-volume reaches the initial activation threshold.
|
||||
// * limit-volume is always correct (verify whenever the book changes).
|
||||
// * current-volume-gate is always correct (verify whenever the book changes).
|
||||
|
||||
// Test Design:
|
||||
// * expectations of the limit cover algo's state are stored in an instance of 'LimitCoverState.'
|
||||
// The actual LimitCoverAlgo does not use this class; the tests just use it as a means
|
||||
// to validate the actual LimitCoverAlgo's state, and to wrap calls to the LimitCoverAlgo
|
||||
// with validation checks.
|
||||
|
||||
// * each test implementation should be executed 6 times:
|
||||
// 2 x 3 = test both sides, with each of the following termination steps:
|
||||
// 1) Fill to completion
|
||||
// 2) Cancel the algo
|
||||
// 3) Cancel the child
|
||||
|
||||
// * Restrictions on the parameters:
|
||||
// - the spec explicitly called for 3 volume gates, and specified that the 3 thresholds
|
||||
// (activation, cover, and delete) be the same for the first gate
|
||||
// (technically, the spec described 2 gates and an 'entry threshold').
|
||||
// - the algo should immediately cancel itself if these parameters are not the case.
|
||||
// while it would most likely not cause a problem, these assumptions allow the tests to focus
|
||||
// on a narrow range of inputs (and of course, such restrictions naturally decrease the
|
||||
// chance of a bug slipping through).
|
||||
|
||||
LimitCoverState init_state(
|
||||
OrderDefinition& mdef,
|
||||
Side side,
|
||||
uint32_t quantity,
|
||||
double price,
|
||||
double price_offset,
|
||||
std::vector<LimitCoverGate> gates,
|
||||
bool cancel_hedges_on_cancel = true,
|
||||
bool cancel_on_external_hedge_cancel = false) {
|
||||
//
|
||||
REQUIRE(!gates.empty());
|
||||
for (auto& g : gates) {
|
||||
g.mdefinition().set_cancel_hedges_on_cancel(cancel_hedges_on_cancel);
|
||||
g.mdefinition().set_cancel_on_external_hedge_cancel(cancel_on_external_hedge_cancel);
|
||||
}
|
||||
LimitCoverDefinition lc_def;
|
||||
lc_def.price_offset(price_offset);
|
||||
uint32_t gate_pos = 0;
|
||||
for (auto g : gates) {
|
||||
lc_def.gate(gate_pos++, g);
|
||||
}
|
||||
DynamicOrderDefinition dyn_def;
|
||||
LimitCoverAdapter::init_proto(lc_def, dyn_def);
|
||||
*(mdef.mutable_custom_def()->mutable_dynamic()) = dyn_def;
|
||||
mdef.set_side(side);
|
||||
return LimitCoverState(mdef, quantity, price);
|
||||
}
|
||||
|
||||
template<typename... Steps>
|
||||
inline void test_impl(Side side, uint32_t qty, double price_offset, double settle, std::vector<LimitCoverGate> gates, Steps... steps) {
|
||||
std::cout << std::endl << std::endl;
|
||||
std::cout << "*************************************************************************************************************************" << std::endl;
|
||||
std::cout << "=========================================================================================================================" << std::endl;
|
||||
int step_count = 1;
|
||||
std::cout << "Test " << "[" << Formatter::to_string(side) << " Order]" << std::endl << "\t1. "
|
||||
<< Formatter::join([&]() {
|
||||
return Formatter::fmt("\n\t", ++step_count, ". ");
|
||||
}, steps...)
|
||||
<< std::endl;
|
||||
std::cout << "=========================================================================================================================" << std::endl;
|
||||
std::cout << "*************************************************************************************************************************" << std::endl;
|
||||
std::cout << std::endl;
|
||||
OrderDefinition def;
|
||||
// price is irrelevant
|
||||
auto state = init_state(def, side, qty, -99999, price_offset, gates);
|
||||
state.settlement(settle);
|
||||
(steps.execute(state), ...);
|
||||
}
|
||||
|
||||
template<typename... Steps>
|
||||
inline void test_permutations(
|
||||
uint32_t qty,
|
||||
double settle,
|
||||
std::vector<LimitCoverGate> gates,
|
||||
Steps... steps) {
|
||||
std::vector<double> price_offsets { 0.0, 0.75, 1.25, -0.75, -1.25 };
|
||||
for (auto po : price_offsets) {
|
||||
// complete
|
||||
test_impl(api::SIDE_BUY, qty, po, settle, gates, steps..., test_steps::Complete());
|
||||
test_impl(api::SIDE_SELL, qty, po, settle, gates, steps..., test_steps::Complete());
|
||||
// cancel child
|
||||
test_impl(api::SIDE_BUY, qty, po, settle, gates, steps..., test_steps::CancelChild());
|
||||
test_impl(api::SIDE_SELL, qty, po, settle, gates, steps..., test_steps::CancelChild());
|
||||
// cancel algo
|
||||
test_impl(api::SIDE_BUY, qty, po, settle, gates, steps..., test_steps::CancelAlgo());
|
||||
test_impl(api::SIDE_SELL, qty, po, settle, gates, steps..., test_steps::CancelAlgo());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("test_activation_thresholds.", "[algo][limit_cover]") {
|
||||
SECTION("Activate 1st gate.") {
|
||||
test_permutations(25, 100, {
|
||||
LimitCoverGate(4, 4, 4),
|
||||
LimitCoverGate(10, 7, 5),
|
||||
LimitCoverGate(15, 12, 11),
|
||||
},
|
||||
// test steps
|
||||
test_steps::BeginLaunch(),
|
||||
test_steps::Launch(),
|
||||
test_steps::BreechActivationThresh());
|
||||
}
|
||||
SECTION("Activate 1st then 2nd gate.") {
|
||||
test_permutations(25, 100, {
|
||||
LimitCoverGate(4, 4, 4),
|
||||
LimitCoverGate(10, 7, 5),
|
||||
LimitCoverGate(15, 12, 11),
|
||||
},
|
||||
// test steps
|
||||
test_steps::BeginLaunch(),
|
||||
test_steps::Launch(),
|
||||
test_steps::BreechActivationThresh(),
|
||||
test_steps::Fill([](const auto& state) { return test_params::fills::child(state, 8); }),
|
||||
test_steps::BreechActivationThresh(),
|
||||
test_steps::Fill([](const auto& state) { return test_params::fills::child(state, 2); }),
|
||||
test_steps::BreechActivationThresh());
|
||||
}
|
||||
SECTION("Activate 2nd gate.") {
|
||||
test_permutations(25, 100, {
|
||||
LimitCoverGate(4, 4, 4),
|
||||
LimitCoverGate(10, 7, 5),
|
||||
LimitCoverGate(15, 12, 11),
|
||||
},
|
||||
// test steps
|
||||
test_steps::BeginLaunch(),
|
||||
test_steps::Launch(),
|
||||
test_steps::BreechActivationThresh(1, 1),
|
||||
test_steps::Fill([](const auto& state) { return test_params::fills::child(state, 8); }));
|
||||
}
|
||||
}
|
||||
} // namespace limit_cover
|
||||
} // namespace test
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,199 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/limit_cover/LimitCoverState.h"
|
||||
#include "catch/catch.hpp"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/md/IntPrice.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orderman_utils/BookUtils.h"
|
||||
#include "lib/orderman/orderman_utils/Formatter.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverAdapter.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/limit_cover/LimitCoverTestUtils.h"
|
||||
|
||||
#define VALIDATE Validation __VALIDATION(this)
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
namespace limit_cover {
|
||||
using namespace std;
|
||||
using namespace bts::api::orderman;
|
||||
using namespace bts::orderman::limit_cover::utils;
|
||||
using Size = bts::md::Size;
|
||||
using Side = bts::api::Side;
|
||||
|
||||
LimitCoverState::LimitCoverState(
|
||||
const OrderDefinition& def,
|
||||
uint32_t quantity,
|
||||
double price)
|
||||
: driver(def, int_price(price), Quantity(quantity)), _definition(def), _gates(), _book(SecurityKey(security().id())) {
|
||||
auto ngs = limit_cover().normal_gates();
|
||||
_gates.insert(ngs.begin(), ngs.end());
|
||||
// attach callbacks
|
||||
driver.on_child_launching([&](OrderKey key, Quantity child_qty, Price child_price) {
|
||||
// should never deploy twice
|
||||
REQUIRE(_child == null_order_key());
|
||||
REQUIRE(key != null_order_key());
|
||||
_child = key;
|
||||
// qty should match the algo's, but price must be the limit price
|
||||
// (the algo's price is irrelevant)
|
||||
REQUIRE(child_qty.value() == driver.quantity().value());
|
||||
REQUIRE(child_price.value() == limit_price());
|
||||
// todo: verify delete and cover def matches the current gate
|
||||
//REQUIRE(child_def.custom_def().delete_and_cover())
|
||||
REQUIRE(limit_volume() >= activation_threshold());
|
||||
});
|
||||
driver.on_child_changed([&](const ChildOrder&, const ChildOrder& new_state) {
|
||||
REQUIRE(new_state.key == _child);
|
||||
REQUIRE(new_state.def.has_value());
|
||||
REQUIRE(new_state.def.value().has_custom_def());
|
||||
REQUIRE(new_state.def.value().custom_def().has_delete_and_cover());
|
||||
});
|
||||
driver.after_child_changed([&](const ChildOrder&, const ChildOrder& new_state) {
|
||||
REQUIRE(driver.total_fills().value() == new_state.cstate.total_fills.value());
|
||||
});
|
||||
}
|
||||
|
||||
uint32_t LimitCoverState::quantity() const {
|
||||
return driver.quantity().value();
|
||||
}
|
||||
double LimitCoverState::price() const {
|
||||
return driver.price().value();
|
||||
}
|
||||
const api::Security& LimitCoverState::security() const {
|
||||
return driver.security();
|
||||
}
|
||||
const bts::api::orderman::OrderDefinition& LimitCoverState::def() const {
|
||||
return _definition;
|
||||
}
|
||||
LimitCoverDefinition LimitCoverState::limit_cover() const {
|
||||
return LimitCoverAdapter::from_proto(def());
|
||||
}
|
||||
uint32_t LimitCoverState::limit_volume() const {
|
||||
return _limit_volume;
|
||||
}
|
||||
double LimitCoverState::limit_price() const {
|
||||
return _limit_price;
|
||||
}
|
||||
OrderKey LimitCoverState::child() const {
|
||||
return _child;
|
||||
}
|
||||
optional<ChildOrder> LimitCoverState::child_state() const {
|
||||
if (has_been_deployed()) {
|
||||
return ChildOrder(*driver.child_orders().begin());
|
||||
}
|
||||
return {};
|
||||
}
|
||||
void LimitCoverState::update_child(const Update& u) {
|
||||
driver.update_child(child(), u);
|
||||
}
|
||||
bool LimitCoverState::has_been_deployed() const {
|
||||
return _child != null_order_key();
|
||||
}
|
||||
bool LimitCoverState::is_terminated() const {
|
||||
switch (driver.state()) {
|
||||
case AlgoState::CANCELED:
|
||||
case AlgoState::TERMINATED:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const std::map<uint32_t, LimitCoverGate>& LimitCoverState::gates() const {
|
||||
return _gates;
|
||||
}
|
||||
const LimitCoverGate& LimitCoverState::curr_gate() const {
|
||||
REQUIRE(!gates().empty());
|
||||
return gates().begin()->second;
|
||||
}
|
||||
uint32_t LimitCoverState::activation_threshold() const {
|
||||
return gates().empty() ? std::numeric_limits<uint32_t>::max() : gates().begin()->first;
|
||||
}
|
||||
// settlement
|
||||
double LimitCoverState::settlement() const { return _settlement; }
|
||||
void LimitCoverState::settlement(double value) {
|
||||
VALIDATE;
|
||||
// the algo is not supposed to disregard settlement changes
|
||||
// after the child has been deployed
|
||||
if (!has_been_deployed()) {
|
||||
_settlement = value;
|
||||
recompute_all();
|
||||
}
|
||||
driver.update_settlement(SecurityKey(security().id()), Price(value));
|
||||
}
|
||||
|
||||
// book
|
||||
const Book& LimitCoverState::book() const { return _book; }
|
||||
void LimitCoverState::book(Book value) {
|
||||
VALIDATE;
|
||||
_book = value;
|
||||
recompute_all();
|
||||
driver.update_book(value);
|
||||
}
|
||||
|
||||
OrderOpResult LimitCoverState::before_launch() {
|
||||
_gates.clear();
|
||||
auto ngs = limit_cover().normal_gates();
|
||||
_gates.insert(ngs.begin(), ngs.end());
|
||||
return driver.before_launch();
|
||||
}
|
||||
void LimitCoverState::launch() {
|
||||
VALIDATE;
|
||||
_is_initialized = true;
|
||||
recompute_all();
|
||||
driver.launch();
|
||||
}
|
||||
void LimitCoverState::do_update(const Update& upd) {
|
||||
REQUIRE(!is_terminated());
|
||||
// updates should not affect the child's price (after it has been launched).
|
||||
// this is bc the literal 'price' parameter is irrelevant to this algo,
|
||||
// for all intents and purposes, its price is the tick offset from settlement.
|
||||
auto upd_valid = validate_update(upd);
|
||||
// keep definition in sync with update
|
||||
if (upd.has_custom() && upd_valid.ok()) {
|
||||
*(_definition.mutable_custom_def()) = upd.custom();
|
||||
}
|
||||
driver.do_update(upd);
|
||||
if (upd_valid.ok()) {
|
||||
recompute_all();
|
||||
} else {
|
||||
REQUIRE(driver.state() == AlgoState::TERMINATED);
|
||||
}
|
||||
}
|
||||
|
||||
void LimitCoverState::recompute_limit_price() {
|
||||
_limit_price = os::is_nan(settlement()) ? os::nan() :
|
||||
securities::improve_price(side(), Price(settlement()), Price(limit_cover().price_offset()))
|
||||
.value();
|
||||
}
|
||||
|
||||
void LimitCoverState::recompute_limit_volume() {
|
||||
_limit_volume = os::is_nan(settlement()) ? os::nan() :
|
||||
BookUtils::volume_at_or_better(_book, side(), limit_price());
|
||||
}
|
||||
|
||||
void LimitCoverState::recompute_all() {
|
||||
recompute_limit_price();
|
||||
recompute_limit_volume();
|
||||
if (_is_initialized)
|
||||
recompute_gate();
|
||||
}
|
||||
|
||||
void LimitCoverState::recompute_gate() {
|
||||
if (gates().empty())
|
||||
return;
|
||||
auto it = _gates.lower_bound(limit_volume());
|
||||
if (it->first > limit_volume() && it != _gates.begin())
|
||||
it--;
|
||||
_gates.erase(_gates.begin(), it);
|
||||
}
|
||||
|
||||
LimitCoverState::ValidateDeployment::ValidateDeployment(LimitCoverState* state) :
|
||||
_state(state), _had_been_deployed(state->has_been_deployed()) { }
|
||||
|
||||
LimitCoverState::ValidateDeployment::~ValidateDeployment() {
|
||||
if (!_had_been_deployed && _state->limit_volume() >= _state->activation_threshold())
|
||||
REQUIRE(_state->has_been_deployed());
|
||||
}
|
||||
} // namespace limit_cover
|
||||
} // namespace test
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,121 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/LimitCoverAlgo.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/limit_cover/LimitCoverDefinition.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/AlgoTestDriver.h"
|
||||
#include "lib/sparkproto/SparkProtos.pb.h"
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
namespace limit_cover {
|
||||
struct LimitCoverState {
|
||||
AlgoTestDriver<LimitCoverAlgo> driver;
|
||||
|
||||
LimitCoverState(
|
||||
const api::orderman::OrderDefinition& def,
|
||||
uint32_t quantity,
|
||||
double price);
|
||||
|
||||
const api::orderman::OrderDefinition& def() const;
|
||||
uint32_t quantity() const;
|
||||
double price() const;
|
||||
const api::Security& security() const;
|
||||
LimitCoverDefinition limit_cover() const;
|
||||
api::Side side() const { return def().side(); }
|
||||
uint32_t activation_threshold() const;
|
||||
// main child
|
||||
OrderKey child() const;
|
||||
std::optional<ChildOrder> child_state() const;
|
||||
// gates/curr gate
|
||||
const std::map<uint32_t, LimitCoverGate>& gates() const;
|
||||
const LimitCoverGate& curr_gate() const;
|
||||
// book
|
||||
const Book& book() const;
|
||||
void book(Book new_book);
|
||||
//
|
||||
bool has_been_deployed() const;
|
||||
// settle
|
||||
double settlement() const;
|
||||
void settlement(double value);
|
||||
// limit price
|
||||
double limit_price() const;
|
||||
// limit volume
|
||||
uint32_t limit_volume() const;
|
||||
bool is_terminated() const;
|
||||
OrderOpResult before_launch();
|
||||
void launch();
|
||||
void do_update(const api::orderman::Update&);
|
||||
void update_child(const api::orderman::Update&);
|
||||
|
||||
private:
|
||||
api::orderman::OrderDefinition _definition;
|
||||
uint32_t _limit_volume = 0;
|
||||
OrderKey _child;
|
||||
double _limit_price = os::nan();
|
||||
double _settlement = os::nan();
|
||||
bool _is_initialized = false;
|
||||
std::map<uint32_t, LimitCoverGate> _gates;
|
||||
Book _book;
|
||||
|
||||
void recompute_limit_price();
|
||||
void recompute_limit_volume();
|
||||
void recompute_all();
|
||||
void recompute_gate();
|
||||
|
||||
struct ValidateDeployment {
|
||||
ValidateDeployment(LimitCoverState* state);
|
||||
~ValidateDeployment();
|
||||
|
||||
private:
|
||||
LimitCoverState* _state;
|
||||
bool _had_been_deployed;
|
||||
};
|
||||
|
||||
struct Validation : public ValidateDeployment {
|
||||
Validation(LimitCoverState* state)
|
||||
: ValidateDeployment(state) {}
|
||||
|
||||
~Validation() {}
|
||||
};
|
||||
};
|
||||
|
||||
// custom formatting uses 1 line (unlike pb::to_string).
|
||||
inline std::ostream& operator<<(std::ostream& ostr, const bts::spark::QuantityMeasure& qm) {
|
||||
switch (qm.quantity_case()) {
|
||||
case bts::spark::QuantityMeasure::kAbsolute:
|
||||
ostr << "[absolute] " << qm.absolute();
|
||||
break;
|
||||
case bts::spark::QuantityMeasure::kRatio:
|
||||
ostr << "[ratio] " << qm.ratio();
|
||||
break;
|
||||
default:
|
||||
ostr << "[quantity_case_" << static_cast<int>(qm.quantity_case()) << "]";
|
||||
break;
|
||||
}
|
||||
return ostr;
|
||||
}
|
||||
|
||||
inline std::ostream& operator<<(std::ostream& ostr, const LimitCoverState& lcs) {
|
||||
ostr << "qty: " << lcs.quantity() << std::endl;
|
||||
ostr << "limit price: " << lcs.limit_price() << std::endl;
|
||||
ostr << "limit volume: " << lcs.limit_volume() << std::endl;
|
||||
ostr << "activation/curr gate thresh: " << lcs.activation_threshold() << std::endl;
|
||||
ostr << "settlement: " << lcs.settlement() << std::endl;
|
||||
ostr << "bbo: " << Formatter::to_string(lcs.book()) << std::endl;
|
||||
ostr << "has been deployed: " << lcs.has_been_deployed() << std::endl;
|
||||
ostr << "status: " << lcs.driver.state() << std::endl;
|
||||
ostr << "child: " << lcs.child() << std::endl;
|
||||
ostr << "child qty: " << (lcs.child_state().has_value() ? lcs.child_state().value().cstate.quantity.value() : 0) << std::endl;
|
||||
ostr << "filled qty: " << lcs.driver.total_fills().value() << std::endl;
|
||||
return ostr;
|
||||
}
|
||||
} // namespace limit_cover
|
||||
} // namespace test
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/md/IntPrice.h"
|
||||
#include "lib/orderman/orderman_utils/Formatter.h"
|
||||
#include "lib/sparkproto/SparkProtos.pb.h"
|
||||
#include <iostream>
|
||||
|
||||
#define SUMMARIZE(...) Summary __summary__ (Formatter::fmt(__VA_ARGS__))
|
||||
|
||||
using Side = bts::api::Side;
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
namespace limit_cover {
|
||||
using namespace std;
|
||||
inline bts::md::IntPrice int_price(double value) {
|
||||
return bts::md::IntPrice_from_double<8>(value);
|
||||
}
|
||||
inline bts::md::PriceSize price_size(double price, uint32_t size) {
|
||||
return bts::md::PriceSize(int_price(price), size);
|
||||
}
|
||||
inline bts::spark::QuantityMeasure qty_measure(uint32_t abs) {
|
||||
bts::spark::QuantityMeasure qm;
|
||||
qm.set_absolute(abs);
|
||||
return qm;
|
||||
}
|
||||
|
||||
struct Summary {
|
||||
Summary(std::string header)
|
||||
: _header(header) {
|
||||
std::cout << "Begin " << header << std::endl;
|
||||
}
|
||||
~Summary() {
|
||||
std::cout << "End " << _header << std::endl;
|
||||
}
|
||||
private:
|
||||
std::string _header;
|
||||
};
|
||||
|
||||
} // namespace limit_cover
|
||||
} // namespace test
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,221 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/limit_cover/TestSteps.h"
|
||||
#include "catch/catch.hpp"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/limit_cover/LimitCoverTestUtils.h"
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
namespace limit_cover {
|
||||
|
||||
std::string ITestStep::name() const { return "step with no name"; }
|
||||
void ITestStep::execute(LimitCoverState &state) const {
|
||||
SUMMARIZE(name());
|
||||
assert_pre_conditions(state);
|
||||
_execute(state);
|
||||
assert_post_conditions(state);
|
||||
}
|
||||
void ITestStep::assert_pre_conditions_core(const LimitCoverState &) const {}
|
||||
void ITestStep::execute_core(LimitCoverState &) const { bts_assert(false); }
|
||||
void ITestStep::assert_post_conditions_core(const LimitCoverState &) const {}
|
||||
void ITestStep::assert_pre_conditions(const LimitCoverState &state) const {
|
||||
// SUMMARIZE("Asserting Pre-conditions (", name(), ")...");
|
||||
assert_pre_conditions_core(state);
|
||||
}
|
||||
void ITestStep::assert_post_conditions(const LimitCoverState &state) const {
|
||||
// SUMMARIZE("Asserting Post-conditions (", name(), ")...");
|
||||
assert_post_conditions_core(state);
|
||||
}
|
||||
void ITestStep::_execute(LimitCoverState &state) const {
|
||||
// SUMMARIZE("Executing (", name(), ")...");
|
||||
execute_core(state);
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr, const ITestStep& ts) {
|
||||
ostr << ts.name();
|
||||
return ostr;
|
||||
}
|
||||
|
||||
LimitCoverState& operator<<(LimitCoverState& state, const ITestStep& ts) {
|
||||
SUMMARIZE(ts.name());
|
||||
ts.execute(state);
|
||||
return state;
|
||||
}
|
||||
|
||||
/////////////////////
|
||||
// Test Params
|
||||
/////////////////////
|
||||
namespace test_params {
|
||||
namespace fills {
|
||||
Fill child(const LimitCoverState& state, std::optional<uint32_t> qty, std::optional<double> price) {
|
||||
if (!qty.has_value()) {
|
||||
REQUIRE(state.child_state().value().active_qty() > 0);
|
||||
}
|
||||
if (!price.has_value()) {
|
||||
REQUIRE(state.child_state().has_value());
|
||||
}
|
||||
return test_params::Fill(
|
||||
qty.has_value() ? qty.value() : state.child_state().value().active_qty(),
|
||||
price.has_value() ? price.value() : state.child_state().value().cstate.price.value());
|
||||
}
|
||||
} // namespace fills
|
||||
} // namespace test_params
|
||||
|
||||
/////////////////////
|
||||
// Order Selectors
|
||||
/////////////////////
|
||||
namespace order_selectors {
|
||||
std::vector<OrderKey> child(const LimitCoverState& state) {
|
||||
std::vector<OrderKey> rv;
|
||||
if (state.has_been_deployed()) {
|
||||
rv.push_back(state.child());
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
std::vector<OrderKey> self(const LimitCoverState&) {
|
||||
std::vector<OrderKey> rv;
|
||||
rv.push_back(null_order_key());
|
||||
return rv;
|
||||
}
|
||||
} // namespace order_selectors
|
||||
|
||||
/////////////////////////
|
||||
// Order Ops (step impls)
|
||||
/////////////////////////
|
||||
namespace ops {
|
||||
void begin_launch(LimitCoverState& state) {
|
||||
REQUIRE(state.driver.before_launch().ok());
|
||||
}
|
||||
|
||||
void launch(LimitCoverState& state) {
|
||||
state.driver.launch();
|
||||
}
|
||||
|
||||
void breech_activation_thresh(LimitCoverState& state, uint32_t thresh_count, int offset) {
|
||||
auto book = state.book();
|
||||
REQUIRE(!os::is_nan(state.limit_price()));
|
||||
auto gate_it = state.gates().upper_bound(state.limit_volume());
|
||||
REQUIRE(gate_it != state.gates().end());
|
||||
while (thresh_count-- > 0 && gate_it != state.gates().end())
|
||||
gate_it++;
|
||||
auto thresh = gate_it->first + offset;
|
||||
int gi = 1;
|
||||
auto& bs = book.side(state.side());
|
||||
if (bs[0].size() == 0) {
|
||||
bs[0] = price_size(state.limit_price(), thresh);
|
||||
} else if (BookUtils::price_comp(state.side())(state.limit_price(), bs[0].price())) {
|
||||
// shift existing levels down 1 index,
|
||||
// put the limit volume at the top
|
||||
for (int i = bs.size() - 1; i > 0; i--) {
|
||||
bs[i] = bs[i - 1];
|
||||
}
|
||||
bs[0] = price_size(state.limit_price(), thresh);
|
||||
} else {
|
||||
// add the difference to the qty for the best price
|
||||
bs[0] = price_size(bs[0].price(), bs[0].size() + thresh - state.limit_volume());
|
||||
}
|
||||
state.book(book);
|
||||
}
|
||||
|
||||
void fill_child(LimitCoverState& state, uint32_t qty, double price, bool allow_overfill) {
|
||||
SUMMARIZE("filling child: ", Formatter::fmt_order(state.side(), qty, price));
|
||||
bts_assert(os::is_finite(price));
|
||||
bts_assert(qty > 0);
|
||||
REQUIRE(state.has_been_deployed());
|
||||
REQUIRE(!state.is_terminated());
|
||||
uint32_t orig_filled_qty = state.driver.total_fills().value();
|
||||
if (state.child_state().has_value()) {
|
||||
auto cs = state.child_state().value();
|
||||
REQUIRE(((cs.cstate.total_fills.value() - cs.cstate.fills_at_attach_time.value()) == orig_filled_qty));
|
||||
if (!allow_overfill) {
|
||||
REQUIRE(cs.active_qty() >= qty);
|
||||
}
|
||||
} else {
|
||||
REQUIRE(orig_filled_qty == 0);
|
||||
}
|
||||
state.driver.fill(state.child(), Price(price), Quantity(qty));
|
||||
}
|
||||
|
||||
void fill_child(LimitCoverState& lcs, uint32_t qty, bool allow_overfill) {
|
||||
fill_child(lcs, qty, lcs.limit_price(), allow_overfill);
|
||||
}
|
||||
|
||||
void cancel_child(LimitCoverState& state) {
|
||||
REQUIRE(state.has_been_deployed());
|
||||
REQUIRE(!state.is_terminated());
|
||||
state.driver.cancel_child(state.child());
|
||||
REQUIRE(state.driver.state() == AlgoState::TERMINATED);
|
||||
}
|
||||
|
||||
void complete(LimitCoverState& state) {
|
||||
REQUIRE(state.has_been_deployed());
|
||||
REQUIRE(!state.is_terminated());
|
||||
REQUIRE(state.driver.quantity().value() > state.driver.total_fills().value());
|
||||
fill_child(state, state.driver.quantity().value() - state.driver.total_fills().value());
|
||||
REQUIRE(state.driver.state() == AlgoState::COMPLETED);
|
||||
}
|
||||
} // namespace ops
|
||||
|
||||
/////////////////////
|
||||
// Test Steps
|
||||
/////////////////////
|
||||
namespace test_steps {
|
||||
// Begin Launch
|
||||
std::string BeginLaunch::name() const { return "Begin Launch"; }
|
||||
void BeginLaunch::execute_core(LimitCoverState &state) const { ops::begin_launch(state); }
|
||||
|
||||
// Launch
|
||||
std::string Launch::name() const { return "Launch"; }
|
||||
void Launch::execute_core(LimitCoverState &state) const { ops::launch(state); }
|
||||
|
||||
// Breech Activation Threshold
|
||||
std::string BreechActivationThresh::name() const {
|
||||
std::stringstream title;
|
||||
title << "Breech Activation Threshold " << (thresh_count + 1);
|
||||
if (offset > 0) {
|
||||
title << " (+" << offset << ")";
|
||||
} else if (offset < 0) {
|
||||
title << " (" << offset << ")";
|
||||
}
|
||||
return title.str();
|
||||
}
|
||||
void BreechActivationThresh::execute_core(LimitCoverState &state) const {
|
||||
ops::breech_activation_thresh(state, thresh_count, offset);
|
||||
}
|
||||
|
||||
// Fill
|
||||
std::string Fill::name() const { return "Fill"; }
|
||||
void Fill::execute_core(LimitCoverState &state) const {
|
||||
auto p = get_params(state);
|
||||
ops::fill_child(state, p.quantity, p.price);
|
||||
}
|
||||
|
||||
// Complete
|
||||
std::string Complete::name() const { return "Complete"; }
|
||||
void Complete::execute_core(LimitCoverState &state) const {
|
||||
ops::complete(state);
|
||||
}
|
||||
|
||||
// Cancel Child
|
||||
std::string CancelChild::name() const { return "Cancel Child"; }
|
||||
void CancelChild::execute_core(LimitCoverState& state) const {
|
||||
ops::cancel_child(state);
|
||||
}
|
||||
|
||||
// Cancel Algo
|
||||
std::string CancelAlgo::name() const { return "Cancel Algo"; }
|
||||
void CancelAlgo::execute_core(LimitCoverState& state) const {
|
||||
state.driver.cancel();
|
||||
REQUIRE(state.driver.state() == AlgoState::CANCELED);
|
||||
}
|
||||
} // namespace test_steps
|
||||
} // namespace limit_cover
|
||||
} // namespace test
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,126 @@
|
||||
#pragma once
|
||||
#include "catch/catch.hpp"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/orderman/orders/algo/algo_orders/tests/limit_cover/LimitCoverState.h"
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace test {
|
||||
namespace limit_cover {
|
||||
// In Visual Studio, intellisense and syntax highlighting don't work (at all)
|
||||
// when <optional> is included, unless 'using namespace std;' is specified.
|
||||
using namespace std;
|
||||
|
||||
struct ITestStep {
|
||||
virtual std::string name() const;
|
||||
void execute(LimitCoverState& state) const;
|
||||
protected:
|
||||
virtual void assert_pre_conditions_core(const LimitCoverState&) const;
|
||||
virtual void execute_core(LimitCoverState&) const;
|
||||
virtual void assert_post_conditions_core(const LimitCoverState&) const;
|
||||
private:
|
||||
void assert_pre_conditions(const LimitCoverState& state) const;
|
||||
void assert_post_conditions(const LimitCoverState& state) const;
|
||||
void _execute(LimitCoverState& state) const;
|
||||
};
|
||||
|
||||
std::ostream& operator<<(std::ostream& ostr, const ITestStep& ts);
|
||||
LimitCoverState& operator<<(LimitCoverState& state, const ITestStep& ts);
|
||||
|
||||
namespace order_selectors {
|
||||
std::vector<OrderKey> child(const LimitCoverState &lcs);
|
||||
std::vector<OrderKey> self(const LimitCoverState &);
|
||||
} // namespace order_selectors
|
||||
|
||||
namespace ops {
|
||||
void begin_launch(LimitCoverState& lcs);
|
||||
void launch(LimitCoverState& lcs);
|
||||
void breech_activation_thresh(LimitCoverState& lcs, uint32_t thresh_count = 0, int offset = 0);
|
||||
|
||||
void fill_child(LimitCoverState& lcs, uint32_t qty, double price, bool allow_overfill = false);
|
||||
void fill_child(LimitCoverState& lcs, uint32_t qty, bool allow_overfill = false);
|
||||
|
||||
void cancel_child(LimitCoverState& state);
|
||||
void complete(LimitCoverState& lcs);
|
||||
}
|
||||
|
||||
namespace test_params {
|
||||
struct Fill {
|
||||
uint32_t quantity;
|
||||
double price;
|
||||
|
||||
Fill(uint32_t quantity, double price)
|
||||
: quantity(quantity), price(price) {}
|
||||
};
|
||||
|
||||
namespace fills {
|
||||
Fill child(const LimitCoverState& state, optional<uint32_t> qty = {}, optional<double> price = {});
|
||||
} // namespace fills
|
||||
} // namespace test_params
|
||||
namespace test_steps {
|
||||
struct BeginLaunch : public ITestStep {
|
||||
std::string name() const override;
|
||||
void execute_core(LimitCoverState& state) const override;
|
||||
};
|
||||
|
||||
struct Launch : public ITestStep {
|
||||
std::string name() const override;
|
||||
void execute_core(LimitCoverState& state) const override;
|
||||
};
|
||||
|
||||
struct BreechActivationThresh : public ITestStep {
|
||||
uint32_t thresh_count;
|
||||
int offset;
|
||||
|
||||
BreechActivationThresh(uint32_t thresh_count = 0, int offset = 0)
|
||||
: thresh_count(thresh_count), offset(offset) { }
|
||||
|
||||
std::string name() const override;
|
||||
void execute_core(LimitCoverState& state) const override;
|
||||
};
|
||||
|
||||
struct Fill : public ITestStep {
|
||||
std::function<test_params::Fill(const LimitCoverState&)> get_params;
|
||||
|
||||
Fill(std::function<test_params::Fill(const LimitCoverState&)> get_params)
|
||||
: get_params(get_params) {}
|
||||
|
||||
std::string name() const override;
|
||||
void execute_core(LimitCoverState& state) const override;
|
||||
};
|
||||
|
||||
struct Complete : public ITestStep {
|
||||
std::string name() const override;
|
||||
void execute_core(LimitCoverState& state) const override;
|
||||
};
|
||||
|
||||
struct CancelChild : public ITestStep {
|
||||
std::string name() const override;
|
||||
void execute_core(LimitCoverState& state) const override;
|
||||
};
|
||||
|
||||
struct CancelAlgo : public ITestStep {
|
||||
std::string name() const override;
|
||||
void execute_core(LimitCoverState& state) const override;
|
||||
};
|
||||
|
||||
struct Custom : public ITestStep {
|
||||
Custom(std::string name, std::function<void(LimitCoverState&)> impl)
|
||||
: _name(name), _impl(impl) { }
|
||||
|
||||
std::string name() const override { return _name; }
|
||||
void execute_core(LimitCoverState& state) const override { _impl(state); }
|
||||
private:
|
||||
std::string _name;
|
||||
std::function<void(LimitCoverState&)> _impl;
|
||||
};
|
||||
} // namespace test_steps
|
||||
} // namespace limit_cover
|
||||
} // namespace test
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -577,7 +577,7 @@ TEST_CASE("Increase then decrease", "[algo][splitter]") {
|
||||
|
||||
// Set up some instant fills
|
||||
bool applied_second_update = false;
|
||||
state.on_child_launching([&, update](const auto &) mutable {
|
||||
state.on_child_launching([&, update](const auto &, auto, auto) mutable {
|
||||
if (!applied_second_update) {
|
||||
applied_second_update = true;
|
||||
update.mutable_custom()->mutable_splitter()->set_exposed_qty(350);
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
#pragma once
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/plugin/SparkServices.h"
|
||||
#include "lib/utils/bts_assert.h"
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <cstdarg>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace dynamic_adapter {
|
||||
#define NOT_IMPLEMENTED bts_assert_msg(false, "Not implemented.")
|
||||
#define _CONCAT_(x,y) x##y
|
||||
#define VALUE_CASE(x) bts::api::orderman::DynamicOrderParam::_CONCAT_(kAs,x)
|
||||
|
||||
#define VALIDATE_PROTO(x,y)\
|
||||
template<> inline bool validate_proto<x>(const bts::api::orderman::DynamicOrderParam& p) { return p.value_case() == VALUE_CASE(y); }
|
||||
|
||||
#define _ASSIGN_TO_PROTO_(x,y)\
|
||||
template<> inline void assign_to_proto<x>(bts::api::orderman::DynamicOrderParam& param, x value) { param.y(value); }
|
||||
#define ASSIGN_TO_PROTO(x,y) _ASSIGN_TO_PROTO_(x,_CONCAT_(set_as_,y))
|
||||
|
||||
#define _GET_FROM_PROTO_(x,y)\
|
||||
template<> inline x get_from_proto<x>(const bts::api::orderman::DynamicOrderParam& param) { return param.y(); }
|
||||
#define GET_FROM_PROTO(x,y) _GET_FROM_PROTO_(x,_CONCAT_(as_,y))
|
||||
|
||||
#define _DEFINE_PARAM_(x,y)\
|
||||
template<> inline void define_param<x>(bts::api::orderman::DynamicOrderDescriptor& desc, std::string name) {\
|
||||
*desc.add_param_desc() = y(name);\
|
||||
}
|
||||
#define DEFINE_PARAM(x,y) _DEFINE_PARAM_(x,_CONCAT_(param_,y))
|
||||
|
||||
#define PRIMITIVE_PROTO_IMPL(x,y,z)\
|
||||
VALIDATE_PROTO(x,z)\
|
||||
ASSIGN_TO_PROTO(x,y)\
|
||||
GET_FROM_PROTO(x,y)\
|
||||
DEFINE_PARAM(x,y)
|
||||
|
||||
using CustomOrderDefinition = bts::api::orderman::CustomOrderDefinition;
|
||||
using DynamicOrderParam = bts::api::orderman::DynamicOrderParam;
|
||||
using DynamicOrderEnumerated = bts::api::orderman::DynamicOrderEnumerated;
|
||||
|
||||
template<class T> inline bool validate_proto(const DynamicOrderParam&) { NOT_IMPLEMENTED; return false; }
|
||||
template<class T> inline void assign_to_proto(DynamicOrderParam&, T) { NOT_IMPLEMENTED; }
|
||||
template<class T> inline T get_from_proto(const api::orderman::DynamicOrderParam&) { NOT_IMPLEMENTED; return T(); }
|
||||
template<class T> inline void define_param(bts::api::orderman::DynamicOrderDescriptor&, std::string) { NOT_IMPLEMENTED; }
|
||||
|
||||
// primitives
|
||||
PRIMITIVE_PROTO_IMPL(int, int32, Int32)
|
||||
PRIMITIVE_PROTO_IMPL(uint32_t, uint32, Uint32)
|
||||
PRIMITIVE_PROTO_IMPL(double, double, Double)
|
||||
PRIMITIVE_PROTO_IMPL(bool, bool, Bool)
|
||||
|
||||
// order def
|
||||
VALIDATE_PROTO(bts::api::orderman::CustomOrderDefinition, OrderDef)
|
||||
GET_FROM_PROTO(bts::api::orderman::CustomOrderDefinition, order_def)
|
||||
template<> inline void assign_to_proto<CustomOrderDefinition>(DynamicOrderParam& param, CustomOrderDefinition value) {
|
||||
*(param.mutable_as_order_def()) = value;
|
||||
}
|
||||
template<> inline void define_param<CustomOrderDefinition>(api::orderman::DynamicOrderDescriptor& desc, std::string name) {
|
||||
*desc.add_param_desc() = param_order(name);
|
||||
}
|
||||
|
||||
// enumerated
|
||||
VALIDATE_PROTO(bts::api::orderman::DynamicOrderEnumerated, Enumerated)
|
||||
GET_FROM_PROTO(bts::api::orderman::DynamicOrderEnumerated, enumerated)
|
||||
template<> inline void assign_to_proto<DynamicOrderEnumerated>(DynamicOrderParam& param, DynamicOrderEnumerated value) {
|
||||
*(param.mutable_as_enumerated()) = value;
|
||||
}
|
||||
|
||||
template<class TDef, class TValue>
|
||||
struct ParamKey {
|
||||
ParamKey(std::string name, std::function<TValue(const TDef&)> get, std::function<void(TDef&, TValue)> set)
|
||||
: _name(name), _get(get), _set(set) { }
|
||||
|
||||
const std::string& name() const { return _name; }
|
||||
virtual void assign_to_def(const api::orderman::DynamicOrderParam& param, TDef& def) {
|
||||
if (dynamic_adapter::validate_proto<TValue>(param))
|
||||
_set(def, get_from_proto<TValue>(param));
|
||||
}
|
||||
virtual void assign_to_proto(const TDef& def, api::orderman::DynamicOrderParam& param) {
|
||||
dynamic_adapter::assign_to_proto<TValue>(param, _get(def));
|
||||
}
|
||||
virtual void assign_to_proto(const TDef& def, api::orderman::DynamicOrderDefinition& dyn) {
|
||||
api::orderman::DynamicOrderParam dop;
|
||||
assign_to_proto(def, dop);
|
||||
(*dyn.mutable_order_params())[name()] = dop;
|
||||
}
|
||||
virtual void define_param(api::orderman::DynamicOrderDescriptor& desc) {
|
||||
dynamic_adapter::define_param<TValue>(desc, name());
|
||||
}
|
||||
virtual bool validate_proto(const api::orderman::DynamicOrderDefinition& dyn) {
|
||||
auto it = dyn.order_params().find(name());
|
||||
return it != dyn.order_params().end() && dynamic_adapter::validate_proto<TValue>(it->second);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _name;
|
||||
std::function<TValue(const TDef&)> _get;
|
||||
std::function<void(TDef&, TValue)> _set;
|
||||
};
|
||||
|
||||
template<class TDef, class TValue>
|
||||
struct EnumParamKey : public ParamKey<TDef, TValue> {
|
||||
EnumParamKey(std::string name, std::vector<std::pair<TValue, std::string>> named_values, std::function<TValue(const TDef&)> get, std::function<void(TDef&, TValue)> set)
|
||||
: ParamKey<TDef, TValue>(name, get, set), _name(name), _named_values(named_values), _get(get), _set(set) {
|
||||
}
|
||||
|
||||
void assign_to_def(const api::orderman::DynamicOrderParam& param, TDef& def) override {
|
||||
if (dynamic_adapter::validate_proto<DynamicOrderEnumerated>(param))
|
||||
_set(def, _from_proto(param.as_enumerated()));
|
||||
}
|
||||
|
||||
void assign_to_proto(const TDef& def, api::orderman::DynamicOrderParam& param) override {
|
||||
dynamic_adapter::assign_to_proto(param, _to_proto(_get(def)));
|
||||
}
|
||||
|
||||
void assign_to_proto(const TDef& def, api::orderman::DynamicOrderDefinition& dyn) override {
|
||||
api::orderman::DynamicOrderParam param;
|
||||
assign_to_proto(def, param);
|
||||
(*dyn.mutable_order_params())[_name] = param;
|
||||
}
|
||||
|
||||
void define_param(api::orderman::DynamicOrderDescriptor& order_desc) override {
|
||||
// enum desc
|
||||
api::orderman::DynamicOrderEnumeratedDescriptor enum_desc;
|
||||
for (auto& p : _named_values) {
|
||||
enum_desc.add_element(p.second);
|
||||
}
|
||||
// param type desc
|
||||
api::orderman::DynamicOrderTypeDescriptor param_type_desc;
|
||||
*param_type_desc.mutable_enumerated() = enum_desc;
|
||||
// param desc
|
||||
api::orderman::DynamicOrderParamDescriptor param_desc;
|
||||
param_desc.set_param_name(_name);
|
||||
*param_desc.mutable_param_type() = param_type_desc;
|
||||
// order desc
|
||||
*order_desc.add_param_desc() = param_desc;
|
||||
}
|
||||
|
||||
bool validate_proto(const api::orderman::DynamicOrderDefinition& dyn) override {
|
||||
auto it = dyn.order_params().find(_name);
|
||||
return it != dyn.order_params().end() && it->second.value_case() == VALUE_CASE(Enumerated);
|
||||
}
|
||||
|
||||
private:
|
||||
std::string _name;
|
||||
std::vector<std::pair<TValue, std::string>> _named_values;
|
||||
|
||||
DynamicOrderEnumerated _to_proto(TValue v) {
|
||||
DynamicOrderEnumerated deo;
|
||||
for (auto p : _named_values) {
|
||||
if (p.first == v) {
|
||||
deo.set_enum_value(p.second);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return deo;
|
||||
}
|
||||
|
||||
TValue _from_proto(const std::string& enum_name) {
|
||||
for (auto p : _named_values) {
|
||||
if (p.second == enum_name)
|
||||
return p.first;
|
||||
}
|
||||
bts_assert_msg(false, "Failed to match enum name to enum value.");
|
||||
return TValue();
|
||||
}
|
||||
TValue _from_proto(DynamicOrderEnumerated deo) {
|
||||
return _from_proto(deo.enum_value());
|
||||
}
|
||||
|
||||
std::function<TValue(const TDef&)> _get;
|
||||
std::function<void(TDef&, TValue)> _set;
|
||||
};
|
||||
|
||||
template<class TParam, class... TNextParam>
|
||||
inline void define_proto(api::orderman::DynamicOrderDescriptor& desc, TParam param, TNextParam... next_param) {
|
||||
param.define_param(desc);
|
||||
(define_proto(desc, next_param), ...);
|
||||
}
|
||||
|
||||
template<class TDef, class TParam, class... TNextParam>
|
||||
inline void from_proto(const google::protobuf::Map<std::string, api::orderman::DynamicOrderParam>& map, TDef& def, TParam param, TNextParam... next_param) {
|
||||
auto pit = map.find(param.name());
|
||||
if (pit != map.end()) {
|
||||
param.assign_to_def(pit->second, def);
|
||||
}
|
||||
(from_proto(map, def, next_param), ...);
|
||||
}
|
||||
|
||||
template<class TDef, class TParam, class... TNextParam>
|
||||
inline void to_proto(api::orderman::DynamicOrderDefinition& dyn, const TDef& def, TParam param_key, TNextParam... next_param) {
|
||||
param_key.assign_to_proto(def, dyn);
|
||||
(to_proto(dyn, def, next_param), ...);
|
||||
}
|
||||
|
||||
template<class TParam, class... TNextParam>
|
||||
inline bool validate_proto(const api::orderman::DynamicOrderDefinition& dyn, TParam param, TNextParam... next_param) {
|
||||
return param.validate_proto(dyn) && validate_proto(dyn, next_param...);
|
||||
}
|
||||
|
||||
template<typename TDef, typename... T>
|
||||
struct DynamicOrderAdapter {
|
||||
void define_proto(api::orderman::DynamicOrderDescriptor& desc) { (dynamic_adapter::define_proto(desc, T()), ...); }
|
||||
void from_proto(const api::orderman::DynamicOrderDefinition& dyn, TDef& def) { (dynamic_adapter::from_proto(dyn.order_params(), def, T()), ...); }
|
||||
void to_proto(api::orderman::DynamicOrderDefinition& dyn, const TDef& def) { (dynamic_adapter::to_proto(dyn, def, T()), ...); }
|
||||
bool validate_proto(const api::orderman::DynamicOrderDefinition& dyn) { return (T().validate_proto(dyn), ...); }
|
||||
};
|
||||
} // namespace dynamic_adapter
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import sys
|
||||
f = open(sys.argv[1], 'rb+')
|
||||
license = '5E50A1DACD94D7C1BECD316EEAE5AB02'
|
||||
license = '1C9210495AA3C9C884B7A37A4C7AD8F9'
|
||||
data = f.read()
|
||||
match = data.find(license)
|
||||
if match > 0:
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<CSharpAPIRoot>$(MSBuildThisFileDirectory)</CSharpAPIRoot>
|
||||
<Configuration Condition="'$(configuration)'==''">Debug</Configuration>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Label="Output">
|
||||
<BaseOutputPath>$(CSharpAPIRoot)\build\$(Configuration)</BaseOutputPath>
|
||||
<OutputPath>$(BaseOutputPath)\bin</OutputPath>
|
||||
<BaseIntermediateOutputPath>$(BaseOutputPath)\obj\$(MSBuildProjectName)</BaseIntermediateOutputPath>
|
||||
<IntermediateOutputPath>$(BaseOutputPath)\obj\$(MSBuildProjectName)</IntermediateOutputPath>
|
||||
<GenDir>$(BaseOutputPath)\gen\$(MSBuildProjectName)</GenDir>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 16
|
||||
VisualStudioVersion = 16.0.30104.148
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "bts.api.csharp", "src/bts.api.csharp.csproj", "{2B9FECC6-16F4-4B85-92BF-62AAA2B7652D}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{2B9FECC6-16F4-4B85-92BF-62AAA2B7652D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{2B9FECC6-16F4-4B85-92BF-62AAA2B7652D}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{2B9FECC6-16F4-4B85-92BF-62AAA2B7652D}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{2B9FECC6-16F4-4B85-92BF-62AAA2B7652D}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {FC6CE0CA-B0CB-44E3-8176-1D759D05AAD4}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>netcoreapp2.1;netcoreapp3.1</TargetFrameworks>
|
||||
<OutputType>Exe</OutputType>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="CommandLineParser" Version="2.8.0" />
|
||||
<PackageReference Include="spark_sdk" Version="2.15.10" />
|
||||
<PackageReference Include="System.Reactive" Version="4.4.1" />
|
||||
<PackageReference Include="System.Reactive.Core" Version="4.4.1" />
|
||||
<PackageReference Include="System.Reactive.Linq" Version="4.4.1" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,23 @@
|
||||
using System;
|
||||
|
||||
namespace bts.api.csharp {
|
||||
readonly struct NameIDPair {
|
||||
public NameIDPair(string name, uint id) {
|
||||
this.name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public string name { get; }
|
||||
public uint id { get; }
|
||||
}
|
||||
|
||||
static class Users {
|
||||
public static NameIDPair Default =>
|
||||
new NameIDPair("bts", 1);
|
||||
}
|
||||
|
||||
static class Accounts {
|
||||
public static NameIDPair Default =>
|
||||
new NameIDPair("TEST", 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using bts.api.core;
|
||||
using bts.api.csharp.examples;
|
||||
using bts.api.orderman;
|
||||
using bts.api.position;
|
||||
using CommandLine;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
|
||||
namespace bts.api.csharp {
|
||||
class Program {
|
||||
static void Main(string[] args) =>
|
||||
Parser.Default.ParseArguments<Options>(args).WithParsed(_run);
|
||||
|
||||
static void _run(Options options) {
|
||||
var exit_event = new ManualResetEvent(false);
|
||||
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
|
||||
var service_context = new ServiceContext();
|
||||
var login = service_context.login("bts", "");
|
||||
login.Wait();
|
||||
var examples = new List<IDisposable>();
|
||||
if (options.subscribe_securities ||! options.any_enabled) {
|
||||
examples.Add(new SecurityExample(service_context.locate<MarketDataService>()));
|
||||
}
|
||||
if (options.subscribe_positions || !options.any_enabled) {
|
||||
examples.Add(new PositionExample(service_context.locate<PositionService>()));
|
||||
}
|
||||
if (options.subscribe_orders || !options.any_enabled) {
|
||||
examples.Add(new OrderExample(service_context.locate<OrderService>()));
|
||||
}
|
||||
if (options.subscribe_book || !options.any_enabled) {
|
||||
examples.Add(new BookExample(service_context.locate<MarketDataService>()));
|
||||
}
|
||||
if (options.send_orders || !options.any_enabled) {
|
||||
examples.Add(SendOrderLoop.init_loop(service_context.locate<OrderService>(), service_context.locate<MarketDataService>()));
|
||||
}
|
||||
Console.CancelKeyPress += on_cancel_key_press;
|
||||
exit_event.WaitOne();
|
||||
foreach (var ex in examples)
|
||||
ex.Dispose();
|
||||
//
|
||||
void on_cancel_key_press(object sender, ConsoleCancelEventArgs e) {
|
||||
exit_event.Set();
|
||||
e.Cancel = true;
|
||||
}
|
||||
}
|
||||
|
||||
class Options {
|
||||
[Option("subscribe-securities", Required = false, HelpText = "run security-subscription example.")]
|
||||
public bool subscribe_securities { get; set; }
|
||||
|
||||
[Option("subscribe-orders", Required = false, HelpText = "run order-subscription example.")]
|
||||
public bool subscribe_orders { get; set; }
|
||||
|
||||
[Option("subscribe-positions", Required = false, HelpText = "run position-subscription example.")]
|
||||
public bool subscribe_positions { get; set; }
|
||||
|
||||
[Option("subscribe-books", Required = false, HelpText = "run book-subscription example.")]
|
||||
public bool subscribe_book { get; set; }
|
||||
|
||||
[Option("send-orders", Required = false, HelpText = "run send-orders example.")]
|
||||
public bool send_orders { get; set; }
|
||||
|
||||
// if none are enabled, all options will be flipped on,
|
||||
// so the user need not specify a single argument to see the examples run.
|
||||
public bool any_enabled =>
|
||||
subscribe_securities ||
|
||||
subscribe_orders ||
|
||||
subscribe_positions ||
|
||||
subscribe_book ||
|
||||
send_orders;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using bts.api.core;
|
||||
using bts.api.csharp.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using SecurityKey = System.UInt32;
|
||||
|
||||
namespace bts.api.csharp.examples {
|
||||
/// <summary>
|
||||
/// Uses the <see cref="MarketDataService"/> api to observe book updates
|
||||
/// for security in the system, and organize them into a Dictionary.
|
||||
/// </summary>
|
||||
class BookExample : IDisposable {
|
||||
readonly Dictionary<SecurityKey, BookEntryImmutable> _books = new Dictionary<SecurityKey, BookEntryImmutable>();
|
||||
readonly IDisposable _subscription;
|
||||
|
||||
public BookExample(MarketDataService market_data_service) {
|
||||
_subscription = market_data_service.subscribei_book()
|
||||
.ObserveOn(SynchronizationContext.Current)
|
||||
.Subscribe(_on_book_update);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_subscription.Dispose();
|
||||
}
|
||||
|
||||
void _on_book_update(Update<BookEntryImmutable, uint> update) {
|
||||
foreach (var new_book in update.added) {
|
||||
// of course, we shouldn't have entries for any books in update.added yet
|
||||
Debug.Assert(!_books.ContainsKey(new_book.Key));
|
||||
// display bbo in the console window
|
||||
_print_book_info(new_book.Key, $"book received for the first time, bbo is: {BookUtils.fmt_bbo(new_book)}");
|
||||
// add the new book to our map
|
||||
_books.Add(new_book.Key, new_book);
|
||||
}
|
||||
foreach (var change in update.changes) {
|
||||
// of course, the security key shouldn't change
|
||||
Debug.Assert(change.Old.Key == change.New.Key);
|
||||
// our book entry for this security should be identitical to change.Old
|
||||
Debug.Assert(change.Old.Bid.SequenceEqual(_books[change.Old.Key].Bid));
|
||||
Debug.Assert(change.Old.Ask.SequenceEqual(_books[change.Old.Key].Ask));
|
||||
// display the current book in the console window
|
||||
_print_book_info(change.New.Key, $"book changed, now bbo is: {BookUtils.fmt_bbo(change.New)}");
|
||||
// update our map with the changed book
|
||||
_books[change.New.Key] = change.New;
|
||||
}
|
||||
foreach (var old_book in update.removed) {
|
||||
// of course, we should have entries for all books in update.removed
|
||||
Debug.Assert(_books.Remove(old_book.Key));
|
||||
}
|
||||
}
|
||||
|
||||
static void _print_book_info(SecurityKey key, string message) {
|
||||
Console.WriteLine($"Security {key}: {message}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using AccountKey = System.UInt32;
|
||||
using OrderKey = System.UInt32;
|
||||
|
||||
namespace bts.api.csharp.examples {
|
||||
/// <summary>
|
||||
/// Uses the <see cref="OrderService"/> to observe order updates,
|
||||
/// and group them by account.
|
||||
/// </summary>
|
||||
class OrderExample : IDisposable {
|
||||
readonly Dictionary<AccountKey, Dictionary<OrderKey, OrderRecordImmutable>> _data = new Dictionary<AccountKey, Dictionary<AccountKey, OrderRecordImmutable>>();
|
||||
IDisposable _subscription;
|
||||
|
||||
public OrderExample(OrderService order_service) {
|
||||
/// 'OrderService' has 2 methods which can be used to subscribe to order updates.
|
||||
/// -------------------------------------------------------------------------------------
|
||||
/// 1) <see cref="OrderService.subscribe_orders(string)"/>.
|
||||
/// 2) <see cref="OrderService.subscribei_orders(string)"/>.
|
||||
/// This method (no. 2) is typically preferrable because the data it provides is immutable.
|
||||
/// --------------------------------------------------------------------------------------
|
||||
_subscription = order_service.subscribei_orders()
|
||||
// the 'ObserveOn' call below ensures that the
|
||||
// callback will be invoked this thread.
|
||||
// Be sure to do this if you don't want to worry
|
||||
// about multi-threading issues (locking, etc).
|
||||
.ObserveOn(SynchronizationContext.Current)
|
||||
.Subscribe(_on_update);
|
||||
|
||||
}
|
||||
|
||||
public event Action<AccountKey> account_orders_changed;
|
||||
|
||||
public void Dispose() {
|
||||
_subscription.Dispose();
|
||||
}
|
||||
|
||||
public IEnumerable<OrderRecordImmutable> get_account_orders(AccountKey account) {
|
||||
if (_data.TryGetValue(account, out var account_orders)) {
|
||||
return account_orders.Values;
|
||||
}
|
||||
return Enumerable.Empty<OrderRecordImmutable>();
|
||||
}
|
||||
|
||||
void _on_order_added(OrderRecordImmutable new_order) {
|
||||
if (!_data.TryGetValue(new_order.Def.AccountId, out var account_orders)) {
|
||||
// this is the first order for this account.
|
||||
// initialize the account orders dictionary, and add it to our data.
|
||||
account_orders = new Dictionary<AccountKey, OrderRecordImmutable>();
|
||||
_data.Add(new_order.Def.AccountId, account_orders);
|
||||
}
|
||||
// now add the order to account_orders
|
||||
account_orders.Add(new_order.Key, new_order);
|
||||
}
|
||||
|
||||
void _on_order_changed(Change<OrderRecordImmutable> change) {
|
||||
// ch.New and ch.Old have the same key.
|
||||
Debug.Assert(change.New.Key == change.Old.Key);
|
||||
var key = change.New.Key;
|
||||
// react to a changed order
|
||||
if (change.New.State.FilledQuantity != change.Old.State.FilledQuantity) {
|
||||
// Display new fills in the console window.
|
||||
var num_new_fills = change.New.State.FilledQuantity - change.Old.State.FilledQuantity;
|
||||
_print_order_info(key, $"received {num_new_fills} new fills.");
|
||||
// Be mindful that orders can be overfilled.
|
||||
if (change.New.State.FilledQuantity > change.New.State.Quantity) {
|
||||
// Display overfills in the console window.
|
||||
var num_over_fills = change.New.State.FilledQuantity - change.New.State.Quantity;
|
||||
_print_order_info(key, $"overfilled by {num_over_fills}");
|
||||
} else {
|
||||
// Display working qty in the console window.
|
||||
var working_qty = change.New.State.Quantity - change.New.State.FilledQuantity;
|
||||
_print_order_info(key, $"still working {working_qty}.");
|
||||
}
|
||||
}
|
||||
// Display order status changes in the console window.
|
||||
if (change.New.State.Status != change.Old.State.Status) {
|
||||
_print_order_info(key, $"status changed from {change.Old.State.Status} to {change.New.State.Status}.");
|
||||
}
|
||||
// Account will not change, so our dictionary will definitely have a key for this account.
|
||||
Debug.Assert(_data.ContainsKey(change.New.Def.AccountId));
|
||||
var account_orders = _data[change.New.Def.AccountId];
|
||||
// The value for this key in account_orders should equal ch.Old.
|
||||
Debug.Assert(account_orders[key].Equals(change.Old));
|
||||
// update account orders with the changed order.
|
||||
account_orders[key] = change.New;
|
||||
}
|
||||
|
||||
void _on_order_removed(OrderRecordImmutable old_val) {
|
||||
// An entry for this order should be present in our data,
|
||||
// as it is in the 'removed' items.
|
||||
// Assert this is the case to illustrate the point.
|
||||
Debug.Assert(_data.ContainsKey(old_val.Def.AccountId));
|
||||
var account_orders = _data[old_val.Def.AccountId];
|
||||
// the removed value should match the one currently in our data.
|
||||
// Assert this is the case, then remove the order.
|
||||
Debug.Assert(account_orders[old_val.Key].Equals(old_val));
|
||||
account_orders.Remove(old_val.Key);
|
||||
_print_order_info(old_val.Key, "has been removed.");
|
||||
}
|
||||
|
||||
void _on_update(Update<OrderRecordImmutable, OrderKey> update) {
|
||||
// record which accounts experienced an order update, then
|
||||
// at the end of this method, fire the account_orders_changed event for each
|
||||
var accounts_with_order_updates = new HashSet<AccountKey>();
|
||||
foreach (OrderRecordImmutable new_order in update.added) {
|
||||
_on_order_added(new_order);
|
||||
accounts_with_order_updates.Add(new_order.Def.AccountId);
|
||||
}
|
||||
foreach (Change<OrderRecordImmutable> change in update.changes) {
|
||||
_on_order_changed(change);
|
||||
accounts_with_order_updates.Add(change.New.Def.AccountId);
|
||||
}
|
||||
foreach (OrderRecordImmutable old_order in update.removed) {
|
||||
_on_order_removed(old_order);
|
||||
accounts_with_order_updates.Add(old_order.Def.AccountId);
|
||||
}
|
||||
// display all accounts with order updates in the Console window.
|
||||
Console.WriteLine("The following accounts experienced order changes: ");
|
||||
foreach (var account in accounts_with_order_updates) {
|
||||
Console.WriteLine("\n\t" + account);
|
||||
}
|
||||
// lastly, fire the account_orders_changed event for each account experienced an order update.
|
||||
foreach (var account in accounts_with_order_updates) {
|
||||
account_orders_changed?.Invoke(account);
|
||||
}
|
||||
}
|
||||
|
||||
void _print_order_info(OrderKey order_key, string message) {
|
||||
Console.WriteLine($"Order {order_key}: {message}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
using bts.api.core;
|
||||
using bts.api.position;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace bts.api.csharp.examples {
|
||||
/// <summary>
|
||||
/// A pair of security, and account id,
|
||||
/// which uniquely identifies each PositionUnit.
|
||||
/// </summary>
|
||||
readonly struct PositionKey {
|
||||
public PositionKey(uint security_id, uint account_id) {
|
||||
this.security_id = security_id;
|
||||
this.account_id = account_id;
|
||||
}
|
||||
|
||||
public uint security_id { get; }
|
||||
public uint account_id { get; }
|
||||
|
||||
public static PositionKey from_position(PositionUnitImmutable p) =>
|
||||
new PositionKey(p.SecurityId, p.AccountId);
|
||||
|
||||
public override string ToString() =>
|
||||
$"Sec {security_id}, Acc {account_id}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Uses the <see cref="PositionService"/> api to monitor positions
|
||||
/// for every account/security pair, and organize them into a Dictionary.
|
||||
/// </summary>
|
||||
class PositionExample : IDisposable {
|
||||
readonly Dictionary<PositionKey, PositionUnitImmutable> _data = new Dictionary<PositionKey, PositionUnitImmutable>();
|
||||
IDisposable _subscription;
|
||||
|
||||
public PositionExample(PositionService position_service) {
|
||||
/// 'PositionService' has 2 methods which can be used to subscribe to position updates.
|
||||
/// -------------------------------------------------------------------------------------
|
||||
/// 1) <see cref="PositionService.subscribe_position_data(string)"/>.
|
||||
/// 2) <see cref="PositionService.subscribei_position_data(string)"/>.
|
||||
/// This method (no. 2) is typically preferrable because the data it provides is immutable.
|
||||
/// --------------------------------------------------------------------------------------
|
||||
_subscription = position_service.subscribei_position_data()
|
||||
// the 'ObserveOn' call below ensures that the
|
||||
// callback will be invoked this thread.
|
||||
// Be sure to do this if you don't want to worry
|
||||
// about multi-threading issues (locking, etc).
|
||||
.ObserveOn(SynchronizationContext.Current)
|
||||
.Subscribe(_on_update);
|
||||
}
|
||||
|
||||
public event Action data_changed;
|
||||
|
||||
public IReadOnlyDictionary<PositionKey, PositionUnitImmutable> data => _data;
|
||||
|
||||
public void Dispose() =>
|
||||
_subscription.Dispose();
|
||||
|
||||
void _on_position_added(PositionUnitImmutable new_val) {
|
||||
var k = PositionKey.from_position(new_val);
|
||||
// this entry should NOT be present in our _data, as it is in the 'added' items.
|
||||
// Assert this is the case to illustrate the point.
|
||||
Debug.Assert(!_data.ContainsKey(k));
|
||||
_print_position_info(k, $"initial position received. Day: Long {new_val.DayLongPos}, Short {new_val.DayShortPos}, Net {new_val.DayLongPos - new_val.DayShortPos}.");
|
||||
// add the new position unit to _data
|
||||
_data.Add(k, new_val);
|
||||
}
|
||||
|
||||
void _on_position_changed(Change<PositionUnitImmutable> change) {
|
||||
var k = PositionKey.from_position(change.New);
|
||||
// ch.New and ch.Old have the same key (i.e., the Account/Security pair),
|
||||
Debug.Assert(change.New.AccountId == change.Old.AccountId);
|
||||
Debug.Assert(change.New.SecurityId == change.Old.SecurityId);
|
||||
// of course, the value for this key in our dictionary should equal ch.Old.
|
||||
Debug.Assert(_data[k].Equals(change.Old));
|
||||
// Display changes to the day position in the console window.
|
||||
// 1) check if the day long changed
|
||||
if (change.New.DayLongPos != change.Old.DayLongPos) {
|
||||
Console.WriteLine($"[{k}] day long changed from {change.Old.DayLongPos} to {change.New.DayLongPos}.");
|
||||
}
|
||||
// 2) check if day short changed
|
||||
if (change.New.DayShortPos != change.Old.DayShortPos) {
|
||||
Console.WriteLine($"[{k}] day short changed from {change.Old.DayShortPos} to {change.New.DayShortPos}.");
|
||||
}
|
||||
// 3) check if net day position changed
|
||||
var new_net_day_pos = change.New.DayLongPos - change.New.DayShortPos;
|
||||
var old_net_day_pos = change.Old.DayLongPos - change.Old.DayShortPos;
|
||||
if (new_net_day_pos != old_net_day_pos) {
|
||||
Console.WriteLine($"[{k}] day net changed from {old_net_day_pos} to {new_net_day_pos}.");
|
||||
}
|
||||
// update _data with the changed position.
|
||||
_data[k] = change.New;
|
||||
}
|
||||
|
||||
void _on_position_removed(PositionUnitImmutable old_val) {
|
||||
var k = PositionKey.from_position(old_val);
|
||||
// this entry should be present in our _data, as it is in the 'removed' items.
|
||||
// Assert this is the case to illustrate the point.
|
||||
Debug.Assert(_data.ContainsKey(k));
|
||||
// this should match our data. Assert this is the case.
|
||||
Debug.Assert(_data[k].Equals(old_val));
|
||||
_data.Remove(k);
|
||||
Console.WriteLine($"[{k}] position has been removed.");
|
||||
}
|
||||
|
||||
void _on_update(Update<PositionUnitImmutable, int> update) {
|
||||
foreach (PositionUnitImmutable p in update.added) {
|
||||
_on_position_added(p);
|
||||
}
|
||||
foreach (Change<PositionUnitImmutable> ch in update.changes) {
|
||||
_on_position_changed(ch);
|
||||
}
|
||||
foreach (PositionUnitImmutable p in update.removed) {
|
||||
_on_position_removed(p);
|
||||
}
|
||||
data_changed?.Invoke();
|
||||
}
|
||||
|
||||
void _print_position_info(PositionKey position_key, string message) {
|
||||
Console.WriteLine($"Position {position_key}: {message}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
using bts.api.core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using SecurityKey = System.UInt32;
|
||||
|
||||
namespace bts.api.csharp.examples {
|
||||
/// <summary>
|
||||
/// Uses the <see cref="MarketDataService"/> api to observe the
|
||||
/// state of securities in the system.
|
||||
/// </summary>
|
||||
class SecurityExample : IDisposable {
|
||||
readonly IDisposable _subscription;
|
||||
readonly Dictionary<SecurityKey, SecurityImmutable> _securities = new Dictionary<SecurityKey, SecurityImmutable>();
|
||||
|
||||
public SecurityExample(MarketDataService market_data_service) {
|
||||
_subscription = market_data_service.subscribei_securities()
|
||||
.ObserveOn(SynchronizationContext.Current)
|
||||
.Subscribe(_on_securities_update);
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
_subscription.Dispose();
|
||||
}
|
||||
|
||||
void _on_securities_update(Update<SecurityImmutable, uint> update) {
|
||||
foreach (var new_security in update.added) {
|
||||
// of course, we shouldn't have entries for any books in update.added yet
|
||||
Debug.Assert(!_securities.ContainsKey(new_security.Id));
|
||||
// display bbo in the console window
|
||||
Console.WriteLine($"received new security: {new_security}.");
|
||||
// add the new security to our map
|
||||
_securities.Add(new_security.Id, new_security);
|
||||
}
|
||||
// As you may have noticed, all of the subcribe methods in the api
|
||||
// return an IObservable<Update<T>>. It is worth noting that for security updates,
|
||||
// 'changes' will always be empty.
|
||||
Debug.Assert(!update.changes.Any());
|
||||
foreach (var old_security in update.removed) {
|
||||
// of course, we should have entries for all books in update.removed
|
||||
Debug.Assert(_securities.Remove(old_security.Id));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
using bts.api.csharp.utils;
|
||||
using bts.api.orderman;
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.api.csharp.examples {
|
||||
static class OrderTemplates {
|
||||
public static CustomOrderDefinition limit() => new CustomOrderDefinition { Limit = new LimitOrderDefinition() };
|
||||
public static CustomOrderDefinition market() => new CustomOrderDefinition { Market = new MarketOrderDefinition() };
|
||||
public static CustomOrderDefinition iceberg(double show_quantity) => iceberg(show_quantity, limit());
|
||||
public static CustomOrderDefinition iceberg(double show_quantity, CustomOrderDefinition child) =>
|
||||
new CustomOrderDefinition {
|
||||
Iceberg = new IcebergOrderDefinition {
|
||||
ShowQuantity = show_quantity,
|
||||
Child = child
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exemplifies how the <see cref="OrderService"/> api may be used to
|
||||
/// send several common order types (limit, market, and iceberg).
|
||||
/// </summary>
|
||||
class SendOrderModel {
|
||||
readonly OrderService _service;
|
||||
|
||||
public SendOrderModel(OrderService service) {
|
||||
_service = service;
|
||||
}
|
||||
|
||||
public OrderDefinition create_order_definition(NameIDPair user, NameIDPair account, uint security_key, CustomOrderDefinition custom_def) {
|
||||
return new OrderDefinition {
|
||||
Account = account.name,
|
||||
AccountId = account.id,
|
||||
User = user.name,
|
||||
UserId = user.id,
|
||||
SecurityKey = security_key,
|
||||
CustomDef = custom_def
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a limit order (asynchronous).
|
||||
/// </summary>
|
||||
/// <returns>The order id if the submission was successful, otherwise null.</returns>
|
||||
public Task<uint?> send_limit_order_async(NameIDPair user, NameIDPair account, uint security_key, uint quantity, double price) {
|
||||
return send_order_async(user, account, security_key, OrderTemplates.limit(), quantity, price);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends a market order (asynchronous). Note no price parameter is required for market orders.
|
||||
/// </summary>
|
||||
/// <returns>The order id if the submission was successful, otherwise null.</returns>
|
||||
public Task<uint?> send_market_order_async(NameIDPair user, NameIDPair account, uint security_key, uint quantity) {
|
||||
return send_order_async(user, account, security_key, OrderTemplates.market(), quantity, 0.0);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an iceberg of limit orders (asynchronous).
|
||||
/// </summary>
|
||||
/// <returns>The order id if the submission was successful, otherwise null.</returns>
|
||||
public Task<uint?> send_iceberg_order_async(NameIDPair user, NameIDPair account, uint security_key, uint quantity, double price, double show_quantity) {
|
||||
return send_order_async(user, account, security_key, OrderTemplates.iceberg(show_quantity), quantity, price);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sends an order of the type specified by <paramref name="custom_order_definition"/> (asynchronous).
|
||||
/// </summary>
|
||||
/// <returns>The order id if the submission was successful, otherwise null.</returns>
|
||||
public async Task<uint?> send_order_async(NameIDPair user, NameIDPair account, uint security_key, CustomOrderDefinition custom_order_definition, uint quantity, double price) {
|
||||
Console.WriteLine($"submitting order for user ({user}), account ({account}): {OrderUtils.fmt_order(quantity, price, custom_order_definition)}.");
|
||||
var reply = await _service.create_order_async(new Create {
|
||||
Quantity = quantity,
|
||||
Price = price,
|
||||
NewOrder = create_order_definition(user, account, security_key, custom_order_definition)
|
||||
});
|
||||
if (reply.success) {
|
||||
Console.WriteLine($"received ack that order has been submitted. Order id is {reply.value.OrderKey}.");
|
||||
return reply.value.OrderKey;
|
||||
} else {
|
||||
Console.WriteLine($"order submission was unsuccessful: [{reply.value.ErrorCode}] {reply.value.Msg}.");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public void send_order(NameIDPair user, NameIDPair account, uint security_key, CustomOrderDefinition custom_order_definition, uint quantity, double price) {
|
||||
/// Note, the thread will be blocked until a response is received from the server.
|
||||
/// For this reason, <see cref="OrderService.create_order_async(Create)"/> is usually preferred.
|
||||
/// An example of how to use that method is provided in <see cref="send_order_async(NameIDPair, NameIDPair, CustomOrderDefinition, uint, double)"/>.
|
||||
Console.WriteLine($"Creating order {OrderUtils.fmt_order(quantity, price, custom_order_definition)}");
|
||||
var response = _service.create_order(new Create {
|
||||
Quantity = quantity,
|
||||
Price = price,
|
||||
NewOrder = create_order_definition(user, account, security_key, custom_order_definition)
|
||||
});
|
||||
switch (response.ErrorCode) {
|
||||
case OrderError.Ok:
|
||||
_print_order_info(response.OrderKey, "successfully created.");
|
||||
break;
|
||||
default:
|
||||
Console.WriteLine($"Failed to create order: [{response.ErrorCode}] {response.Msg}.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
static void _print_order_info(uint order_key, string message) {
|
||||
Console.WriteLine($"Order {order_key}: {message}.");
|
||||
}
|
||||
}
|
||||
}
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
using bts.api.core;
|
||||
using bts.api.orderman;
|
||||
using mbts;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace bts.api.csharp.examples {
|
||||
/// <summary>
|
||||
/// The code in this example is a bit more advanced than the others,
|
||||
/// as it is designed to send orders, asynchronously, in a continuous loop.
|
||||
/// For straightforward examples of how to send orders with the api,
|
||||
/// check out <see cref="SendOrderModel"/>.
|
||||
/// </summary>
|
||||
static class SendOrderLoop {
|
||||
/// <summary>
|
||||
/// As a security key is required to send an order, this method
|
||||
/// uses the <see cref="MarketDataService"/> to observe the securities stream,
|
||||
/// then on the first update, initializes the send-order-loop <see cref="LoopImpl"/>
|
||||
/// with one of the newly received security keys.
|
||||
/// </summary>
|
||||
public static IDisposable init_loop(OrderService order_service, MarketDataService market_data_service) {
|
||||
// If you need to familiarize yourself with the IObservable pattern, check out the official docs:
|
||||
// https://docs.microsoft.com/en-us/dotnet/api/system.iobservable-1?view=netcore-3.1
|
||||
|
||||
var single_security_stream = market_data_service.subscribei_securities()
|
||||
.Where(u => u.all.Any())
|
||||
.Take(1)
|
||||
.Select(u => u.all.FirstOrDefault());
|
||||
|
||||
return init_loop(single_security_stream, create_loop);
|
||||
IDisposable create_loop(SecurityImmutable sec) {
|
||||
return new LoopImpl(order_service, sec.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Observes the security stream and creates a new send-order-loop
|
||||
/// whenever a new security arrives. The previous send-order-loop
|
||||
/// is disposed when the next security arrives, so only one send-order-loop
|
||||
/// shall be active at a time.
|
||||
/// </summary>
|
||||
/// <param name="security">The security stream used to initialize send-order-loops.</param>
|
||||
/// <returns>An IDisposable that unsubsribes from the securities stream, and disposes the active send-order-loop.</returns>
|
||||
public static IDisposable init_loop(IObservable<SecurityImmutable> security, Func<SecurityImmutable, IDisposable> factory) {
|
||||
IDisposable active_loop = null;
|
||||
var security_sub = security.Subscribe(s =>
|
||||
Interlocked.Exchange(ref active_loop, factory(s))?.Dispose());
|
||||
return Disposable.Create(dispose);
|
||||
void dispose() {
|
||||
security_sub.Dispose();
|
||||
active_loop?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class LoopImpl : IDisposable {
|
||||
bool _is_disposed;
|
||||
|
||||
public LoopImpl(OrderService service, uint security_key)
|
||||
: this(service, Users.Default, Accounts.Default, security_key, 10, TimeSpan.FromSeconds(3)) { }
|
||||
|
||||
public LoopImpl(OrderService service, NameIDPair user, NameIDPair account, uint security_key, int max_orders, TimeSpan delay_between_orders) {
|
||||
var model = new SendOrderModel(service);
|
||||
send_orders_continuously();
|
||||
|
||||
async void send_orders_continuously() {
|
||||
// Periodically send an order with one of the CustomOrderDefinitions below.
|
||||
// If you are unfamiliar with async/await, this will also serve as a simple example
|
||||
// of how those keywords may be used. However, there is no substite for the official docs:
|
||||
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/async/
|
||||
var order_templates = new CustomOrderDefinitionImmutable[] {
|
||||
new CustomOrderDefinitionImmutable(OrderTemplates.limit()),
|
||||
new CustomOrderDefinitionImmutable(OrderTemplates.market()),
|
||||
new CustomOrderDefinitionImmutable(OrderTemplates.iceberg(5))
|
||||
};
|
||||
for (int num_orders_sent = 0; num_orders_sent < max_orders && !_is_disposed; num_orders_sent++) {
|
||||
int template_index = num_orders_sent % order_templates.Length;
|
||||
CustomOrderDefinitionImmutable templatei = order_templates[template_index];
|
||||
// The order templates we created are immutable, however the api uses the mutable form.
|
||||
// Not to worry tho, there is a nifty method called 'CloneToMutable.'
|
||||
// Note that all of the non-primitive immutable parameters in the api methods have a 'CloneToMutable' method,
|
||||
// it is not unique to CustomOrderDefinitionImmutable.
|
||||
CustomOrderDefinition template = templatei.CloneToMutable();
|
||||
await model.send_order_async(user, account, security_key, template, 12, 100);
|
||||
// pause for a moment before sending the next order
|
||||
await Task.Delay(delay_between_orders);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() {
|
||||
// flip _is_disposed to true, so the 'send_orders_continuously' loop
|
||||
// knows it's time to stop.
|
||||
_is_disposed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace bts.api.csharp.utils {
|
||||
static class BookUtils {
|
||||
/// <summary>
|
||||
/// Get a display string of the bbo.
|
||||
/// </summary>
|
||||
public static string fmt_bbo(BookEntryImmutable book) {
|
||||
return fmt_bbo(get_bbo(book));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a display string of the bbo.
|
||||
/// </summary>
|
||||
public static string fmt_bbo((PriceEntryImmutable? bid, PriceEntryImmutable? ask) bbo) {
|
||||
return $"{fmt_price_entry(bbo.bid)} <--> {fmt_price_entry(bbo.ask)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a display string of a <see cref="PriceEntryImmutable"/>.
|
||||
/// </summary>
|
||||
public static string fmt_price_entry(PriceEntryImmutable? price_entry) {
|
||||
return price_entry.HasValue ? fmt_price_entry(price_entry.Value) : "N/A";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a display string of a <see cref="PriceEntryImmutable"/>.
|
||||
/// </summary>
|
||||
public static string fmt_price_entry(PriceEntryImmutable price_entry) {
|
||||
return $"{price_entry}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get a display string of the bbo.
|
||||
/// </summary>
|
||||
public static (PriceEntryImmutable? best_bid, PriceEntryImmutable? best_ask) get_bbo(BookEntryImmutable book) {
|
||||
return (get_top_of_book(book.Bid), get_top_of_book(book.Ask));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the first element in <paramref name="depth"/>,
|
||||
/// as this represents the top price level
|
||||
/// of a particular side of the market (bid or ask).
|
||||
/// </summary>
|
||||
public static PriceEntryImmutable? get_top_of_book(IEnumerable<PriceEntryImmutable> depth) {
|
||||
foreach (var d in depth)
|
||||
return d;
|
||||
// there are no elements (the book is empty for this side), so return null.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using bts.api.orderman;
|
||||
|
||||
namespace bts.api.csharp.utils {
|
||||
static class OrderUtils {
|
||||
public static string fmt_order(uint quantity, double price, CustomOrderDefinition custom_order_definition) {
|
||||
return $"{quantity} @ {price}: {custom_order_definition}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
|
||||
namespace bts.api.csharp.utils {
|
||||
static class SecurityUtils {
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
set(sources ../../externals/linux/include/google/protobuf/descriptor.proto;../../externals/linux/include/google/protobuf/timestamp.proto;../lib/api/apicore.proto;../lib/api/apigateway.proto;../lib/api/apioptions.proto;../lib/api/services/AdminService.proto;../lib/api/services/AlgoDefinitionService.proto;../lib/api/services/AlgoService.proto;../lib/api/services/ApiUnitTestService.proto;../lib/api/services/AuthService.proto;../lib/api/services/BasePositionService.proto;../lib/api/services/ErrorReportService.proto;../lib/api/services/HelloService.proto;../lib/api/services/MBOService.proto;../lib/api/services/MarginService.proto;../lib/api/services/MarketDataAuxiliaryService.proto;../lib/api/services/MarketDataRelay.proto;../lib/api/services/MarketDataService.proto;../lib/api/services/MoneyFileService.proto;../lib/api/services/OrderService.proto;../lib/api/services/OutOfProcessURLListener.proto;../lib/api/services/PersistenceService.proto;../lib/api/services/PositionService.proto;../lib/api/services/ShellDiagnosticService.proto;../lib/api/services/ShellLatencyService.proto;../lib/api/services/SyntheticSecurityService.proto;../lib/domain_model/Security.proto;../lib/md/protos/co2.proto;../lib/sparkproto/SparkProtos.proto;api/build.sh;api/node_modules/exit/test/fixtures/create-files.sh;api/node_modules/object.assign/test.sh;api/node_modules/watch/release.sh;api/quiet_build.sh;api/src/DynamicService.ts;api/src/ServiceContext.ts;api/src/bts_assert.ts;ui/src/Access.fs;ui/src/Accounts/ClipLimits.fs;ui/src/Accounts/ProductLimits.fs;ui/src/Accounts/SecurityLimits.fs;ui/src/Accounts/State.fs;ui/src/Accounts/Types.fs;ui/src/Accounts/View.fs;ui/src/App.fs;ui/src/Cmd.fs;ui/src/Controls/Button.fs;ui/src/Controls/DataGrid.fs;ui/src/Controls/Fields.fs;ui/src/Controls/Modal.fs;ui/src/Controls/ReactGrid.fs;ui/src/Controls/Table.fs;ui/src/Controls/nbsp.fs;ui/src/Global.fs;ui/src/Imports.fs;ui/src/Lens.fs;ui/src/Login/Login.fs;ui/src/Nav.fs;ui/src/State.fs;ui/src/Types.fs;ui/src/Users/State.fs;ui/src/Users/Types.fs;ui/src/Users/View.fs;ui/src/Util/EnumUtils.fs;ui/src/Util/JSMap.fs;ui/src/Util/StreamUtils.fs;ui/src/obj/Debug/netstandard2.0/ui.AssemblyInfo.fs)
|
||||
set(sources ../../externals/linux/include/google/protobuf/descriptor.proto;../../externals/linux/include/google/protobuf/timestamp.proto;../lib/api/apicore.proto;../lib/api/apigateway.proto;../lib/api/apioptions.proto;../lib/api/services/AdminService.proto;../lib/api/services/AlgoDefinitionService.proto;../lib/api/services/AlgoService.proto;../lib/api/services/ApiUnitTestService.proto;../lib/api/services/AuthService.proto;../lib/api/services/BasePositionService.proto;../lib/api/services/ErrorReportService.proto;../lib/api/services/HelloService.proto;../lib/api/services/MBOService.proto;../lib/api/services/MarginService.proto;../lib/api/services/MarketDataAuxiliaryService.proto;../lib/api/services/MarketDataRelay.proto;../lib/api/services/MarketDataService.proto;../lib/api/services/MoneyFileService.proto;../lib/api/services/OrderService.proto;../lib/api/services/OutOfProcessURLListener.proto;../lib/api/services/PersistenceService.proto;../lib/api/services/PositionService.proto;../lib/api/services/ShellDiagnosticService.proto;../lib/api/services/ShellLatencyService.proto;../lib/api/services/SyntheticSecurityService.proto;../lib/domain_model/Security.proto;../lib/md/protos/co2.proto;../lib/sparkproto/SparkProtos.proto;api/build.sh;api/quiet_build.sh;api/src/DynamicService.ts;api/src/ServiceContext.ts;api/src/bts_assert.ts;ui/src/Access.fs;ui/src/Accounts/ClipLimits.fs;ui/src/Accounts/ProductLimits.fs;ui/src/Accounts/SecurityLimits.fs;ui/src/Accounts/State.fs;ui/src/Accounts/Types.fs;ui/src/Accounts/View.fs;ui/src/App.fs;ui/src/Cmd.fs;ui/src/Controls/Button.fs;ui/src/Controls/DataGrid.fs;ui/src/Controls/Fields.fs;ui/src/Controls/Modal.fs;ui/src/Controls/ReactGrid.fs;ui/src/Controls/Table.fs;ui/src/Controls/nbsp.fs;ui/src/Global.fs;ui/src/Imports.fs;ui/src/Lens.fs;ui/src/Login/Login.fs;ui/src/Nav.fs;ui/src/State.fs;ui/src/Types.fs;ui/src/Users/State.fs;ui/src/Users/Types.fs;ui/src/Users/View.fs;ui/src/Util/EnumUtils.fs;ui/src/Util/JSMap.fs;ui/src/Util/StreamUtils.fs;ui/src/obj/Debug/netstandard2.0/ui.AssemblyInfo.fs)
|
||||
@@ -12,6 +12,12 @@ ignore = Path docker
|
||||
ignore = Regex .*\.pyc
|
||||
ignore = Path x64
|
||||
ignore = Path .vs
|
||||
ignore = Path src/.vs
|
||||
ignore = Path src/lib/.vs
|
||||
ignore = Path src/lib/out
|
||||
ignore = Path src/sdk/out
|
||||
ignore = Path src/server/sparkd/out
|
||||
ignore = Path src/web/out
|
||||
ignore = Path .vscode
|
||||
ignore = Path src/client
|
||||
ignore = Path src/web/api/node_modules
|
||||
|
||||
Reference in new issue
Block a user