Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b3369db7cc | ||
|
|
279abcfefd | ||
|
|
872e2465a6 | ||
|
|
89fb79c556 | ||
|
|
c92e9df77b | ||
|
|
81aaa853d5 | ||
|
|
4f308faba2 | ||
|
|
0b924033c2 | ||
|
|
3606c707e4 | ||
|
|
5998107e6a | ||
|
|
297982433b | ||
|
|
15ac9d945b | ||
|
|
a0f1d11692 | ||
|
|
e7890a459c | ||
|
|
f7fb5ed400 | ||
|
|
75e98d353f | ||
|
|
5fc6ff4c00 | ||
|
|
6951272633 | ||
|
|
548611b44f | ||
|
|
322da04723 | ||
|
|
3544eac815 | ||
|
|
7fef7d88ce | ||
|
|
7531cf5eb4 | ||
|
|
5826a083c0 | ||
|
|
ec0d5d22f1 | ||
|
|
d433368795 | ||
|
|
dc4453e510 | ||
|
|
1b1583abda | ||
|
|
9d2f2888bb | ||
|
|
26816f0048 | ||
|
|
e97e6b93c7 | ||
|
|
0d8252e131 | ||
|
|
801b9622f8 | ||
|
|
a5df3f2da2 | ||
|
|
f03a413855 | ||
|
|
f3a692bda2 | ||
|
|
6c3bdbdc34 | ||
|
|
d4749dd381 | ||
|
|
a008f4d0fd | ||
|
|
0ee9521454 | ||
|
|
62cef7c2f7 | ||
|
|
1b39f4e47c | ||
|
|
3212b2fcee | ||
|
|
1576541bd1 | ||
|
|
62e97364fb | ||
|
|
93b954ce46 | ||
|
|
c9da022c58 | ||
|
|
0772a13b09 | ||
|
|
c53f6ad906 | ||
|
|
1058c8a80e | ||
|
|
7ecb106880 | ||
|
|
b0c1d93c50 | ||
|
|
0c5a039226 | ||
|
|
2677144bfa | ||
|
|
6b30c42c0d | ||
|
|
828f867a80 | ||
|
|
c9663cf7ab | ||
|
|
b85de51d0c | ||
|
|
562b9419bd | ||
|
|
3e035197d0 | ||
|
|
c9d8a640b6 | ||
|
|
1d97e620fd | ||
|
|
f6299ab5c3 | ||
|
|
eaff4774e0 | ||
|
|
bed52db98a | ||
|
|
bd3ef33607 | ||
|
|
25a594895e | ||
|
|
4d468c8026 | ||
|
|
68ed90487b | ||
|
|
ddc2c67fea | ||
|
|
77459e9c64 | ||
|
|
db64f2f45b | ||
|
|
3f7ea71de7 | ||
|
|
d334418655 | ||
|
|
d1844afeae | ||
|
|
b6dc2bd4d4 | ||
|
|
58906eb3b6 | ||
|
|
963a4207e2 | ||
|
|
4e58342677 | ||
|
|
5eeb5a534c | ||
|
|
f20c53bced | ||
|
|
cf977befd1 | ||
|
|
c9b2ac290c | ||
|
|
340d65a47c | ||
|
|
1b3f5bc8a6 | ||
|
|
1b767f6e5e | ||
|
|
fe647044f8 | ||
|
|
69dcfdb2bd | ||
|
|
e179f7e5a4 | ||
|
|
dba3747d98 | ||
|
|
3e7627ac24 | ||
|
|
1b043f77cb |
No files matched your search
@@ -4,6 +4,23 @@ weight = 3
|
||||
sort_by = "weight"
|
||||
insert_anchor_links = "right"
|
||||
+++
|
||||
## 2.13.0
|
||||
### User Visible Changes
|
||||
- Added the name of the spreader to the parent algo badge (for spreader child orders), in the fills, orderbook and floating orderbook windows
|
||||
- Added support for sounds for new orders
|
||||
- Added a new quoting throttle parameter for the spreader, that throttles quotes that are moving toward the inside market but are still a configurable number of ticks away
|
||||
- Enabled the ladder to remember the left panel scroll position across front end restarts
|
||||
### Bug Fixes
|
||||
- Fixed the local fills window so that it does not show the admin fills
|
||||
- Removed the deleted accounts from the ladder accounts dropdown
|
||||
- Fixed the orderbook price for iceberg of spreaders
|
||||
|
||||
## 2.12.0
|
||||
### User Visible Changes
|
||||
- Added support for ICE market data and order execution
|
||||
### Internal Changes
|
||||
- Lots of permission / admin changes to facilitate ICE
|
||||
|
||||
## 2.11.0
|
||||
### User Visible Changes
|
||||
- Fixed fills form time filter to also apply to SOD fills
|
||||
|
||||
@@ -25,7 +25,7 @@ namespace mbts.spark {
|
||||
usr.UserData.AccountPermissions.AccountPermissions
|
||||
.Where(kv => (kv.Value.TradingPermission != null && kv.Value.TradingPermission.HasPermissions) || kv.Value.AdminPermission.HasPermissions)
|
||||
.Select(kv => ad.Accounts.Where(a => a.Id == kv.Key).TryGetSingle(out var acc) ? acc : (BTSAccountImmutable?)null)
|
||||
.Where(acc => acc.HasValue)
|
||||
.Where(acc => acc.HasValue && acc.Value.IsPresent)
|
||||
.Select(acc => acc.Value)
|
||||
.ToDictionary(acc => acc.Id, acc => acc.Name) :
|
||||
new Dictionary<uint, string>(0))
|
||||
|
||||
@@ -43,6 +43,11 @@ namespace bts.shell {
|
||||
void _show_order_window(Window window) {
|
||||
foreach (var kv in AlgoStudioModelUtils.get_dynamic_resources(_model.algo_model.ui_configuration))
|
||||
window.Resources[kv.Key] = kv.Value;
|
||||
// the dynamic algo 'SecurityKey' template has a ContractPickerPopup that expects the security map to be available as a DynamicResource.
|
||||
var sub = _bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => window.Resources[ResourceKeys.Securities] = sec_map);
|
||||
window.Closed += (s, e) => Disposable.dispose(ref sub);
|
||||
window.Show(-200, -160, true);
|
||||
}
|
||||
|
||||
|
||||
@@ -81,9 +81,9 @@ namespace bts.shell {
|
||||
#region QuoteParams
|
||||
public static QuoteParams QuoteParams => new QuoteParams {
|
||||
LeanQuantityRatio = 1,
|
||||
QuoteInsideMarketDistance = QuoteInsideMarketDistance,
|
||||
//QuoteInsideMarketDistance = QuoteInsideMarketDistance,
|
||||
QuoteMaxOrderMove = QuoteMaxOrderMove,
|
||||
QuoteMuteZone = QuoteMuteZone,
|
||||
//QuoteMuteZone = QuoteMuteZone,
|
||||
QuoteRepriceThrottle = QuoteRepriceThrottle,
|
||||
QuoteInsideMarketDistanceWithTravel = QuoteInsideMarketDistanceWithTravel,
|
||||
};
|
||||
@@ -113,6 +113,7 @@ namespace bts.shell {
|
||||
};
|
||||
public static QuoteInsideMarketDistanceWithTravel QuoteInsideMarketDistanceWithTravel => new QuoteInsideMarketDistanceWithTravel { Priority = RulePriority };
|
||||
public static QuoteInsideMarketDistance QuoteInsideMarketDistance => new QuoteInsideMarketDistance { Priority = RulePriority };
|
||||
public static QuoteInsideMarketDistanceThrottle QuoteInsideMarketDistanceThrottle => new QuoteInsideMarketDistanceThrottle();
|
||||
#endregion
|
||||
|
||||
#region PreHedge
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
using bts.shell;
|
||||
using mbts.ui.spark;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class SyntheticEditorVM {
|
||||
class QuoteInsideMarketDistanceEnabledModel : RowModelBase<bool> {
|
||||
readonly QuoteInsideMarketDistanceTowardModel _toward;
|
||||
|
||||
public QuoteInsideMarketDistanceEnabledModel(QuoteInsideMarketDistanceTowardModel toward) =>
|
||||
_toward = toward;
|
||||
|
||||
public override string display_name { get; } = string.Empty;
|
||||
public override string help_text { get; } = string.Empty;
|
||||
public override string man_page { get; } = string.Empty;
|
||||
public override ParameterSection[] section_path { get; } = new[] { Sections.Quote, Subsections.InsideMarketDistance };
|
||||
|
||||
public override bool get_value(LegSpreaderParamsTuple leg_tup) =>
|
||||
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle != null;
|
||||
|
||||
public override void set_value(LegSpreaderParamsTuple leg_tup, bool value) {
|
||||
if (value) {
|
||||
if (leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle == null) {
|
||||
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle = SyntheticDataHelper.Defaults.QuoteInsideMarketDistanceThrottle;
|
||||
_toward.apply_value(leg_tup);
|
||||
}
|
||||
} else {
|
||||
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle = null;
|
||||
}
|
||||
_toward.set_enabled(leg_tup, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
using bts.spark;
|
||||
using mbts.ui.spark;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace mbts.spark {
|
||||
partial class SyntheticEditorVM {
|
||||
class QuoteInsideMarketDistanceTowardModel : RowModelBase<uint> {
|
||||
IDictionary<uint, uint> _value_store = new Dictionary<uint, uint>();
|
||||
|
||||
public override string display_name { get; } = "Toward Distance";
|
||||
public override string help_text { get; } = string.Empty;
|
||||
public override string man_page { get; } = string.Empty;
|
||||
public override ParameterSection[] section_path { get; } = new[] { Sections.Quote, Subsections.InsideMarketDistance };
|
||||
|
||||
public void apply_value(LegSpreaderParamsTuple leg_tup) =>
|
||||
_get_inside_market_distance(leg_tup).OnTowardDistance = _value_from_store(leg_tup);
|
||||
|
||||
public override uint get_value(LegSpreaderParamsTuple leg_tup) =>
|
||||
_get_inside_market_distance(leg_tup) is QuoteInsideMarketDistanceThrottle inside_market_distance ?
|
||||
inside_market_distance.OnTowardDistance :
|
||||
_value_from_store(leg_tup);
|
||||
|
||||
public override void set_value(LegSpreaderParamsTuple leg_tup, uint value) {
|
||||
_value_store[leg_tup.SecurityKey] = value;
|
||||
if (_get_inside_market_distance(leg_tup) is QuoteInsideMarketDistanceThrottle inside_market_distance)
|
||||
inside_market_distance.OnTowardDistance = value;
|
||||
}
|
||||
|
||||
protected override bool _get_enabled(LegSpreaderParamsTuple leg_tup) =>
|
||||
_get_inside_market_distance(leg_tup) != null;
|
||||
|
||||
QuoteInsideMarketDistanceThrottle _get_inside_market_distance(LegSpreaderParamsTuple leg_tup) =>
|
||||
leg_tup.leg_params.QuoteParams.QuoteInsideMarketDistanceThrottle;
|
||||
|
||||
uint _value_from_store(LegSpreaderParamsTuple leg_tup) =>
|
||||
_value_store.GetValueOrDefault(leg_tup.SecurityKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,11 @@ namespace mbts.spark {
|
||||
"dictates if this logic applies to the quoting market or to the synthetic spread " +
|
||||
"market");
|
||||
|
||||
public static ParameterSection InsideMarketDistance =>
|
||||
new ParameterSection("Inside Market Distance", true,
|
||||
"Reduces re-quoting by throttling quotes that are moving toward the inside market " +
|
||||
"but are more than 'Toward Distance' away from the inside market");
|
||||
|
||||
public static ParameterSection InsideMarketThrottleThresholds =>
|
||||
new ParameterSection("Inside Market Throttle Thresholds", true,
|
||||
"Reduces re-quoting for quotes that are moving toward the inside market. " +
|
||||
|
||||
@@ -190,6 +190,8 @@ namespace mbts.spark {
|
||||
var quote_inside_market_throttle_ticks = new QuoteInsideMarketThrottleTicksModel();
|
||||
var quote_inside_market_throttle_min_move = new QuoteInsideMarketThrottleMinMoveModel();
|
||||
|
||||
var quote_inside_market_distance_toward = new QuoteInsideMarketDistanceTowardModel();
|
||||
|
||||
var pre_hedge_payless_threshold = new PreHedgePaylessThresholdModel();
|
||||
var pre_hedge_payless_ticks = new PreHedgePaylessTicksModel();
|
||||
|
||||
@@ -227,6 +229,8 @@ namespace mbts.spark {
|
||||
//quote_inside_market_throttle_ticks,
|
||||
//quote_inside_market_throttle_min_move,
|
||||
//new MaxTicksInsideMarketModel(),
|
||||
new QuoteInsideMarketDistanceEnabledModel(quote_inside_market_distance_toward),
|
||||
quote_inside_market_distance_toward,
|
||||
new PreHedgePaylessEnabledModel(pre_hedge_payless_threshold, pre_hedge_payless_ticks),
|
||||
pre_hedge_payless_threshold,
|
||||
pre_hedge_payless_ticks,
|
||||
|
||||
@@ -62,7 +62,10 @@ namespace mbts.spark {
|
||||
_vm = new AlgoStudioVM(_model);
|
||||
_vm.load_from(_state);
|
||||
_view = new AlgoStudioView { DataContext = _vm };
|
||||
_resources = new CompositeDisposable();
|
||||
_resources = new CompositeDisposable(
|
||||
_bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => _view.Resources[ResourceKeys.Securities] = sec_map));
|
||||
// add definition
|
||||
_commands.Add(new SparkCommandCommand("Add Algo", Icons.AddBoxes, _vm.add_definition_cmd));
|
||||
// clone definition
|
||||
|
||||
@@ -13,6 +13,9 @@ namespace mbts.spark {
|
||||
string _template_key;
|
||||
ICommand _set_value_cmd;
|
||||
|
||||
public AlgoParamRowVM(IAlgoVM algo, IAlgoParamVM param, AlgoParamRowVM parent)
|
||||
: this(algo, param, parent, (param as ITemplateSelectorKey)?.key) { }
|
||||
|
||||
public AlgoParamRowVM(IAlgoVM algo, IAlgoParamVM param, AlgoParamRowVM parent, string template_key = null) {
|
||||
this.param = param;
|
||||
this.algo = algo;
|
||||
|
||||
+40
@@ -247,6 +247,31 @@
|
||||
</TextBlock>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static u:TemplateKeys.SecurityKey}">
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<!--Note: Ideally, this 'securities BindingProxy' would be a static resource, but it only seems to detect the dynamic securities when it is in the visual tree.-->
|
||||
<!--<Grid.Resources>
|
||||
<u:BindingProxy x:Key="SecuritiesBindingProxy" x:Shared="False" Value="{DynamicResource {x:Static u:ResourceKeys.Securities}}"/>
|
||||
</Grid.Resources>-->
|
||||
<u:BindingProxy x:Name="SecuritiesBindingProxy" Value="{DynamicResource {x:Static u:ResourceKeys.Securities}}"/>
|
||||
<TextBlock>
|
||||
<TextBlock.Text>
|
||||
<MultiBinding>
|
||||
<MultiBinding.Converter>
|
||||
<cv:MultiValueConverterPipe>
|
||||
<cv:DictionaryLookupConverter/>
|
||||
<x:Static MemberType="cv:ValueConverters" Member="SecuritySelectorText"/>
|
||||
</cv:MultiValueConverterPipe>
|
||||
</MultiBinding.Converter>
|
||||
<Binding Path="Value" ElementName="SecuritiesBindingProxy"/>
|
||||
<Binding Path="param.value"/>
|
||||
</MultiBinding>
|
||||
</TextBlock.Text>
|
||||
</TextBlock>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</usp:AlgoParamTemplateSelector.KeyedTemplates>
|
||||
<!--Bool-->
|
||||
<usp:AlgoParamTemplateSelector.BoolTemplate>
|
||||
@@ -320,6 +345,21 @@
|
||||
</usp:PriceEntryControl>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
<u:TemplateSelectorKeyValue Key="{x:Static u:TemplateKeys.SecurityKey}">
|
||||
<DataTemplate>
|
||||
<c:ContractPickerPopup
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding SelectedSecurity, RelativeSource={RelativeSource Self}, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
|
||||
IsEditMode="False"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource {x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurityKey="{Binding param.value}"
|
||||
ShowDialogSkirt="True"
|
||||
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityKeyHandler}"
|
||||
/>
|
||||
</DataTemplate>
|
||||
</u:TemplateSelectorKeyValue>
|
||||
</usp:AlgoParamTemplateSelector.KeyedTemplates>
|
||||
<!--Bool-->
|
||||
<usp:AlgoParamTemplateSelector.BoolTemplate>
|
||||
|
||||
@@ -285,7 +285,6 @@ namespace mbts.spark {
|
||||
public DateTime fill_time => _fill.Timestamp.to_local_time();
|
||||
public TimeInForce time_in_force => _order.time_in_force();
|
||||
public OrderType order_type => _order.order_type();
|
||||
//public OrderType root_algo_type => _or
|
||||
public string display_name => OrderUtils.display_name(_order, fill_tuple.algo);
|
||||
public OrderMetadata order_metadata => _order_metadata ?? (_order_metadata = _model.factory.get_metadata(_order.Def.CustomDef.CustomCase));
|
||||
public Brush badge_fill => _badge_fill ?? (_badge_fill = OrderUtils.badge_fill(fill_tuple.algo_meta, order_metadata));
|
||||
@@ -304,7 +303,7 @@ namespace mbts.spark {
|
||||
public OrderType root_algo_order_type => _root_algo.HasValue ? _root_algo.Value.value.CustomCase : OrderType.None;
|
||||
public OrderMetadata root_algo_order_metadata => _model.factory.get_metadata(root_algo_order_type);
|
||||
public Color root_algo_badge_color => _root_algo_badge_color ?? (_root_algo_badge_color = OrderUtils.badge_color(_root_algo_meta, root_algo_order_metadata)).Value;
|
||||
public string root_algo_display_name => _root_algo?.name ?? root_algo_order_type.display_name();
|
||||
public string root_algo_display_name => fill_tuple.root_order_security?.display_name() ?? OrderType.None.display_name();
|
||||
|
||||
public bool Equals(Row other) => other != null && other._fill.Equals(_fill) && other._order.Equals(_order);
|
||||
public override bool Equals(object obj) => Equals(obj as Row);
|
||||
|
||||
@@ -23,6 +23,10 @@ namespace mbts.spark {
|
||||
// TODO: somehow allow windows that are using the default settings to still have individual column widths.
|
||||
[ProtoContract]
|
||||
class LadderColumnSettings : IEquatable<LadderColumnSettings> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(LadderColumnSettings).GetHashCode();
|
||||
|
||||
public static LadderColumnSettings Default => new LadderColumnSettings {
|
||||
show = true,
|
||||
width = 50,
|
||||
@@ -74,14 +78,7 @@ namespace mbts.spark {
|
||||
other.show == show;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as LadderColumnSettings);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
width.GetHashCode() +
|
||||
background.GetHashCode() +
|
||||
foreground.GetHashCode() +
|
||||
font_family.GetHashCode() +
|
||||
font_size.GetHashCode() +
|
||||
font_weight.GetHashCode();
|
||||
public override int GetHashCode() => _type_hash;
|
||||
|
||||
public static bool operator ==(LadderColumnSettings x, LadderColumnSettings y) => x.safe_equals(y);
|
||||
public static bool operator !=(LadderColumnSettings x, LadderColumnSettings y) => !(x == y);
|
||||
@@ -89,6 +86,9 @@ namespace mbts.spark {
|
||||
|
||||
[ProtoContract]
|
||||
class HistogramBarAnimation : IEquatable<HistogramBarAnimation> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(HistogramBarAnimation).GetHashCode();
|
||||
[ProtoMember(1)]
|
||||
public ColorShowProto bars { get; set; }
|
||||
[ProtoMember(2)]
|
||||
@@ -102,14 +102,14 @@ namespace mbts.spark {
|
||||
show_numbers == other.show_numbers;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as HistogramBarAnimation);
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
bars.GetHashCode(),
|
||||
show_numbers.GetHashCode());
|
||||
public override int GetHashCode() => _type_hash;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class VolumeColumnSettings : IEquatable<VolumeColumnSettings> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(VolumeColumnSettings).GetHashCode();
|
||||
public static VolumeColumnSettings Default {
|
||||
get {
|
||||
var gen = LadderColumnSettings.Default;
|
||||
@@ -149,19 +149,14 @@ namespace mbts.spark {
|
||||
show_ltq.Equals(other.show_ltq);
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as VolumeColumnSettings);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
general.GetHashCode(),
|
||||
buy.GetHashCode(),
|
||||
sell.GetHashCode(),
|
||||
passive.GetHashCode(),
|
||||
total.GetHashCode(),
|
||||
show_ltq.GetHashCode());
|
||||
public override int GetHashCode() => _type_hash;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class LadderMarketBreakLine : IEquatable<LadderMarketBreakLine> {
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(LadderMarketBreakLine).GetHashCode();
|
||||
[ProtoMember(1)]
|
||||
public bool show { get; set; }
|
||||
[ProtoMember(2)]
|
||||
@@ -184,8 +179,7 @@ namespace mbts.spark {
|
||||
Equals(obj as LadderMarketBreakLine);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
show.GetHashCode() +
|
||||
(int)color;
|
||||
_type_hash;
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
@@ -255,6 +249,11 @@ namespace mbts.spark {
|
||||
const double _default_order_entry_panel_width = 110.0;
|
||||
const double _default_zoom_factor = 1.0;
|
||||
const bool _default_show_warn_on_cross = true;
|
||||
public static readonly IEqualityComparer<LadderSettings> profile_equality_comparer =
|
||||
EqualityComparer.Create<LadderSettings>(profile_equals);
|
||||
// this class is 100% mutable (there are literally 0 readonly members),
|
||||
// so GetHashCode must return the same hash for every instance.
|
||||
static readonly int _type_hash = typeof(LadderSettings).GetHashCode();
|
||||
|
||||
// Default proto constructor
|
||||
// Added this so that when clients upgrade to a version with new settings,
|
||||
@@ -497,6 +496,9 @@ namespace mbts.spark {
|
||||
[ProtoMember(55)]
|
||||
public LadderMiniOrderBookSettings mini_ob { get; set; }
|
||||
|
||||
[ProtoMember(57)]
|
||||
public double left_panel_scroll_position { get; set; }
|
||||
|
||||
public void sanitize() =>
|
||||
sanitize(this);
|
||||
|
||||
@@ -533,112 +535,81 @@ namespace mbts.spark {
|
||||
return s;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether or not the specified LadderSettings are 'profile equivalent.'
|
||||
/// This means that 2 ladders, each with a different one of the specified settings, could share the same profile.
|
||||
/// </summary>
|
||||
///
|
||||
/// Note: the impetus for this version of equality is to avoid disconnecting a ladder from its profile due to 'trivial' settings changes.
|
||||
/// Consider the '<see cref="default_algo"/>' property:
|
||||
/// If a ladder is using a settings profile with Limit as the default algo, and the user decides to send a 'Market' order,
|
||||
/// the ladder would suddenly be disconnected from the profile, because it's settings state would no longer be equal to the profile's settings,
|
||||
/// since they changed the selected algo type to 'Market.'
|
||||
/// To avoid such travesties, a separate equals logic is needed: this is called 'profile-equivalent' and is implemented in <see cref="profile_equals)"/>.
|
||||
/// Devs: be sure to account for properties that are excluded in the 'profile-equals' logic in the [regular]'equals' logic.
|
||||
public static bool profile_equals(LadderSettings a, LadderSettings b) =>
|
||||
a == null ? b == null :
|
||||
b != null &&
|
||||
a.row_height == b.row_height &&
|
||||
a.grid_line_color == b.grid_line_color &&
|
||||
a.orders_column.safe_equals(b.orders_column) &&
|
||||
a.bid_column.safe_equals(b.bid_column) &&
|
||||
a.ask_column.safe_equals(b.ask_column) &&
|
||||
a.price_column.safe_equals(b.price_column) &&
|
||||
a.last_column.safe_equals(b.last_column) &&
|
||||
a.piq_column.safe_equals(b.piq_column) &&
|
||||
a.calculator_buttons.safe_equals(b.calculator_buttons, Enumerable.SequenceEqual) &&
|
||||
a.scroll_bar_visible == b.scroll_bar_visible &&
|
||||
a.default_account == b.default_account &&
|
||||
a.default_time_in_force == b.default_time_in_force &&
|
||||
a.size == b.size &&
|
||||
a.animate_recenter == b.animate_recenter &&
|
||||
a.badge_style == b.badge_style &&
|
||||
a.auto_clear == b.auto_clear &&
|
||||
a.bid_break_line.safe_equals(b.bid_break_line) &&
|
||||
a.ask_break_line.safe_equals(b.ask_break_line) &&
|
||||
a.mid_break_line.safe_equals(b.mid_break_line) &&
|
||||
a.alternate_order_size == b.alternate_order_size &&
|
||||
a.session_low_line.safe_equals(b.session_low_line) &&
|
||||
a.session_high_line.safe_equals(b.session_high_line) &&
|
||||
a.session_range_fill.safe_equals(b.session_range_fill) &&
|
||||
a.volume_column.safe_equals(b.volume_column) &&
|
||||
a.size_display.safe_equals(b.size_display) &&
|
||||
a.day_visible == b.day_visible &&
|
||||
a.high_visible == b.high_visible &&
|
||||
a.low_visible == b.low_visible &&
|
||||
a.open_visible == b.open_visible &&
|
||||
a.settle_visible == b.settle_visible &&
|
||||
a.volume_visible == b.volume_visible &&
|
||||
a.account_visible == b.account_visible &&
|
||||
a.tif_visible == b.tif_visible &&
|
||||
a.order_type_visible == b.order_type_visible &&
|
||||
a.account_position_visible == b.account_position_visible &&
|
||||
a.day_position_visible == b.day_position_visible &&
|
||||
a.raw_day_position_visible == b.raw_day_position_visible &&
|
||||
a.size_presets_visible == b.size_presets_visible &&
|
||||
a.cancel_buys_visible == b.cancel_buys_visible &&
|
||||
a.cancel_sells_visible == b.cancel_sells_visible &&
|
||||
a.cancel_all_visible == b.cancel_all_visible &&
|
||||
a.auto_clear_visible == b.auto_clear_visible &&
|
||||
a.clear_visible == b.clear_visible &&
|
||||
a.order_entry_panel_initial_width == b.order_entry_panel_initial_width &&
|
||||
a.zoom_slider_visible == b.zoom_slider_visible &&
|
||||
a.zoom_factor == b.zoom_factor &&
|
||||
a.use_defaults == b.use_defaults &&
|
||||
a.favorite_algos == b.favorite_algos &&
|
||||
a.net_change == b.net_change &&
|
||||
a.show_warn_on_cross == b.show_warn_on_cross &&
|
||||
a.mini_ob == b.mini_ob;
|
||||
|
||||
public bool Equals(LadderSettings other) =>
|
||||
other != null &&
|
||||
row_height == other.row_height &&
|
||||
grid_line_color == other.grid_line_color &&
|
||||
orders_column.safe_equals(other.orders_column) &&
|
||||
bid_column.safe_equals(other.bid_column) &&
|
||||
ask_column.safe_equals(other.ask_column) &&
|
||||
price_column.safe_equals(other.price_column) &&
|
||||
last_column.safe_equals(other.last_column) &&
|
||||
piq_column.safe_equals(other.piq_column) &&
|
||||
calculator_buttons.safe_equals(other.calculator_buttons, (a, b) => a.SequenceEqual(b)) &&
|
||||
scroll_bar_visible == other.scroll_bar_visible &&
|
||||
default_account == other.default_account &&
|
||||
profile_equals(this, other) &&
|
||||
default_algo == other.default_algo &&
|
||||
default_time_in_force == other.default_time_in_force &&
|
||||
size == other.size &&
|
||||
animate_recenter == other.animate_recenter &&
|
||||
badge_style == other.badge_style &&
|
||||
auto_clear == other.auto_clear &&
|
||||
bid_break_line.safe_equals(other.bid_break_line) &&
|
||||
ask_break_line.safe_equals(other.ask_break_line) &&
|
||||
mid_break_line.safe_equals(other.mid_break_line) &&
|
||||
alternate_order_size == other.alternate_order_size &&
|
||||
session_low_line.safe_equals(other.session_low_line) &&
|
||||
session_high_line.safe_equals(other.session_high_line) &&
|
||||
session_range_fill.safe_equals(other.session_range_fill) &&
|
||||
volume_column.safe_equals(other.volume_column) &&
|
||||
size_display.safe_equals(other.size_display) &&
|
||||
day_visible == other.day_visible &&
|
||||
high_visible == other.high_visible &&
|
||||
low_visible == other.low_visible &&
|
||||
open_visible == other.open_visible &&
|
||||
settle_visible == other.settle_visible &&
|
||||
volume_visible == other.volume_visible &&
|
||||
account_visible == other.account_visible &&
|
||||
tif_visible == other.tif_visible &&
|
||||
order_type_visible == other.order_type_visible &&
|
||||
account_position_visible == other.account_position_visible &&
|
||||
day_position_visible == other.day_position_visible &&
|
||||
raw_day_position_visible == other.raw_day_position_visible &&
|
||||
size_presets_visible == other.size_presets_visible &&
|
||||
cancel_buys_visible == other.cancel_buys_visible &&
|
||||
cancel_sells_visible == other.cancel_sells_visible &&
|
||||
cancel_all_visible == other.cancel_all_visible &&
|
||||
auto_clear_visible == other.auto_clear_visible &&
|
||||
clear_visible == other.clear_visible &&
|
||||
order_entry_panel_initial_width == other.order_entry_panel_initial_width &&
|
||||
zoom_slider_visible == other.zoom_slider_visible &&
|
||||
zoom_factor == other.zoom_factor &&
|
||||
use_defaults == other.use_defaults &&
|
||||
favorite_algos == other.favorite_algos &&
|
||||
net_change == other.net_change &&
|
||||
show_warn_on_cross == other.show_warn_on_cross &&
|
||||
mini_ob == other.mini_ob;
|
||||
left_panel_scroll_position == other.left_panel_scroll_position;
|
||||
|
||||
public override bool Equals(object obj) => Equals(obj as LadderSettings);
|
||||
|
||||
// TODO: remove mutable props from hash logic (or conversely, make them immutable).
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.aggregate_hash(
|
||||
row_height.GetHashCode(),
|
||||
grid_line_color.GetHashCode(),
|
||||
EqualityComparer.safe_get_hash_code(orders_column),
|
||||
EqualityComparer.safe_get_hash_code(bid_column),
|
||||
EqualityComparer.safe_get_hash_code(ask_column),
|
||||
EqualityComparer.safe_get_hash_code(price_column),
|
||||
EqualityComparer.safe_get_hash_code(last_column),
|
||||
EqualityComparer.safe_get_hash_code(piq_column),
|
||||
EqualityComparer.safe_get_hash_code(calculator_buttons),
|
||||
scroll_bar_visible.GetHashCode(),
|
||||
default_time_in_force.GetHashCode(),
|
||||
size.GetHashCode(),
|
||||
animate_recenter.GetHashCode(),
|
||||
badge_style.GetHashCode(),
|
||||
EqualityComparer.safe_get_hash_code(bid_break_line),
|
||||
EqualityComparer.safe_get_hash_code(ask_break_line),
|
||||
EqualityComparer.safe_get_hash_code(mid_break_line),
|
||||
alternate_order_size.GetHashCode(),
|
||||
EqualityComparer.safe_get_hash_code(session_low_line),
|
||||
EqualityComparer.safe_get_hash_code(session_high_line),
|
||||
EqualityComparer.safe_get_hash_code(session_range_fill),
|
||||
EqualityComparer.safe_get_hash_code(volume_column),
|
||||
EqualityComparer.safe_get_hash_code(size_display),
|
||||
day_visible.GetHashCode(),
|
||||
high_visible.GetHashCode(),
|
||||
low_visible.GetHashCode(),
|
||||
open_visible.GetHashCode(),
|
||||
settle_visible.GetHashCode(),
|
||||
volume_visible.GetHashCode(),
|
||||
account_visible.GetHashCode(),
|
||||
tif_visible.GetHashCode(),
|
||||
order_type_visible.GetHashCode(),
|
||||
account_position_visible.GetHashCode(),
|
||||
size_presets_visible.GetHashCode(),
|
||||
cancel_buys_visible.GetHashCode(),
|
||||
cancel_sells_visible.GetHashCode(),
|
||||
cancel_all_visible.GetHashCode(),
|
||||
auto_clear_visible.GetHashCode(),
|
||||
clear_visible.GetHashCode(),
|
||||
order_entry_panel_initial_width.GetHashCode(),
|
||||
zoom_slider_visible.GetHashCode(),
|
||||
zoom_factor.GetHashCode(),
|
||||
use_defaults.GetHashCode(),
|
||||
favorite_algos.GetHashCode(),
|
||||
net_change.GetHashCode(),
|
||||
show_warn_on_cross.GetHashCode());
|
||||
public override int GetHashCode() => _type_hash;
|
||||
|
||||
public struct WindowSpecifics {
|
||||
public readonly double order_entry_panel_width, zoom_factor;
|
||||
|
||||
@@ -81,11 +81,9 @@ namespace bts.shell {
|
||||
if (!_settings_hub.current.ladder_profiles.TryGetFirst(lp => lp.id == id, out var result)) {
|
||||
// profile was deleted
|
||||
id = 0;
|
||||
} else {
|
||||
if (!s.Equals(result.value)) {
|
||||
// ladder's settings diverged from the profile (this happens when the user changes settings outside of the settings dialog, such as column widths, left panel width, etc.)
|
||||
id = 0;
|
||||
}
|
||||
} else if (!LadderSettings.profile_equals(s, result.value)) {
|
||||
// ladder's settings diverged from the profile (this happens when the user changes settings outside of the settings dialog, such as column widths, left panel width, etc.)
|
||||
id = 0;
|
||||
}
|
||||
}
|
||||
return new LadderSaveState { security_key = vm.security.Id, settings = s, settings_id = id };
|
||||
@@ -149,7 +147,7 @@ namespace bts.shell {
|
||||
var curr = vm.get_save_state();
|
||||
if (settings_id == 0) {
|
||||
last_value = curr;
|
||||
} else if (!curr.Equals(last_value)) {
|
||||
} else if (!LadderSettings.profile_equals(curr, last_value)) {
|
||||
settings_id = 0;
|
||||
last_value = curr;
|
||||
}
|
||||
@@ -163,7 +161,7 @@ namespace bts.shell {
|
||||
// not on a profile, don't respond to default settings changes
|
||||
return;
|
||||
} else if (us.ladder_profiles.TryGetFirst(p => p.id == settings_id, out var profile)) {
|
||||
if (!last_value.Equals(vm.get_save_state())) {
|
||||
if (!LadderSettings.profile_equals(last_value, vm.get_save_state())) {
|
||||
settings_id = 0;
|
||||
} else {
|
||||
settings_subject.OnNext(last_value = profile.value);
|
||||
@@ -211,6 +209,7 @@ namespace bts.shell {
|
||||
_ => _,
|
||||
vm.get_save_state(),
|
||||
UserSettings.FileExtensions.LADDER,
|
||||
LadderSettings.profile_equals,
|
||||
settings_id);
|
||||
dialog_vm.applied += apply;
|
||||
// dialog
|
||||
|
||||
@@ -35,7 +35,14 @@ namespace mbts.spark {
|
||||
_show_cancel_sells, _show_cancel_all, _show_zoom;
|
||||
readonly PropertyAutoWrapper<LadderSettings, Brush> _bid_background, _bid_foreground, _ask_background, _ask_foreground;
|
||||
readonly HashSet<AlgoKey> _sticky_algos = new HashSet<AlgoKey>();
|
||||
double _position, _day_position, _raw_day_position;
|
||||
double
|
||||
_position,
|
||||
_day_position,
|
||||
_raw_day_position,
|
||||
_left_panel_extent_height,
|
||||
_left_panel_view_port_height,
|
||||
_left_panel_scroll_position,
|
||||
_left_panel_scroll_position_target;
|
||||
bool _show_account_position, _show_day_position, _show_raw_day_position;
|
||||
ulong _misc_time = 0;
|
||||
ulong _last_trade_time = 0;
|
||||
@@ -76,6 +83,7 @@ namespace mbts.spark {
|
||||
_volume_column_width = backing(nameof(volume_column_width), ws.volume_column_width);
|
||||
_left_panel_width = backing(nameof(left_panel_width), ws.order_entry_panel_width);
|
||||
_zoom_factor = backing(nameof(zoom_factor), ws.zoom_factor);
|
||||
_left_panel_scroll_position_target = init.left_panel_scroll_position;
|
||||
// remember: account_positions needs to subscribe to selected_account; both the sub and account_positions need to be disposed
|
||||
_volume = backing(nameof(volume), 0ul);
|
||||
_auto_clear = backing(nameof(auto_clear), init.auto_clear);
|
||||
@@ -333,6 +341,33 @@ namespace mbts.spark {
|
||||
public bool show_cancel_sells => _show_cancel_sells.view;
|
||||
public bool show_cancel_all => _show_cancel_all.view;
|
||||
public bool show_zoom => _show_zoom.view;
|
||||
// left panel scroll props
|
||||
public double left_panel_view_port_height {
|
||||
get => _left_panel_view_port_height;
|
||||
set {
|
||||
using (detect_property_changes(nameof(left_panel_scroll_position)))
|
||||
_left_panel_view_port_height = value;
|
||||
}
|
||||
}
|
||||
public double left_panel_scroll_position {
|
||||
get {
|
||||
if (!_left_panel_scroll_position_target.is_nan()) {
|
||||
if (_left_panel_scroll_position_target < _max_left_panel_scroll_position) {
|
||||
_left_panel_scroll_position = _left_panel_scroll_position_target;
|
||||
_left_panel_scroll_position_target = double.NaN;
|
||||
}
|
||||
}
|
||||
return Math.Min(_left_panel_scroll_position, _left_panel_extent_height);
|
||||
}
|
||||
set => Set(ref _left_panel_scroll_position, value);
|
||||
}
|
||||
public double left_panel_extent_height {
|
||||
get => _left_panel_extent_height;
|
||||
set {
|
||||
using (detect_property_changes(nameof(left_panel_scroll_position)))
|
||||
Set(ref _left_panel_extent_height, value);
|
||||
}
|
||||
}
|
||||
public double left_panel_width { get => _left_panel_width.Value; set => _left_panel_width.set(value); }
|
||||
public double zoom_factor { get => _zoom_factor.Value; set => _zoom_factor.set(value); }
|
||||
public string position_display_text {
|
||||
@@ -364,6 +399,10 @@ namespace mbts.spark {
|
||||
public bool show_warn_on_cross_from_settings => current_settings.show_warn_on_cross;
|
||||
internal LadderModel model => _model;
|
||||
internal IObservable<LadderSettings> settings { get; }
|
||||
|
||||
double _max_left_panel_scroll_position =>
|
||||
Math.Max(0, _left_panel_extent_height - _left_panel_view_port_height);
|
||||
|
||||
internal OrderRecordImmutable get_root_at_price((OrderRecordImmutable order, uint? _, AlgoData? __, AlgoDefinitionMetadata? meta) op) {
|
||||
var order = op.order;
|
||||
double p = order.price();
|
||||
@@ -394,6 +433,7 @@ namespace mbts.spark {
|
||||
zoom_factor,
|
||||
piq_column_width));
|
||||
rv.auto_clear = auto_clear;
|
||||
rv.left_panel_scroll_position = left_panel_scroll_position;
|
||||
if (order_properties_vm != null) {
|
||||
rv.default_time_in_force = order_properties_vm.selected_time_in_force;
|
||||
if (order_properties_vm.selected_account.HasValue)
|
||||
|
||||
@@ -301,7 +301,14 @@
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="0" Grid.RowSpan="2" VerticalScrollBarVisibility="Hidden">
|
||||
<ScrollViewer
|
||||
x:Name="_left_panel_scroll"
|
||||
Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
mbts:ScrollViewerBehavior.VerticalOffset="{Binding left_panel_scroll_position, Mode=TwoWay}"
|
||||
mbts:ScrollViewerBehavior.EnableTwoWayVerticalOffsetBinding="True"
|
||||
VerticalScrollBarVisibility="Hidden"
|
||||
>
|
||||
<Grid RowSpan="2" Background="{mbts:BTSColor UI.spark_menu_background}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*"/>
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
using bts;
|
||||
using bts.collections;
|
||||
using bts.utils;
|
||||
using mbts.ui;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.extensions;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Input;
|
||||
|
||||
@@ -80,6 +86,8 @@ namespace mbts.spark {
|
||||
_ladder.visible_columns_changed += () => _column_headers_presenter.Columns.SetItems(
|
||||
_ladder.visible_column_types.Select(t => ColumnFactory.create(_ladder, t)));
|
||||
this.vm = vm;
|
||||
this.ConnectDataContext<LadderVM>(lvm => lvm.bus.subscribe(SparkTags.SecuritiesTag).OnDispatcher().Subscribe(sec_map => Resources[ResourceKeys.Securities] = sec_map));
|
||||
vm.SubscribeDisposed(_connect_left_panel_scroll());
|
||||
DataContext = vm;
|
||||
DataContextChanged += datacontext_changed;
|
||||
_ui_scaler.MouseDoubleClick += _ui_scaler_MouseDoubleClick;
|
||||
@@ -87,7 +95,39 @@ namespace mbts.spark {
|
||||
this.PreviewMouseDown += LadderView_PreviewMouseDown;
|
||||
}
|
||||
|
||||
|
||||
IDisposable _connect_left_panel_scroll() {
|
||||
// extent height
|
||||
bool busy = false;
|
||||
var extent_height_hook = _left_panel_scroll.ObserveProperty(ScrollViewer.ExtentHeightProperty, (s, e) => {
|
||||
if (!busy) {
|
||||
double scroll_pos = vm.left_panel_scroll_position;
|
||||
busy = true;
|
||||
Dispatcher.InvokeAsync(() => {
|
||||
vm.left_panel_extent_height = _left_panel_scroll.ExtentHeight;
|
||||
if (_left_panel_scroll.VerticalOffset != scroll_pos)
|
||||
_left_panel_scroll.TryScrollToVerticalOffset(scroll_pos);
|
||||
busy = false;
|
||||
}, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
});
|
||||
// view port height
|
||||
_left_panel_scroll.Loaded += on_loaded;
|
||||
var dispose = Disposable.CreateDisposeOnce(dispose_impl);
|
||||
return Disposable.Create(dispose, vm.SubscribeDisposed(dispose));
|
||||
// local methods
|
||||
void on_loaded(object sender, RoutedEventArgs e) {
|
||||
if (_left_panel_scroll.descendants_of_type<ScrollBar>().TryGetFirst(sb => sb.Orientation == Orientation.Vertical, out var vscroll)) {
|
||||
vscroll.SetBinding(ScrollBar.ViewportSizeProperty, new Binding(nameof(LadderVM.left_panel_view_port_height)) {
|
||||
Converter = ValueConverter.create<double, Size>(d => new Size(0, d), sz => sz.Height),
|
||||
Mode = BindingMode.OneWayToSource
|
||||
});
|
||||
}
|
||||
}
|
||||
void dispose_impl() {
|
||||
Disposable.dispose(ref extent_height_hook);
|
||||
_left_panel_scroll.Loaded -= on_loaded;
|
||||
}
|
||||
}
|
||||
|
||||
// (Left|Right) CTRL + Wheel to zoom
|
||||
// This is neat b/c the user doesn't even need to have the slider visible to zoom
|
||||
|
||||
@@ -62,15 +62,15 @@
|
||||
Header="{Binding security}"
|
||||
IsEditMode="False"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.SecuritiesKey}}"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurity="{Binding security, UpdateSourceTrigger=Explicit}"
|
||||
ShowDialogSkirt="True"
|
||||
ToolTip="{Binding SelectedCriteria, RelativeSource={RelativeSource Self}, Converter={x:Static c:ContractPickerPopup.SecurityCriteriaTextConverter}, Mode=OneWay}"
|
||||
OKClick="{x:Static c:ContractPickerPopup.FlushSelectedSecurityHandler}"
|
||||
/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.AccountsKey}}" SelectedItem="{Binding account}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.UsersKey}}" SelectedItem="{Binding user}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static usp:ManualFillRowEditor.SidesKey}}" SelectedItem="{Binding side}" Width="48"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="2" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Accounts}}" SelectedItem="{Binding account}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="4" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Users}}" SelectedItem="{Binding user}"/>
|
||||
<ComboBox Grid.Row="2" Grid.Column="6" ItemsSource="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Sides}}" SelectedItem="{Binding side}" Width="48"/>
|
||||
<c:TickUpDownUInt Grid.Row="2" Grid.Column="8" MinWidth="40" Value="{Binding size}"/>
|
||||
<usp:PriceEntryControl Grid.Row="2" Grid.Column="10" MinWidth="55" Value="{Binding price}" ContractSpec="{Binding spec}"/>
|
||||
</Grid>
|
||||
|
||||
@@ -6,12 +6,6 @@ namespace mbts.ui.spark {
|
||||
/// Interaction logic for ManualFillRowEditor.xaml
|
||||
/// </summary>
|
||||
partial class ManualFillRowEditor : UserControl {
|
||||
public const string
|
||||
SecuritiesKey = nameof(SecuritiesKey),
|
||||
AccountsKey = nameof(AccountsKey),
|
||||
UsersKey = nameof(UsersKey),
|
||||
SidesKey = nameof(SidesKey);
|
||||
|
||||
public static readonly RoutedCommand
|
||||
CommitCommand = new RoutedCommand(),
|
||||
CancelCommand = new RoutedCommand();
|
||||
|
||||
@@ -50,11 +50,11 @@ namespace mbts.ui.spark {
|
||||
row_editor.CommandBindings.Add(new CommandBinding(ManualFillRowEditor.CommitCommand, commit_executed));
|
||||
row_editor.CommandBindings.Add(new CommandBinding(ManualFillRowEditor.CancelCommand, cancel_executed));
|
||||
// dynamic resources
|
||||
row_editor.Resources[ManualFillRowEditor.AccountsKey] = vm.accounts;
|
||||
row_editor.Resources[ManualFillRowEditor.UsersKey] = vm.users;
|
||||
row_editor.Resources[ManualFillRowEditor.SidesKey] = vm.sides;
|
||||
row_editor.Resources[ResourceKeys.Accounts] = vm.accounts;
|
||||
row_editor.Resources[ResourceKeys.Users] = vm.users;
|
||||
row_editor.Resources[ResourceKeys.Sides] = vm.sides;
|
||||
FocusManager.SetIsFocusScope(row_editor, true);
|
||||
var sec_hook = vm.as_demuxer().subscribe(nameof(ManualFillsVM.security_map)).OnDispatcher().Subscribe(v => row_editor.Resources[ManualFillRowEditor.SecuritiesKey] = v);
|
||||
var sec_hook = vm.as_demuxer().subscribe(nameof(ManualFillsVM.security_map)).OnDispatcher().Subscribe(v => row_editor.Resources[ResourceKeys.Securities] = v);
|
||||
// popup adorner
|
||||
var popup_adorner = new PopupAdorner(
|
||||
data_grid, data_grid,
|
||||
|
||||
@@ -108,7 +108,7 @@ namespace mbts.spark {
|
||||
public CustomOrderDefinition.CustomOneofCase root_algo_order_type => order_tuple.root.algo.value.CustomCase;
|
||||
public OrderMetadata root_algo_order_metadata => _owner.get_metadata(root_algo_order_type);
|
||||
public Brush root_algo_badge_fill => _root_algo_badge_fill ?? (_root_algo_badge_fill = new SolidColorBrush(_root_algo_badge_color));
|
||||
public string root_algo_display_name => order_tuple.root.algo.name;
|
||||
public string root_algo_display_name => order_tuple.root.display_name();
|
||||
|
||||
// Tree
|
||||
public bool is_parent { get => _is_parent; set => Set(ref _is_parent, value); }
|
||||
|
||||
@@ -66,6 +66,10 @@ namespace mbts.spark {
|
||||
_vm = new OrderBookVM(_bus, _notification_service, _model, _settings_manager);
|
||||
_vm.load_from(_state);
|
||||
_view = new OrderBookView(_vm);
|
||||
_vm.SubscribeDisposed(
|
||||
_bus.subscribe(SparkTags.SecuritiesTag)
|
||||
.OnDispatcher()
|
||||
.Subscribe(sec_map => _view.Resources[ResourceKeys.Securities] = sec_map));
|
||||
|
||||
var show_fills_command = SparkCommands.ShowFills(SparkCommands.KeyGestures.ShowFills).complete(() => new FillsSettings(_vm.selected_orders.Select(r => r.Key).ToArray()));
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui;
|
||||
using mbts.ui.shell;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
@@ -6,8 +7,9 @@ using System.Reactive.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace mbts.spark {
|
||||
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged {
|
||||
class AlgoParamVM<T> : IAlgoParamVM<T>, INotifyPropertyChanged, ITemplateSelectorKey {
|
||||
readonly IEqualityComparer<T> _comparer;
|
||||
readonly string _template_key;
|
||||
AlgoParamPermissions _permissions;
|
||||
bool _is_enabled, _has_value;
|
||||
T _committed_value, _value;
|
||||
@@ -15,15 +17,16 @@ namespace mbts.spark {
|
||||
public AlgoParamVM(string id, string display_name)
|
||||
: this(id, display_name, default, EqualityComparer<T>.Default) { }
|
||||
|
||||
public AlgoParamVM(string id, string display_name, T value)
|
||||
: this(id, display_name, value, EqualityComparer<T>.Default) { }
|
||||
public AlgoParamVM(string id, string display_name, T value, string template_key = null)
|
||||
: this(id, display_name, value, EqualityComparer<T>.Default, template_key) { }
|
||||
|
||||
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer) {
|
||||
public AlgoParamVM(string id, string display_name, T value, IEqualityComparer<T> comparer, string template_key = null) {
|
||||
this.id = id;
|
||||
this.display_name = display_name;
|
||||
_value = value;
|
||||
_committed_value = value;
|
||||
_comparer = comparer;
|
||||
_template_key = template_key;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
@@ -50,7 +53,9 @@ namespace mbts.spark {
|
||||
public bool is_enabled { get => _is_enabled; set => _set(ref _is_enabled, value); }
|
||||
public bool has_value { get => _has_value; set => _set(ref _has_value, value); }
|
||||
public bool is_dirty { get; private set; }
|
||||
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
|
||||
object IAlgoParamVM.value { get => value; set => this.value = (T)value; }
|
||||
|
||||
string ITemplateSelectorKey.key => _template_key;
|
||||
|
||||
public void revert() {
|
||||
if (is_dirty) {
|
||||
|
||||
@@ -36,6 +36,8 @@ namespace mbts.spark {
|
||||
public AlgoDefinitionMetadata? root_algo_meta =>
|
||||
_order_tuple.HasValue ? _order_tuple.Value.root.meta : null;
|
||||
|
||||
public OrderSecurityAlgo? root_order_security => _order_tuple?.root;
|
||||
|
||||
public string account {
|
||||
get {
|
||||
var account_string = string.Empty;
|
||||
@@ -85,4 +87,4 @@ namespace mbts.spark {
|
||||
public static string user(this FillTuple fill_tuple) =>
|
||||
fill_tuple.order.HasValue ? fill_tuple.order.Value.user() : string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,7 @@ namespace mbts.spark {
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: param.AsInt32 = 0; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveOrderDef: param.AsOrderDef = new CustomOrderDefinition(); break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: param.AsString = string.Empty; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey:
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: param.AsUint32 = 0u; break;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: param.AsQuantity = new bts.spark.QuantityMeasure(); break;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ using System.Reactive.Linq;
|
||||
namespace mbts.spark {
|
||||
interface IDynamicOrderParamVM {
|
||||
DynamicOrderParamDescriptor descriptor { get; }
|
||||
DynamicOrderParam param { get; set; }
|
||||
DynamicOrderParam param { get; }
|
||||
}
|
||||
|
||||
interface IDynamicOrderParamVM<T> : IDynamicOrderParamVM {
|
||||
@@ -21,26 +21,16 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
abstract class DynamicOrderParamVM : DisposableViewModelBase, IDynamicOrderParamVM {
|
||||
DynamicOrderParam _param;
|
||||
|
||||
protected DynamicOrderParamVM(DynamicOrderParamDescriptor descriptor)
|
||||
: this(descriptor, DynamicOrderHelper.init_param(descriptor)) { }
|
||||
|
||||
private DynamicOrderParamVM(DynamicOrderParamDescriptor descriptor, DynamicOrderParam param) {
|
||||
this.descriptor = descriptor;
|
||||
_param = param;
|
||||
this.param = param;
|
||||
}
|
||||
|
||||
public DynamicOrderParamDescriptor descriptor { get; set; }
|
||||
public DynamicOrderParam param {
|
||||
get => _param;
|
||||
set {
|
||||
_param = value;
|
||||
on_param_changed();
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract void on_param_changed();
|
||||
public DynamicOrderParam param { get; }
|
||||
}
|
||||
|
||||
class DynamicOrderParamVM<TView, TModel> : DynamicOrderParamVM, IDynamicOrderParamVM<TView> {
|
||||
@@ -53,14 +43,12 @@ namespace mbts.spark {
|
||||
_field_desc = field_descriptor;
|
||||
_convert = convert;
|
||||
_convert_back = convert_back;
|
||||
value = _convert(ConvertUtils.try_convert<TModel>(_field_desc.Accessor.GetValue(param)));
|
||||
}
|
||||
|
||||
public TView value {
|
||||
get => _value;
|
||||
set {
|
||||
_field_desc.Accessor.SetValue(param, _convert_back(_value = value));
|
||||
RaisePropertyChanged();
|
||||
}
|
||||
set => Set(ref _value, value);
|
||||
}
|
||||
|
||||
public IObservable<TView> subscribe_value() =>
|
||||
@@ -70,11 +58,6 @@ namespace mbts.spark {
|
||||
void on_prop_changed(object sender, PropertyChangedEventArgs e) =>
|
||||
o.OnNext(value);
|
||||
});
|
||||
|
||||
protected override void on_param_changed() {
|
||||
_value = _convert(ConvertUtils.try_convert<TModel>(_field_desc.Accessor.GetValue(param)));
|
||||
RaisePropertyChanged(nameof(value));
|
||||
}
|
||||
}
|
||||
|
||||
class DynamicOrderEnumeratedVM : DynamicOrderParamVM, IDynamicOrderParamVM<string> {
|
||||
@@ -105,11 +88,6 @@ namespace mbts.spark {
|
||||
o.OnNext(selected_element);
|
||||
});
|
||||
|
||||
protected override void on_param_changed() {
|
||||
_sync_elements();
|
||||
RaisePropertyChanged(nameof(selected_element));
|
||||
}
|
||||
|
||||
static IEnumerable<string> _get_elements(DynamicOrderParamDescriptor descriptor) =>
|
||||
descriptor.ParamType?.DescriptorCase == DynamicOrderTypeDescriptor.DescriptorOneofCase.Enumerated ?
|
||||
descriptor.ParamType.Enumerated.Element : Enumerable.Empty<string>();
|
||||
@@ -148,10 +126,5 @@ namespace mbts.spark {
|
||||
o.OnNext(quantity);
|
||||
});
|
||||
}
|
||||
|
||||
protected override void on_param_changed()
|
||||
{
|
||||
RaisePropertyChanged(nameof(quantity));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ namespace mbts.ui.spark
|
||||
public DataTemplate StringTemplate { get; set; }
|
||||
public DataTemplate OrderDefTemplate { get; set; }
|
||||
public DataTemplate QuantityMeasureTemplate { get; set; }
|
||||
public DataTemplate SecurityKeyTemplate { get; set; }
|
||||
|
||||
public DataTemplate EnumTemplate { get; set; }
|
||||
|
||||
@@ -38,6 +39,7 @@ namespace mbts.ui.spark
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveDouble: return DoubleTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: return IntTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: return UIntTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey: return SecurityKeyTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: return StringTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveOrderDef: return OrderDefTemplate;
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: return QuantityMeasureTemplate;
|
||||
|
||||
@@ -4,6 +4,7 @@ using bts.collections;
|
||||
using bts.spark;
|
||||
using mbts.collections;
|
||||
using mbts.spark.utilities;
|
||||
using mbts.ui;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using System;
|
||||
@@ -73,9 +74,9 @@ namespace mbts.spark {
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveDouble: return mk<double, double>(DynamicOrderParam.AsDoubleFieldNumber, v => v, v => v, double.NaN);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveInt: return mk<int?, int>(DynamicOrderParam.AsInt32FieldNumber, v => v, v => v.GetValueOrDefault());
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveUint: return mk<uint?, uint>(DynamicOrderParam.AsUint32FieldNumber, v => v, v => v.GetValueOrDefault());
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveSecurityKey: return mk<uint?, uint>(DynamicOrderParam.AsUint32FieldNumber, v => v, v => v.GetValueOrDefault(), template_key: TemplateKeys.SecurityKey);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveString: return mk<string, string>(DynamicOrderParam.AsStringFieldNumber, v => v, v => v);
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity:
|
||||
{
|
||||
case DynamicOrderPrimitiveDescriptor.DynamicOrderPrimitiveQuantity: {
|
||||
var qm = new DynamicOrderQuantityVM(descriptor);
|
||||
return attached(qm, DynamicOrderHelper.get_quantity_value, DynamicOrderHelper.set_quantity_value, new QuantityMeasure { Ratio = 1.0, Absolute = 1 });
|
||||
}
|
||||
@@ -88,16 +89,16 @@ namespace mbts.spark {
|
||||
default: throw ExceptionBuilder.Argument.unexpected_case(descriptor.ParamType.DescriptorCase, nameof(descriptor));
|
||||
}
|
||||
|
||||
DynamicOrderParamVM<TView, TModel> mk<TView, TModel>(int field_number, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default) {
|
||||
DynamicOrderParamVM<TView, TModel> mk<TView, TModel>(int field_number, Func<TModel, TView> convert, Func<TView, TModel> convert_back, TView default_value = default, string template_key = null) {
|
||||
var fdesc = DynamicOrderParam.Descriptor.FindFieldByNumber(field_number);
|
||||
var pvm = new DynamicOrderParamVM<TView, TModel>(descriptor, fdesc, convert, convert_back);
|
||||
return attached<TView, DynamicOrderParamVM<TView, TModel>>(pvm, get_from_param, assign_to_param, default_value);
|
||||
return attached<TView, DynamicOrderParamVM<TView, TModel>>(pvm, get_from_param, assign_to_param, default_value, template_key);
|
||||
|
||||
TView get_from_param(DynamicOrderParam param) => convert(ConvertUtils.try_convert<TModel>(fdesc.Accessor.GetValue(param)));
|
||||
void assign_to_param(DynamicOrderParam param, TView value) => fdesc.Accessor.SetValue(param, convert_back(value));
|
||||
}
|
||||
|
||||
TParam attached<T, TParam>(TParam pvm, Func<DynamicOrderParam, T> selector, Action<DynamicOrderParam, T> assign_to_param, T default_value = default) where TParam : DynamicOrderParamVM, IDynamicOrderParamVM<T> {
|
||||
TParam attached<T, TParam>(TParam pvm, Func<DynamicOrderParam, T> selector, Action<DynamicOrderParam, T> assign_to_param, T default_value = default, string template_key = null) where TParam : DynamicOrderParamVM, IDynamicOrderParamVM<T> {
|
||||
add_property<T>(
|
||||
pvm.descriptor.ParamName,
|
||||
mdef => mdef.OrderParams.TryGetValue(pvm.descriptor.ParamName, out DynamicOrderParam p) ? selector(p) : default_value,
|
||||
@@ -105,7 +106,8 @@ namespace mbts.spark {
|
||||
() => pvm.value,
|
||||
v => pvm.value = v,
|
||||
pvm.subscribe_value(),
|
||||
default_value);
|
||||
default_value,
|
||||
template_key: template_key);
|
||||
return pvm;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +89,7 @@ namespace mbts.spark {
|
||||
case AlgoType.Dynamic: vm = dynamic(key, name, name, editing_permissions); break;
|
||||
case AlgoType.Splitter: vm = splitter(key,name,editing_permissions); break;
|
||||
case AlgoType.Composite: vm = composite(key, name, editing_permissions); break;
|
||||
case AlgoType.None: vm = new DefaultAlgoVM(); break;
|
||||
default: vm = generic(key, name, algo_type, editing_permissions); break;
|
||||
//default:
|
||||
// Log.Ui.debug_fatal($"Failed to create {typeof(IAlgoVM)}: unknown algo type '{algo_type}.'");
|
||||
@@ -166,7 +167,8 @@ namespace mbts.spark {
|
||||
};
|
||||
void subscribe_new_algo(IAlgoVM old_algo, IAlgoVM new_algo) {
|
||||
RaisePropertyChanged(sd.prop_name);
|
||||
Disposable.replace(ref def_sub, new CompositeDisposable());
|
||||
Disposable.dispose(ref def_sub);
|
||||
def_sub = new CompositeDisposable();
|
||||
if (assert(new_algo != null, sd.prop_name, "No algo specified.")) {
|
||||
def_sub.Add(new_algo.subscribe_definition().Subscribe(_ => _def_change_notifier.execute()));
|
||||
if (new_algo is INotifyDataErrorInfo ndei)
|
||||
@@ -255,14 +257,14 @@ namespace mbts.spark {
|
||||
_security_changed += o.OnNext;
|
||||
return Disposable.Create(() => _security_changed -= o.OnNext);
|
||||
});
|
||||
Disposable.replace(ref _pending_load, Disposable.Create(
|
||||
Disposable.dispose(ref _pending_load);
|
||||
_pending_load = Disposable.Create(
|
||||
// load children
|
||||
ObservableAutoWrapper.connect_item_hooks(
|
||||
_child_algo_selector_data.AsCollection(),
|
||||
sd => security_src.SubscribeEach(_ => ObservableAutoWrapper.connect_item_actions(_child_algo_selector_data.AsCollection(), selector_load_impl))),
|
||||
// load regular props
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load(algom))));
|
||||
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load(algom)));
|
||||
}
|
||||
|
||||
void selector_load_impl(AlgoSelectorData sd) {
|
||||
@@ -298,11 +300,12 @@ namespace mbts.spark {
|
||||
_security_changed += o.OnNext;
|
||||
return Disposable.Create(() => _security_changed -= o.OnNext);
|
||||
});
|
||||
Disposable.replace(ref _pending_load, Disposable.Create(
|
||||
Disposable.dispose(ref _pending_load);
|
||||
_pending_load = Disposable.Create(
|
||||
ObservableAutoWrapper.connect_item_hooks(
|
||||
_child_algo_selector_data.AsCollection(),
|
||||
sd => security_src.Subscribe(_ => ObservableAutoWrapper.connect_item_actions(_child_algo_selector_data.AsCollection(), selector_load_impl))),
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load_many(algos))));
|
||||
ObservableAutoWrapper.connect_item_actions(_auto_props.AsCollection(), p => p.load_many(algos)));
|
||||
|
||||
void selector_load_impl(AlgoSelectorData sd) {
|
||||
// **assume that algos has more than 1 element
|
||||
@@ -542,8 +545,19 @@ namespace mbts.spark {
|
||||
}
|
||||
}
|
||||
|
||||
protected void add_property<TValue>(string prop_name, Func<TMutable, TValue> get_from_def, Action<TMutable, TValue> assign_to_def, Func<TValue> get_local, Action<TValue> set_local, IObservable<TValue> subscribe_local, TValue non_uniform_value = default, AlgoParamPermissions permissions = AlgoParamPermissions.ReadWrite) {
|
||||
var param = new AlgoParamVM<TValue>(prop_name, DisplayNameHelper.snake_to_title_case(prop_name), get_local()) { permissions = permissions };
|
||||
protected void add_property<TValue>(
|
||||
string prop_name,
|
||||
Func<TMutable, TValue> get_from_def,
|
||||
Action<TMutable, TValue> assign_to_def,
|
||||
Func<TValue> get_local,
|
||||
Action<TValue> set_local,
|
||||
IObservable<TValue> subscribe_local,
|
||||
TValue non_uniform_value = default,
|
||||
AlgoParamPermissions permissions = AlgoParamPermissions.ReadWrite,
|
||||
string template_key = null) {
|
||||
var param = new AlgoParamVM<TValue>(prop_name, DisplayNameHelper.snake_to_title_case(prop_name), get_local(), template_key) {
|
||||
permissions = permissions
|
||||
};
|
||||
_algo_params.Add(param);
|
||||
_auto_props[prop_name] = new AutoProp(prop_name,
|
||||
def => {
|
||||
@@ -816,12 +830,11 @@ namespace mbts.spark {
|
||||
var default_algo = AlgoKey.from_known_type(OrderDataHelper.get_default_algo(OrderDataHelper.get_algo_format(security.Value.ContractCase)));
|
||||
foreach (var sd in _child_algo_selectors)
|
||||
sd.load_algo_data_recursive(algo_data, default_algo);
|
||||
Disposable.replace(
|
||||
ref _selector_data_hooks,
|
||||
ObservableAutoWrapper.connect_item_hooks(child_algo_selectors, s => {
|
||||
s.load_algo_data_recursive(algo_data, default_algo);
|
||||
return Disposable.empty;
|
||||
}));
|
||||
Disposable.dispose(ref _selector_data_hooks);
|
||||
_selector_data_hooks = ObservableAutoWrapper.connect_item_hooks(child_algo_selectors, s => {
|
||||
s.load_algo_data_recursive(algo_data, default_algo);
|
||||
return Disposable.empty;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,9 @@ namespace mbts.spark {
|
||||
public static string display_name(this OrderSecurity order_tuple) =>
|
||||
display_name(order_tuple.order, order_tuple.algo);
|
||||
|
||||
public static string display_name(OrderRecordImmutable order, AlgoData? algo) {
|
||||
public static string display_name(OrderRecordImmutable order, AlgoData? algo, SecurityImmutable? security = null) {
|
||||
if (order.order_type() == CustomOrderDefinition.CustomOneofCase.Spreader && security.HasValue)
|
||||
return security.Value.ToString();
|
||||
if (algo.HasValue) {
|
||||
return algo.Value.name;
|
||||
} else if (order.HasDef && order.Def.HasCustomDef) {
|
||||
@@ -250,7 +252,7 @@ namespace mbts.spark {
|
||||
order_type.ToString().ToUpper();
|
||||
|
||||
public static string display_name(this OrderSecurityAlgo order_sec_algo) =>
|
||||
display_name(order_sec_algo.order, order_sec_algo.algo);
|
||||
display_name(order_sec_algo.order, order_sec_algo.algo, order_sec_algo.security);
|
||||
|
||||
public static string display_name(this CustomOrderDefinitionImmutable algo) =>
|
||||
algo.CustomCase == CustomOrderDefinition.CustomOneofCase.Dynamic ? algo.Dynamic.DynamicOrderType : algo.CustomCase.display_name();
|
||||
|
||||
@@ -210,6 +210,7 @@ namespace mbts.spark {
|
||||
// manual fills
|
||||
on_manual_fills_securities = _position_service.subscribei_manual_fills()
|
||||
.Naggle(interval, scheduler)
|
||||
.Filter(f => f.FillType == ManualFill.Types.FillType.Local)
|
||||
.join(sstore, mf => mf.SecurityKey)
|
||||
.Table().Cache();
|
||||
var manual_fill_tuples_account_join = on_manual_fills_securities.LeftJoin(
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
using bts.api;
|
||||
using bts.api.orderman;
|
||||
using bts.shell;
|
||||
using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.ui.settings;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
|
||||
namespace mbts {
|
||||
static class OrderSentEventPredicateVMFactory {
|
||||
public static Dictionary<OrderSentEventPredicate.PredicateTypeOneofCase, ITemplatedPropertyWrapperVM<OrderSentEventPredicate>> get_oneofs(IBus bus) =>
|
||||
new Dictionary<OrderSentEventPredicate.PredicateTypeOneofCase, ITemplatedPropertyWrapperVM<OrderSentEventPredicate>> {
|
||||
{ OrderSentEventPredicate.PredicateTypeOneofCase.AnyOrder, PredicateVMWrapper.create<OrderSentEventPredicate>(p => p.security = null) },
|
||||
{ OrderSentEventPredicate.PredicateTypeOneofCase.SecurityOrder, SecurityCriteriaVMFactory.create<OrderSentEventPredicate>(bus, o => o.security?.is_value ?? new SecurityCriteria(),
|
||||
(p, s) => {
|
||||
if (p.security == null) {
|
||||
p.security = new SecurityPredicate();
|
||||
}
|
||||
p.security.is_value = s;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
[ProtoContract]
|
||||
class OrderSentEventPredicate : IEquatable<OrderSentEventPredicate> {
|
||||
static readonly int _type_hash = typeof(OrderSentEventPredicate).GetHashCode();
|
||||
// arbitrary short time period (can override in config)
|
||||
static readonly TimeSpan _stale_order_threshold = TimeSpan.FromSeconds(config.Configuration.get<bts.config.OrderAlerts>().stale_order_threshold_seconds);
|
||||
|
||||
public enum PredicateTypeOneofCase {
|
||||
[Description("(any order)")]
|
||||
AnyOrder,
|
||||
[Description("choose securities")]
|
||||
SecurityOrder
|
||||
}
|
||||
|
||||
[ProtoMember(1)]
|
||||
public SecurityPredicate security { get; set; }
|
||||
|
||||
public PredicateTypeOneofCase predicate_type_case {
|
||||
get {
|
||||
if (security != null)
|
||||
return PredicateTypeOneofCase.SecurityOrder;
|
||||
return PredicateTypeOneofCase.AnyOrder;
|
||||
}
|
||||
}
|
||||
|
||||
public IPredicate<OrderSecurity> create(object obj) =>
|
||||
create_predicate((INotificationServiceDataCache)obj);
|
||||
|
||||
public IPredicate<OrderSecurity> create_predicate(INotificationServiceDataCache data_cache) {
|
||||
var sec_pred = security?.create();
|
||||
return PredicateBuilder.create<OrderSecurity>(os =>
|
||||
// verify security
|
||||
(sec_pred == null || (os.security.HasValue && sec_pred.trigger(os.security.Value))) &&
|
||||
// verify not stale
|
||||
((DateTime.Now - os.order.created()) < _stale_order_threshold) &&
|
||||
// verify order hasn't been added yet
|
||||
// Note: the notification service should offer a way to connect predicates so that they only receive items in TableUpdate.added,
|
||||
// however, the notification service predated TableUpdate.Changes, so for now, just check that the history.Count == 1)
|
||||
(data_cache.order_history.TryGetValue(os.order.Key, out var hist) && hist.Count == 1));
|
||||
}
|
||||
|
||||
public INotificationPredicate<OrderSecurity> create_toast(INotificationServiceDataCache data_cache) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public bool Equals(OrderSentEventPredicate other) =>
|
||||
other != null && other.security.safe_equals(other.security);
|
||||
|
||||
public override bool Equals(object obj) =>
|
||||
Equals(obj as OrderSentEventPredicate);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
EqualityComparer.auto_phase(_type_hash, security?.GetHashCode() ?? 0);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,9 @@ using bts.utils;
|
||||
using mbts.spark;
|
||||
using mbts.ui;
|
||||
using mbts.ui.settings;
|
||||
using mbts.ui.utility;
|
||||
using mbts.utils;
|
||||
using ProtoBuf;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using OrderType = bts.api.orderman.CustomOrderDefinition.CustomOneofCase;
|
||||
using PTOC = mbts.OrderRecordTuplePredicate.PredicateTypeOneofCase;
|
||||
|
||||
namespace mbts {
|
||||
@@ -20,6 +15,7 @@ namespace mbts {
|
||||
// TODO: uncomment the remaining cases when ready
|
||||
public static Dictionary<PTOC, ITemplatedPropertyWrapperVM<OrderRecordTuplePredicate>> get_oneofs(IBus bus, OrdersModel orders_model) {
|
||||
return new Dictionary<PTOC, ITemplatedPropertyWrapperVM<OrderRecordTuplePredicate>> {
|
||||
{ PTOC.SentEvent, wrap(rp => rp.sent_event, (rp, p) => rp.sent_event = p, p => p.predicate_type_case, OrderSentEventPredicateVMFactory.get_oneofs(bus)) },
|
||||
{ PTOC.FilledEvent, wrap(rp => rp.filled_event, (rp, p) => rp.filled_event = p, p => p.predicate_type_case, OrderFilledEventPredicateVMFactory.get_oneofs(bus)) },
|
||||
{ PTOC.CompletedEvent, wrap(rp => rp.completed_event, (rp, p) => rp.completed_event = p, p => p.predicate_type_case, OrderCompletedEventPredicateVMFactory.get_oneofs(bus)) },
|
||||
{ PTOC.CanceledEvent, wrap(rp => rp.canceled_event, (rp, p) => rp.canceled_event = p, p => p.predicate_type_case, OrderCanceledEventPredicateVMFactory.get_oneofs(bus)) },
|
||||
@@ -77,6 +73,7 @@ namespace mbts {
|
||||
OrderFilledEventPredicate _filled_event;
|
||||
OrderCompletedEventPredicate _completed_event;
|
||||
OrderCanceledEventPredicate _canceled_event;
|
||||
OrderSentEventPredicate _sent_event;
|
||||
|
||||
public enum PredicateTypeOneofCase {
|
||||
None,
|
||||
@@ -120,6 +117,8 @@ namespace mbts {
|
||||
OrderKey,
|
||||
[PredicateCase(is_toastable:false)]
|
||||
ParentKey,
|
||||
[PredicateCase("When order is sent", PredicateCategory.Event)]
|
||||
SentEvent,
|
||||
[PredicateCase("When order gets a fill", PredicateCategory.Event)]
|
||||
FilledEvent,
|
||||
[PredicateCase("When order is completed", PredicateCategory.Event)]
|
||||
@@ -176,6 +175,8 @@ namespace mbts {
|
||||
public OrderCompletedEventPredicate completed_event { get => _completed_event; set => _set(ref _completed_event, value); }
|
||||
[ProtoMember(23)]
|
||||
public OrderCanceledEventPredicate canceled_event { get => _canceled_event; set => _set(ref _canceled_event, value); }
|
||||
[ProtoMember(26)]
|
||||
public OrderSentEventPredicate sent_event { get => _sent_event; set => _set(ref _sent_event, value); }
|
||||
[ProtoMember(24)]
|
||||
public bool is_toast_enabled { get; set; }
|
||||
[ProtoMember(25)]
|
||||
@@ -206,6 +207,7 @@ namespace mbts {
|
||||
if (_filled_event != null) return PTOC.FilledEvent;
|
||||
if (_completed_event != null) return PTOC.CompletedEvent;
|
||||
if (_canceled_event != null) return PTOC.CanceledEvent;
|
||||
if (_sent_event != null) return PTOC.SentEvent;
|
||||
if (_attached_target_order_key != null) return PTOC.AttachedTargetOrderKey;
|
||||
return PTOC.None;
|
||||
}
|
||||
@@ -236,6 +238,7 @@ namespace mbts {
|
||||
case PTOC.FilledEvent: return _filled_event.create_predicate(data_cache);
|
||||
case PTOC.CompletedEvent: return _completed_event.create_predicate(data_cache);
|
||||
case PTOC.CanceledEvent: return _canceled_event.create(data_cache);
|
||||
case PTOC.SentEvent: return _sent_event.create(data_cache);
|
||||
case PTOC.AttachedTargetOrderKey: return wrap(_attached_target_order_key.create(), r => r.order.attached_target_order_key());
|
||||
default: return null;
|
||||
}
|
||||
@@ -285,6 +288,7 @@ namespace mbts {
|
||||
case PTOC.FilledEvent: return _filled_event.Equals(other._filled_event);
|
||||
case PTOC.CompletedEvent: return _completed_event.Equals(other._completed_event);
|
||||
case PTOC.CanceledEvent: return _canceled_event.Equals(other._canceled_event);
|
||||
case PTOC.SentEvent: return _sent_event.Equals(other._sent_event);
|
||||
case PTOC.AttachedTargetOrderKey: return _attached_target_order_key.Equals(other._attached_target_order_key);
|
||||
case PTOC.None: return true;
|
||||
default: throw ExceptionHelper.unexpected_oneof_case(ptoc);
|
||||
@@ -320,6 +324,7 @@ namespace mbts {
|
||||
case PTOC.FilledEvent: raw_hash = _filled_event.GetHashCode(); break;
|
||||
case PTOC.CompletedEvent: raw_hash = _completed_event.GetHashCode(); break;
|
||||
case PTOC.CanceledEvent: raw_hash = _canceled_event.GetHashCode(); break;
|
||||
case PTOC.SentEvent: raw_hash = _sent_event.GetHashCode(); break;
|
||||
case PTOC.AttachedTargetOrderKey: raw_hash = _attached_target_order_key.GetHashCode(); break;
|
||||
case PTOC.None: raw_hash = 0; break;
|
||||
default: throw ExceptionHelper.unexpected_oneof_case(ptoc);
|
||||
@@ -355,6 +360,7 @@ namespace mbts {
|
||||
_filled_event = null;
|
||||
_completed_event = null;
|
||||
_canceled_event = null;
|
||||
_sent_event = null;
|
||||
_attached_target_order_key = null;
|
||||
}
|
||||
field = value;
|
||||
|
||||
@@ -1,7 +1,36 @@
|
||||
using System.Linq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
|
||||
namespace mbts {
|
||||
public static class DoubleHelper {
|
||||
public static bool any_nan_or_infinity(params double[] values) {
|
||||
for (int i = 0; i < values.Length; i++) {
|
||||
if (double.IsNaN(values[i]) || double.IsInfinity(values[i]))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static bool are_close(double v1, double v2, double threshold) =>
|
||||
!any_nan_or_infinity(v1, v2, threshold) && Math.Abs(threshold) > Math.Abs(v1 - v2);
|
||||
|
||||
// bc double.IsNaN gets annoying write, and a lazy programmer is a happy programmer ;)
|
||||
public static bool is_nan(this double value) =>
|
||||
double.IsNaN(value);
|
||||
|
||||
/// <summary>
|
||||
/// If the specified value is a <see cref="double"/>, <see cref="int"/>, or <see cref="uint"/>, returns the equivalent <see cref="double"/>; otherwise, returns <see cref="double.NaN"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The value to convert to a <see cref="double"/>.</param>
|
||||
/// <returns>The <see cref="double"/> equivalent value if the cast succeeds, otherwise <see cref="double.NaN"/>.</returns>
|
||||
public static double nan_cast(object value) {
|
||||
// devs: add numeric type checks as needed
|
||||
if (value is double d) return d;
|
||||
if (value is int i) return i;
|
||||
if (value is uint u) return u;
|
||||
return double.NaN;
|
||||
}
|
||||
|
||||
public static bool IsNaNOrInfinity(double d) => double.IsNaN(d) || double.IsInfinity(d);
|
||||
public static bool IsNaNOrInfinityOr(double d, params double[] values) => IsNaNOrInfinity(d) || values.Contains(d);
|
||||
public static bool IsOneOf(double d, params double[] values) => values.Contains(d);
|
||||
|
||||
@@ -4,6 +4,8 @@ using System.Linq;
|
||||
namespace mbts.ui.controls {
|
||||
static class DoubleUtil {
|
||||
public const int API_DECIMAL_PRECISION = 9;
|
||||
// arbitrary small number that can be used to determine whether or not scroll changes are significant
|
||||
public const double MIN_SCROLL_DELTA = 0.000015;
|
||||
/* smallest such that 1.0+DBL_EPSILON != 1.0 */
|
||||
private const double EPSILON = 2.2204460492503131e-016;
|
||||
private const double BIG_EPSILON = EPSILON * 10;
|
||||
|
||||
@@ -1145,6 +1145,30 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.UIntTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.SecurityKeyTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center" Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"/>
|
||||
<c:ContractPickerPopup
|
||||
Grid.Column="2"
|
||||
CloseOnCancel="True"
|
||||
CloseOnOK="True"
|
||||
Header="{Binding SelectedSecurity, RelativeSource={RelativeSource Self}, Converter={x:Static cv:ValueConverters.SecuritySelectorText}}"
|
||||
IsEditMode="False"
|
||||
IsEnabled="{Binding DataContext.editing_permissions, RelativeSource={RelativeSource AncestorType=ItemsControl}, Converter={StaticResource CanEditValuesConverter}}"
|
||||
IsLazy="True"
|
||||
SecurityMap="{DynamicResource ResourceKey={x:Static u:ResourceKeys.Securities}}"
|
||||
SelectedSecurityKey="{Binding value}"
|
||||
ShowDialogSkirt="True"
|
||||
/>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.SecurityKeyTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.StringTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
@@ -1181,30 +1205,35 @@
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.EnumTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"/>
|
||||
<uag:QuantityMeasureControl
|
||||
Grid.Column="2" Focusable="False" HorizontalAlignment="Stretch"
|
||||
QuantityMeasure="{Binding quantity, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl">
|
||||
<uag:QuantityMeasureControl.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
|
||||
</uag:QuantityMeasureControl.CommandBindings>
|
||||
<uag:QuantityMeasureControl.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
|
||||
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
|
||||
</uag:QuantityMeasureControl.InputBindings>
|
||||
</uag:QuantityMeasureControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
<DataTemplate>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" SharedSizeGroup="c0"/>
|
||||
<ColumnDefinition Width="{Binding Source={StaticResource HorizontalPush3}}"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock
|
||||
VerticalAlignment="Center"
|
||||
Text="{Binding descriptor.ParamName, Converter={x:Static ut:DisplayNameHelper.SnakeToTitleCaseConverter}}"
|
||||
/>
|
||||
<uag:QuantityMeasureControl
|
||||
Grid.Column="2"
|
||||
Focusable="False"
|
||||
HorizontalAlignment="Stretch"
|
||||
QuantityMeasure="{Binding quantity, UpdateSourceTrigger=PropertyChanged}" ArrowKeyTickModifiers="Ctrl"
|
||||
>
|
||||
<uag:QuantityMeasureControl.CommandBindings>
|
||||
<CommandBinding Command="{x:Static u:NavigationHelper.MoveFocusCommand}" Executed="{x:Static u:NavigationHelper.KeyboardMoveFocusExecuted}"/>
|
||||
</uag:QuantityMeasureControl.CommandBindings>
|
||||
<uag:QuantityMeasureControl.InputBindings>
|
||||
<KeyBinding Key="Up" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Up}"/>
|
||||
<KeyBinding Key="Down" Command="{x:Static u:NavigationHelper.MoveFocusCommand}" CommandParameter="{x:Static FocusNavigationDirection.Down}"/>
|
||||
</uag:QuantityMeasureControl.InputBindings>
|
||||
</uag:QuantityMeasureControl>
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector.QuantityMeasureTemplate>
|
||||
</usp:DynamicOrderParamTemplateSelector>
|
||||
</ItemsControl.ItemTemplateSelector>
|
||||
</ItemsControl>
|
||||
|
||||
@@ -11,6 +11,9 @@ namespace mbts.ui.converters {
|
||||
public static IValueConverter create<TIn, TOut>(Func<TIn, TOut> convert) =>
|
||||
new AnonymousValueConverter<TIn, TOut>(convert);
|
||||
|
||||
public static IValueConverter create<TIn, TOut>(Func<TIn, TOut> convert, Func<TOut, TIn> convert_back) =>
|
||||
new AnonymousValueConverter<TIn, TOut>(convert, convert_back);
|
||||
|
||||
public static IMultiValueConverter create<TIn1, TIn2, TOut>(Func<TIn1, TIn2, TOut> convert) =>
|
||||
new AnonymousMultiValueConverter<TIn1, TIn2, TOut>(convert);
|
||||
|
||||
@@ -28,9 +31,17 @@ namespace mbts.ui.converters {
|
||||
IntTextConverter = new IntTextConverter(),
|
||||
UIntTextConverter = new UIntTextConverter(),
|
||||
QuantityMeasureTextConverter = ValueConverter.create<QuantityMeasureImmutable?, string>(fmt),
|
||||
SecuritySelectorText,
|
||||
SimpleString = new AnonymousValueConverter(obj => obj?.ToString()),
|
||||
ThicknessToDouble = new ThicknessToDoubleConverter();
|
||||
|
||||
static ValueConverters() {
|
||||
SecuritySelectorText = new NullSubstitutionConverterWrapper {
|
||||
Converter = SimpleString,
|
||||
ValueForNull = "(None)"
|
||||
};
|
||||
}
|
||||
|
||||
public static readonly IMultiValueConverter
|
||||
AreEqualConverter = new AreEqualConverter();
|
||||
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace mbts.ui.converters {
|
||||
class DictionaryLookupConverter : IMultiValueConverter {
|
||||
[ThreadStatic]
|
||||
Dictionary<Type, Lookup> _type_impls_backing;
|
||||
Dictionary<Type, Lookup> _type_impls => _type_impls_backing ?? (_type_impls_backing = new Dictionary<Type, Lookup>());
|
||||
|
||||
delegate bool Lookup(object source, object key, out object result);
|
||||
|
||||
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture) {
|
||||
if (values.Length == 2 && values[0] != null && values[1] != null) {
|
||||
var lookup_type = values[0].GetType();
|
||||
var lookup = _type_impls.GetOrAdd(lookup_type, TypeImplResolver.resolve);
|
||||
if (lookup(values[0], values[1], out var result))
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
static class TypeImplResolver {
|
||||
public static Lookup resolve(Type lookup_type) =>
|
||||
_prioritized_impls(lookup_type)
|
||||
.OrderBy(tup => tup.priority)
|
||||
.Select(tup => tup.impl)
|
||||
.FirstOrDefault() ?? _lookup_fail;
|
||||
|
||||
static bool _lookup_fail(object source, object key, out object result) {
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static IEnumerable<(int priority, Lookup impl)> _prioritized_impls(Type lookup_type) {
|
||||
foreach (var itype in lookup_type.GetInterfaces()) {
|
||||
if (itype.IsGenericType) {
|
||||
var gen_def = itype.GetGenericTypeDefinition();
|
||||
if (gen_def == typeof(IDictionary<,>))
|
||||
yield return (1, _get_generic_impl(nameof(_GenericImpls<int, int>.idictionary), itype));
|
||||
if (gen_def == typeof(IReadOnlyDictionary<,>))
|
||||
yield return (2, _get_generic_impl(nameof(_GenericImpls<int, int>.ireadonly_dictionary), itype));
|
||||
if (gen_def == typeof(ILookup<,>))
|
||||
yield return (3, _get_generic_impl(nameof(_GenericImpls<int, int>.ilookup), itype));
|
||||
}
|
||||
if (itype == typeof(IDictionary))
|
||||
yield return (4, _NonGenericImpls.idictionary);
|
||||
}
|
||||
}
|
||||
|
||||
static Lookup _get_generic_impl(string impl_name, Type impl_type) =>
|
||||
_get_generic_impl(impl_name, impl_type.GetGenericArguments());
|
||||
|
||||
static Lookup _get_generic_impl(string impl_name, Type[] key_value_types) =>
|
||||
(Lookup)typeof(_GenericImpls<,>)
|
||||
.MakeGenericType(key_value_types)
|
||||
.GetField(nameof(_GenericImpls<int, int>.idictionary))
|
||||
.GetValue(null);
|
||||
|
||||
static class _GenericImpls<TKey, TValue> {
|
||||
public static readonly Lookup
|
||||
idictionary = _idictionary,
|
||||
ireadonly_dictionary = _ireadonly_dictionary,
|
||||
ilookup = _ilookup;
|
||||
|
||||
static bool _idictionary(object lookup, object key, out object result) {
|
||||
if (key is TKey k && lookup is IDictionary<TKey, TValue> d && d.TryGetValue(k, out var value)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _ireadonly_dictionary(object lookup, object key, out object result) {
|
||||
if (key is TKey k && lookup is IReadOnlyDictionary<TKey, TValue> rod && rod.TryGetValue(k, out var value)) {
|
||||
result = value;
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool _ilookup(object lookup, object key, out object result) {
|
||||
if ((key is TKey k) && lookup is ILookup<TKey, TValue> l && l.Contains(k)) {
|
||||
result = l[k];
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static class _NonGenericImpls {
|
||||
public static readonly Lookup
|
||||
idictionary = _idictionary;
|
||||
|
||||
static bool _idictionary(object lookup, object key, out object result) {
|
||||
var dict = (IDictionary)lookup;
|
||||
if (dict.Contains(key)) {
|
||||
result = dict[key];
|
||||
return true;
|
||||
}
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,29 @@ using CompositeDisposable = System.Reactive.Disposables.CompositeDisposable;
|
||||
|
||||
namespace mbts.ui.extensions {
|
||||
static class FrameworkElementHelper {
|
||||
/// <summary>
|
||||
/// Invokes the specified action immediately if the element is already loaded, otherwise hooks up to the <see cref="FrameworkElement.Loaded"/> event,
|
||||
/// and [conditionally]invokes the specified action if <paramref name="should_still_execute"/> returns true.
|
||||
/// </summary>
|
||||
/// <param name="element">The element that must be loaded before the specified action is invoked.</param>
|
||||
/// <param name="action">The action to invoke when the element is loaded.</param>
|
||||
/// <param name="should_still_execute">If the element is not already loaded, this delegate determines whether or not to invoke the action in the Loaded event callback.</param>
|
||||
/// <returns></returns>
|
||||
public static void InvokeOnLoaded(this FrameworkElement element, Action action, Func<bool> should_still_execute) {
|
||||
if (action == null)
|
||||
throw new ArgumentNullException(nameof(action));
|
||||
if (element.IsLoaded) {
|
||||
action();
|
||||
} else {
|
||||
element.Loaded += on_loaded;
|
||||
void on_loaded(object sender, RoutedEventArgs e) {
|
||||
element.Loaded -= on_loaded;
|
||||
if (should_still_execute())
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static Point GetCenter(this FrameworkElement element) =>
|
||||
new Point(element.ActualWidth * 0.5, element.ActualHeight * 0.5);
|
||||
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
using mbts.ui.controls;
|
||||
using mbts.ui.extensions;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace mbts.ui {
|
||||
static class ScrollViewerBehavior {
|
||||
public static readonly DependencyProperty VerticalOffsetProperty =
|
||||
DependencyProperty.RegisterAttached("VerticalOffset", typeof(double), typeof(ScrollViewerBehavior), new PropertyMetadata(double.NaN, OnVerticalOffsetChanged));
|
||||
|
||||
static void OnVerticalOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (d is ScrollViewer scroll) {
|
||||
double voff = DoubleHelper.nan_cast(e.NewValue);
|
||||
// Ignore small deltas to prevent strange layout rounding issues/loops.
|
||||
// Be mindful that small deltas are not accumulated into a single larger delta.
|
||||
if (!double.IsNaN(voff) && !DoubleHelper.are_close(DoubleHelper.nan_cast(e.OldValue), voff, DoubleUtil.MIN_SCROLL_DELTA)) {
|
||||
scroll.InvokeOnLoaded(() => scroll.ScrollToVerticalOffset(voff),
|
||||
() => scroll.GetValue(VerticalOffsetProperty) is double curr_voff && curr_voff == voff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static double GetVerticalOffset(DependencyObject d) => (double)d.GetValue(VerticalOffsetProperty);
|
||||
public static void SetVerticalOffset(DependencyObject d, double value) => d.SetValue(VerticalOffsetProperty, value);
|
||||
|
||||
public static readonly DependencyProperty EnableTwoWayVerticalOffsetBindingProperty =
|
||||
DependencyProperty.RegisterAttached("EnableTwoWayVerticalOffsetBinding", typeof(bool), typeof(ScrollViewerBehavior), new PropertyMetadata(false, OnEnableTwoWayVerticalOffsetBindingChanged));
|
||||
|
||||
static void OnEnableTwoWayVerticalOffsetBindingChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
|
||||
if (d is ScrollViewer scroll) {
|
||||
if ((bool)e.NewValue) {
|
||||
scroll.ScrollChanged += EnableTwoWayVerticalOffsetBindingUtils.on_scroll_changed;
|
||||
} else {
|
||||
scroll.ScrollChanged -= EnableTwoWayVerticalOffsetBindingUtils.on_scroll_changed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static bool GetEnableTwoWayVerticalOffsetBinding(DependencyObject d) => (bool)d.GetValue(EnableTwoWayVerticalOffsetBindingProperty);
|
||||
public static void SetEnableTwoWayVerticalOffsetBinding(DependencyObject d, bool value) => d.SetValue(EnableTwoWayVerticalOffsetBindingProperty, value);
|
||||
|
||||
public static readonly DependencyProperty VerticalOffsetMirrorBusyProperty =
|
||||
DependencyProperty.RegisterAttached("VerticalOffsetMirrorBusy", typeof(bool), typeof(ScrollViewerBehavior), new PropertyMetadata(false));
|
||||
|
||||
static class EnableTwoWayVerticalOffsetBindingUtils {
|
||||
public static void on_scroll_changed(object sender, ScrollChangedEventArgs e) {
|
||||
if (e.VerticalChange != 0 && sender is ScrollViewer scroll) {
|
||||
if (!(bool)scroll.GetValue(VerticalOffsetMirrorBusyProperty)) {
|
||||
scroll.SetValue(VerticalOffsetMirrorBusyProperty, true);
|
||||
// scroll changes could get noisy while layouts are being processed;
|
||||
// let everything settle down before updating the VerticalOffset binding sources/targets.
|
||||
scroll.Dispatcher.InvokeAsync(() => {
|
||||
scroll.SetCurrentValue(VerticalOffsetProperty, scroll.VerticalOffset);
|
||||
scroll.SetValue(VerticalOffsetMirrorBusyProperty, false);
|
||||
}, System.Windows.Threading.DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,6 +236,14 @@ namespace mbts.ui.extensions {
|
||||
target.ScrollChanged += callback;
|
||||
return Disposable.Create(() => target.ScrollChanged -= callback);
|
||||
}
|
||||
|
||||
public static bool TryScrollToVerticalOffset(this ScrollViewer scroll_viewer, double vertical_offset) {
|
||||
if (vertical_offset >= 0 && scroll_viewer.IsLoaded && vertical_offset < scroll_viewer.ExtentHeight - scroll_viewer.ViewportHeight) {
|
||||
scroll_viewer.ScrollToVerticalOffset(vertical_offset);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static class TextBoxExtensions {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
namespace mbts.ui {
|
||||
static class ResourceKeys {
|
||||
public const string
|
||||
Securities = nameof(Securities),
|
||||
Accounts = nameof(Accounts),
|
||||
Users = nameof(Users),
|
||||
Sides = nameof(Sides);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace mbts.ui {
|
||||
static class TemplateKeys {
|
||||
public const string
|
||||
SecurityKey = nameof(SecurityKey);
|
||||
}
|
||||
}
|
||||
@@ -130,8 +130,10 @@ namespace mbts.spark {
|
||||
DialogSkirtItem _apply_item, _ok_item, _cancel_item;
|
||||
MenuItemVM _import_item, _export_item, _load_factory_item;
|
||||
|
||||
public ProfiledShellSettingsDialogVM(ISettingsProfileService<T> profile_service, Func<UserSettings, T> get_fallback, Func<T, TVM> vm_factory, Func<T, T> normalize, T init, string file_ext, uint settings_id = 0) {
|
||||
public ProfiledShellSettingsDialogVM(ISettingsProfileService<T> profile_service, Func<UserSettings, T> get_fallback, Func<T, TVM> vm_factory, Func<T, T> normalize, T init, string file_ext, Func<T, T, bool> profile_equals = null, uint settings_id = 0) {
|
||||
_vm_factory = vm_factory;
|
||||
if (profile_equals == null)
|
||||
profile_equals = EqualityComparer<T>.Default.Equals;
|
||||
// profile collections
|
||||
var skip_local = profiles.WrapSkip(1);
|
||||
var skip_local_wrapper = skip_local.WrapSelector(
|
||||
@@ -185,12 +187,19 @@ namespace mbts.spark {
|
||||
continue;
|
||||
i = ~i;
|
||||
var temp = ns.with(value: normalize(ns.value));
|
||||
skip_local_wrapper.Insert(i, temp);
|
||||
if (settings_id == temp.id) {
|
||||
if (temp.value.Equals(init))
|
||||
selected_profile = skip_local[i];
|
||||
settings_id = 0;
|
||||
if (profile_equals(temp.value, init)) {
|
||||
if (!temp.value.Equals(init)) {
|
||||
// they aren't exactly equal (don't overwrite any props in 'init' - use it to create the vm)
|
||||
temp = temp.with(value: init);
|
||||
}
|
||||
skip_local_wrapper.Insert(i, temp);
|
||||
selected_profile = skip_local[i];
|
||||
continue;
|
||||
}
|
||||
}
|
||||
skip_local_wrapper.Insert(i, temp);
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ namespace mbts.spark {
|
||||
readonly IRaiseCanExecuteChangedCommand _add_criteria_command, _remove_criteria_command, _ok_button_command;
|
||||
|
||||
bool _allow_empty, _is_edit_mode;
|
||||
SecurityImmutable? _pending_sec;
|
||||
(SecurityCriteria criteria, SecurityImmutable? sec, SecurityMD? md)? _snapshot;
|
||||
uint? _pending_sec_key;
|
||||
(SecurityCriteria criteria, uint? sec, SecurityMD? md)? _snapshot;
|
||||
|
||||
public ContractPickerModel(IBus bus, bool edit_mode, SecurityCriteria existing_criteria, SecurityCriteria world_view_filter = null)
|
||||
: this(bus.subscribe(SecuritiesTag), edit_mode, existing_criteria, world_view_filter) { }
|
||||
@@ -209,9 +209,9 @@ namespace mbts.spark {
|
||||
_update_security_types();
|
||||
_update_symbols();
|
||||
_update_securities();
|
||||
if (_pending_sec.HasValue && changed_securities.ContainsKey(_pending_sec.Value.Id)) {
|
||||
select_security_request?.Invoke(_pending_sec.Value);
|
||||
_pending_sec = null;
|
||||
if (_pending_sec_key.HasValue && changed_securities.TryGetValue(_pending_sec_key.Value, out var smd)) {
|
||||
select_security_request?.Invoke(smd.security);
|
||||
_pending_sec_key = null;
|
||||
}
|
||||
}
|
||||
internal void criteria_selection_changed(IEnumerable<SecurityCriteriaClause> selected_clauses) {
|
||||
@@ -229,6 +229,8 @@ namespace mbts.spark {
|
||||
security_criteria_clause_changed?.Invoke(this, e);
|
||||
}
|
||||
|
||||
public bool is_editing => _snapshot.HasValue;
|
||||
|
||||
public ObservableCollection<Venue> all_exchanges { get; } = new ObservableCollection<Venue>();
|
||||
|
||||
public List<Venue> selected_exchanges = new List<Venue>();
|
||||
@@ -298,7 +300,22 @@ namespace mbts.spark {
|
||||
if (all_securities.ContainsKey(value.Value.security.Id)) {
|
||||
select_security_request?.Invoke(value.Value.security);
|
||||
} else {
|
||||
_pending_sec = value.Value.security;
|
||||
_pending_sec_key = value.Value.security.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public uint? selected_security_key {
|
||||
get => selected_securities.Count == 1 && all_securities.TryGetValue(selected_securities[0], out var smd) ? smd.security.Id : (uint?)null;
|
||||
set {
|
||||
if (value != selected_security_key) {
|
||||
if (value.HasValue) {
|
||||
if (all_securities.TryGetValue(value.Value, out var smd)) {
|
||||
select_security_request?.Invoke(smd.security);
|
||||
} else {
|
||||
_pending_sec_key = value.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -313,7 +330,7 @@ namespace mbts.spark {
|
||||
if (all_securities.ContainsKey(value.Value.Id)) {
|
||||
select_security_request?.Invoke(value.Value);
|
||||
} else {
|
||||
_pending_sec = value.Value;
|
||||
_pending_sec_key = value.Value.Id;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,18 +353,19 @@ namespace mbts.spark {
|
||||
}
|
||||
|
||||
#region IEditableObject
|
||||
public void BeginEdit() => _snapshot = (result_criteria, _pending_sec ?? selected_security, selected_security_md);
|
||||
public void BeginEdit() => _snapshot = (result_criteria, _pending_sec_key ?? selected_security_key, selected_security_md);
|
||||
public void EndEdit() => _snapshot = null;
|
||||
public void CancelEdit() {
|
||||
if (_snapshot.HasValue) {
|
||||
var (criteria, sec, md) = _snapshot.Value;
|
||||
security_criteria_clauses.SetItems(criteria.clauses);
|
||||
selected_security = sec;
|
||||
selected_security_key = sec;
|
||||
/// A <see cref="_pending_sec"/> may have been used to create the snapshot when <see cref="BeginEdit"/> was called (this happens when securities are loaded lazily).
|
||||
/// If this is the case, pending sec was assigned to the <see cref="_snapshot"/> security;
|
||||
/// check if the security is now present in <see cref="all_securities"/>, if so, use that value instead of the null.
|
||||
selected_security_md = sec.HasValue && !md.HasValue && all_securities.TryGetValue(sec.Value.Id, out var md_match) ? md_match : md;
|
||||
selected_security_md = sec.HasValue && !md.HasValue && all_securities.TryGetValue(sec.Value, out var md_match) ? md_match : md;
|
||||
_snapshot = null;
|
||||
RaisePropertyChanged(nameof(selected_security));
|
||||
_add_criteria_command.RaiseCanExecuteChanged();
|
||||
_remove_criteria_command.RaiseCanExecuteChanged();
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
using mbts.spark;
|
||||
using mbts.ui.converters;
|
||||
using mbts.ui.shell;
|
||||
using mbts.ui.utility;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
@@ -16,8 +17,11 @@ namespace mbts.ui.controls {
|
||||
partial class ContractPickerPopup : PopupControl {
|
||||
public static readonly EventHandler
|
||||
FlushSelectedCriteriaHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedCriteriaProperty)?.UpdateSource(),
|
||||
FlushSelectedSecurityKeyHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedSecurityKeyProperty)?.UpdateSource(),
|
||||
FlushSelectedSecurityHandler = (s, e) => (s as ContractPickerPopup)?.GetBindingExpression(SelectedSecurityProperty)?.UpdateSource();
|
||||
|
||||
readonly PropertyUpdateGate _selection_sync_gate = new PropertyUpdateGate();
|
||||
|
||||
IReadOnlyDictionary<uint, SecurityMD> _cached_security_map = DictionaryHelper.empty<uint, SecurityMD>();
|
||||
ContractPickerPopupController _controller;
|
||||
bool _allow_empty, _is_lazy;
|
||||
@@ -97,12 +101,55 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region SelectedSecurityKey
|
||||
public static readonly DependencyProperty SelectedSecurityKeyProperty =
|
||||
WPFHelper.create_dp<uint?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurityKey),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security_key = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(o.SelectedSecurityMD, o.SelectedSecurity, v, v));
|
||||
});
|
||||
//(o, base_value) => base_value is uint u && u != 0 ? u : (uint?)null);
|
||||
|
||||
public uint? SelectedSecurityKey {
|
||||
get => GetValue(SelectedSecurityKeyProperty) as uint?;
|
||||
set => SetValue(SelectedSecurityKeyProperty, value);
|
||||
}
|
||||
#endregion
|
||||
|
||||
enum SecurityProp {
|
||||
Security,
|
||||
SecurityMD,
|
||||
ID
|
||||
}
|
||||
|
||||
void _sync_selection_props(SecurityMD? curr_sec_md, SecurityImmutable? curr_sec, uint? curr_sec_key, uint? target_sec_key) {
|
||||
if (curr_sec_key != target_sec_key) {
|
||||
SetCurrentValue(SelectedSecurityKeyProperty, target_sec_key);
|
||||
}
|
||||
bool sec_md_dirty = target_sec_key != (curr_sec_md?.security)?.Id;
|
||||
bool sec_dirty = target_sec_key != curr_sec?.Id;
|
||||
if (sec_md_dirty || sec_dirty) {
|
||||
curr_sec_md = target_sec_key.HasValue && _cached_security_map.TryGetValue(target_sec_key.Value, out var smd) ? smd : (SecurityMD?)null;
|
||||
if (sec_md_dirty)
|
||||
SetCurrentValue(SelectedSecurityMDProperty, curr_sec_md);
|
||||
if (sec_dirty)
|
||||
SetCurrentValue(SelectedSecurityProperty, curr_sec_md?.security);
|
||||
}
|
||||
if (!IsEditMode)
|
||||
SetCurrentValue(SelectedCriteriaProperty, target_sec_key.HasValue ? SecurityCriteria.from_keys(new[] { target_sec_key.Value }) : null);
|
||||
}
|
||||
|
||||
#region SelectedSecurity
|
||||
public static readonly DependencyProperty SelectedSecurityProperty =
|
||||
WPFHelper.create_dp<SecurityImmutable?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurity),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => o.Controller.model.selected_security = v);
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(o.SelectedSecurityMD, v, o.SelectedSecurityKey, v?.Id));
|
||||
});
|
||||
|
||||
public SecurityImmutable? SelectedSecurity {
|
||||
get => GetValue(SelectedCriteriaProperty) as SecurityImmutable?;
|
||||
@@ -115,7 +162,10 @@ namespace mbts.ui.controls {
|
||||
WPFHelper.create_dp<SecurityMD?, ContractPickerPopup>(
|
||||
nameof(SelectedSecurityMD),
|
||||
FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
|
||||
(o, v) => o.Controller.model.selected_security_md = v);
|
||||
(o, v) => {
|
||||
o.Controller.model.selected_security_md = v;
|
||||
o._selection_sync_gate.try_update(() => o._sync_selection_props(v, o.SelectedSecurity, o.SelectedSecurityKey, (v?.security)?.Id));
|
||||
});
|
||||
|
||||
public SecurityMD? SelectedSecurityMD {
|
||||
get => GetValue(SelectedCriteriaProperty) as SecurityMD?;
|
||||
@@ -248,12 +298,10 @@ namespace mbts.ui.controls {
|
||||
}
|
||||
void on_cancel() {
|
||||
model.CancelEdit();
|
||||
propagate_to_owner();
|
||||
owner.CancelClick?.Invoke(owner, EventArgs.Empty);
|
||||
}
|
||||
void propagate_to_owner() {
|
||||
owner.set_current_if_changed(SelectedSecurityMDProperty, owner.Controller.model.selected_security_md);
|
||||
owner.set_current_if_changed(SelectedSecurityProperty, owner.Controller.model.selected_security);
|
||||
owner.set_current_if_changed(SelectedSecurityKeyProperty, owner.Controller.model.selected_security_key);
|
||||
owner.set_current_if_changed(SelectedCriteriaProperty, owner.Controller.model.result_criteria);
|
||||
}
|
||||
}
|
||||
@@ -262,4 +310,4 @@ namespace mbts.ui.controls {
|
||||
public SecurityCriteria selected_criteria => _selected_criteria.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace mbts.ui {
|
||||
class BindingProxy : FrameworkElement {
|
||||
public static readonly DependencyProperty ValueProperty =
|
||||
DependencyProperty.Register(nameof(Value), typeof(object), typeof(BindingProxy));
|
||||
|
||||
public object Value {
|
||||
get => GetValue(ValueProperty);
|
||||
set => SetValue(ValueProperty, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,7 +212,10 @@ namespace System.Reactive.Linq {
|
||||
IDisposable source_sub = source.Select(v => v).Subscribe(on_source);
|
||||
return Disposable.Create(dispose);
|
||||
// local methods
|
||||
void on_source(T value) => Disposable.replace(ref value_sub, subscribe_value(value));
|
||||
void on_source(T value) {
|
||||
Disposable.dispose(ref value_sub);
|
||||
value_sub = subscribe_value(value);
|
||||
}
|
||||
void dispose() {
|
||||
Disposable.dispose(ref source_sub);
|
||||
Disposable.dispose(ref value_sub);
|
||||
|
||||
@@ -67,7 +67,12 @@ namespace bts.tests
|
||||
|
||||
return (s1: sec1.Value, s2: sec2.Value);
|
||||
}
|
||||
public static async Task<(IEnumerable<OrderRecordImmutable> book, (Security security, ExecutionStrategy strategy) synthetic)> common_setup_one(OrderService order_service, SyntheticSecurityService synthetic_service, (SecurityImmutable? s1, SecurityImmutable? s2) securities, (String name, uint id) account, (String name, uint id) user) {
|
||||
public static async Task<(IEnumerable<OrderRecordImmutable> book, (Security security, ExecutionStrategy strategy) synthetic)> common_setup_one(
|
||||
OrderService order_service,
|
||||
SyntheticSecurityService synthetic_service,
|
||||
(SecurityImmutable? s1, SecurityImmutable? s2) securities,
|
||||
(String name, uint id) account, (String name, uint id) user)
|
||||
{
|
||||
var order_defs = new (OrderDefinition order_def, double price, uint size)[10];
|
||||
|
||||
for (int i = 0; i < 4; i ++) {
|
||||
|
||||
@@ -11,7 +11,7 @@ if [ -d "$sparkdata_dir/data" ]
|
||||
then
|
||||
[ -d $sparkdata_dir/data/databases/orders ] && rm -rf $sparkdata_dir/data/databases/orders
|
||||
[ -d $sparkdata_dir/data/databases/fills ] && rm -rf $sparkdata_dir/data/databases/fills
|
||||
[ -d $sparkdata_dir/data/databases/ex_sim_orders ] && rm -rf $sparkdata_dir/data/databases/ex_sim_orders
|
||||
[ -d $sparkdata_dir/data/databases/ex_sim_orders_venue_sim ] && rm -rf $sparkdata_dir/data/databases/ex_sim_orders_venue_sim
|
||||
|
||||
[ -d $sparkdata_dir/data/jetfix ] && rm -rf $sparkdata_dir/data/jetfix
|
||||
[ -d $sparkdata_dir/data/exchange_sim ] && rm -rf $sparkdata_dir/data/exchange_sim
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace bts {
|
||||
var result = relay.requestmd(request);
|
||||
|
||||
foreach (var ch in result.Channels) {
|
||||
// if (ch.Platform != Platform.Sim) continue;
|
||||
foreach (var update in ch.Updates) {
|
||||
if (update.Security != null) {
|
||||
var m = new Market() {
|
||||
@@ -129,6 +130,7 @@ namespace bts {
|
||||
}
|
||||
|
||||
var orders = ctx.locate<OrderService>();
|
||||
var factor = TimeSpan.TicksPerMillisecond / 1000;
|
||||
foreach (var m in all_markets(ctx)) {
|
||||
ConsoleEx.WriteLine(m.security.security_desc(), ConsoleColor.White);
|
||||
var tick = m.security.ContractSpec.LowTick;
|
||||
@@ -136,7 +138,12 @@ namespace bts {
|
||||
for (var x = 0; x < o.depth; ++x) {
|
||||
var price = m.ask.Value.price() + (tick * x);
|
||||
ConsoleEx.WriteLine($"Sell {o.quantity} at {price}", ConsoleColor.Red);
|
||||
orders.create_order_async(mkorder(m.security.Id, price, o.quantity, Side.Sell));
|
||||
var sw = new System.Diagnostics.Stopwatch();
|
||||
sw.Start();
|
||||
var r = orders.create_order(mkorder(m.security.Id, price, o.quantity, Side.Sell));
|
||||
sw.Stop();
|
||||
// Log.Api.info($"Order: id {r.OrderKey} time {sw.ElapsedTicks/factor}us");
|
||||
Log.Api.info($"Order {m.security.security_desc()}: id {r.OrderKey} {r.ErrorCode} time {sw.ElapsedTicks/factor}us");
|
||||
System.Threading.Thread.Sleep(o.sleep);
|
||||
}
|
||||
}
|
||||
@@ -145,7 +152,11 @@ namespace bts {
|
||||
for (var x=0; x < o.depth; ++x) {
|
||||
var price = m.bid.Value.price() - (tick * x);
|
||||
ConsoleEx.WriteLine($"Buy {o.quantity} at {price}", ConsoleColor.Blue);
|
||||
orders.create_order_async(mkorder(m.security.Id, price, o.quantity, Side.Buy));
|
||||
var sw = new System.Diagnostics.Stopwatch();
|
||||
sw.Start();
|
||||
var r = orders.create_order(mkorder(m.security.Id, price, o.quantity, Side.Buy));
|
||||
sw.Stop();
|
||||
Log.Api.info($"Order {m.security.security_desc()}: id {r.OrderKey} {r.ErrorCode} time {sw.ElapsedTicks/factor}us");
|
||||
System.Threading.Thread.Sleep(o.sleep);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ using System.Threading;
|
||||
|
||||
namespace bts {
|
||||
|
||||
[Verb("servicetest", HelpText="Scratch space for testing services")]
|
||||
public class ServiceTestOptions {
|
||||
[Option(nameof(synthetic))]
|
||||
public bool synthetic { get; set; }
|
||||
@@ -16,10 +17,10 @@ namespace bts {
|
||||
public bool user { get; set; }
|
||||
[Option(nameof(admin))]
|
||||
public bool admin { get; set; }
|
||||
|
||||
[Option(nameof(latency))]
|
||||
public bool latency { get; set; }
|
||||
}
|
||||
|
||||
[Verb("servicetest", HelpText="Scratch space for testing services")]
|
||||
public class ServiceTest {
|
||||
public static int Run(ServiceTestOptions o) {
|
||||
if (o.synthetic) {
|
||||
@@ -31,6 +32,9 @@ namespace bts {
|
||||
if (o.admin) {
|
||||
admin_test();
|
||||
}
|
||||
if (o.latency) {
|
||||
latency_test();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -114,5 +118,21 @@ namespace bts {
|
||||
Thread.Sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
private static void latency_test() {
|
||||
var ctx = new ServiceContext();
|
||||
var s = ctx.locate<bts.metrics.ShellLatencyService>();
|
||||
|
||||
UInt64 sequence = 0;
|
||||
var factor = TimeSpan.TicksPerMillisecond / 1000;
|
||||
while(true) {
|
||||
var sw = new System.Diagnostics.Stopwatch();
|
||||
sw.Start();
|
||||
var reply = s.ping(new metrics.BufferMessage() { Sequence = ++sequence });
|
||||
sw.Stop();
|
||||
Console.WriteLine($"{reply.Sequence} - {sw.ElapsedMilliseconds}ms -- {sw.ElapsedTicks/factor} us");
|
||||
System.Threading.Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -603,6 +603,7 @@ enum DynamicOrderPrimitiveDescriptor {
|
||||
DYNAMIC_ORDER_PRIMITIVE_BOOL = 4;
|
||||
DYNAMIC_ORDER_PRIMITIVE_ORDER_DEF = 5;
|
||||
DYNAMIC_ORDER_PRIMITIVE_QUANTITY = 6;
|
||||
DYNAMIC_ORDER_PRIMITIVE_SECURITY_KEY = 7;
|
||||
}
|
||||
|
||||
message DynamicOrderTypeDescriptor {
|
||||
|
||||
@@ -15,8 +15,13 @@ message Report {
|
||||
string user = 3;
|
||||
uint64 timestamp = 10;
|
||||
}
|
||||
message BufferMessage {
|
||||
uint64 sequence = 1;
|
||||
string data = 10;
|
||||
}
|
||||
|
||||
service ShellLatencyService {
|
||||
rpc clock(bts.api.VoidMessage) returns (stream Tick);
|
||||
rpc report(Report) returns (bts.api.VoidMessage);
|
||||
rpc ping(BufferMessage) returns (BufferMessage);
|
||||
}
|
||||
@@ -3,11 +3,10 @@
|
||||
# source: tools/cpp_helper/generate-cpps.ps1
|
||||
#
|
||||
|
||||
CppConfig
|
||||
{
|
||||
value
|
||||
CppConfig
|
||||
{
|
||||
value
|
||||
{
|
||||
cpp_entry { key: "098" value: 2000 }
|
||||
cpp_entry { key: "0A" value: 100 }
|
||||
cpp_entry { key: "0B" value: 100 }
|
||||
cpp_entry { key: "0C" value: 100 }
|
||||
@@ -459,9 +458,9 @@ CppConfig
|
||||
cpp_entry { key: "DDE" value: 1000 }
|
||||
cpp_entry { key: "DDI" value: 1000 }
|
||||
cpp_entry { key: "DDO" value: 1000 }
|
||||
cpp_entry { key: "DEB" value: 720 }
|
||||
cpp_entry { key: "DEB" value: 745 }
|
||||
cpp_entry { key: "DEN" value: 250 }
|
||||
cpp_entry { key: "DEP" value: 252 }
|
||||
cpp_entry { key: "DEP" value: 276 }
|
||||
cpp_entry { key: "DFL" value: 100 }
|
||||
cpp_entry { key: "DFN" value: 100 }
|
||||
cpp_entry { key: "DG2" value: 100 }
|
||||
@@ -730,8 +729,8 @@ CppConfig
|
||||
cpp_entry { key: "IPO" value: 10 }
|
||||
cpp_entry { key: "IPT" value: 10 }
|
||||
cpp_entry { key: "IS" value: 50 }
|
||||
cpp_entry { key: "ITB" value: 720 }
|
||||
cpp_entry { key: "ITP" value: 252 }
|
||||
cpp_entry { key: "ITB" value: 745 }
|
||||
cpp_entry { key: "ITP" value: 276 }
|
||||
cpp_entry { key: "IW" value: 50 }
|
||||
cpp_entry { key: "J4L" value: 80 }
|
||||
cpp_entry { key: "J7" value: 6250000 }
|
||||
@@ -773,7 +772,6 @@ CppConfig
|
||||
cpp_entry { key: "K5" value: 20 }
|
||||
cpp_entry { key: "K5K" value: 20 }
|
||||
cpp_entry { key: "K5N" value: 20 }
|
||||
cpp_entry { key: "KC" value: 375 }
|
||||
cpp_entry { key: "KE" value: 50 }
|
||||
cpp_entry { key: "KET" value: 50 }
|
||||
cpp_entry { key: "KQ" value: 20 }
|
||||
@@ -809,6 +807,7 @@ CppConfig
|
||||
cpp_entry { key: "LIY" value: 1000 }
|
||||
cpp_entry { key: "LL" value: 42000 }
|
||||
cpp_entry { key: "LLR" value: 1000 }
|
||||
cpp_entry { key: "LNG" value: 10000 }
|
||||
cpp_entry { key: "LPE" value: 47000 }
|
||||
cpp_entry { key: "LPS" value: 1000 }
|
||||
cpp_entry { key: "LSF" value: 1 }
|
||||
@@ -1003,7 +1002,6 @@ CppConfig
|
||||
cpp_entry { key: "OAD" value: 1 }
|
||||
cpp_entry { key: "ODO" value: 1000 }
|
||||
cpp_entry { key: "OFF" value: 5 }
|
||||
cpp_entry { key: "OJ" value: 150 }
|
||||
cpp_entry { key: "OML" value: 5 }
|
||||
cpp_entry { key: "OMM" value: 80 }
|
||||
cpp_entry { key: "OOD" value: 5 }
|
||||
@@ -1146,7 +1144,7 @@ CppConfig
|
||||
cpp_entry { key: "SBT" value: 50 }
|
||||
cpp_entry { key: "SD" value: 1000 }
|
||||
cpp_entry { key: "SDA" value: 250 }
|
||||
cpp_entry { key: "SDD" value: 1600 }
|
||||
cpp_entry { key: "SDD" value: 1840 }
|
||||
cpp_entry { key: "SDG" value: 250 }
|
||||
cpp_entry { key: "SDI" value: 1000 }
|
||||
cpp_entry { key: "SDM" value: 100 }
|
||||
@@ -1163,10 +1161,12 @@ CppConfig
|
||||
cpp_entry { key: "SFR" value: 250 }
|
||||
cpp_entry { key: "SG" value: 250 }
|
||||
cpp_entry { key: "SGB" value: 1000 }
|
||||
cpp_entry { key: "SGC" value: 1000 }
|
||||
cpp_entry { key: "SGE" value: 42000 }
|
||||
cpp_entry { key: "SGF" value: 1000 }
|
||||
cpp_entry { key: "SGG" value: 1000 }
|
||||
cpp_entry { key: "SGT" value: 250 }
|
||||
cpp_entry { key: "SGU" value: 32.15 }
|
||||
cpp_entry { key: "SI" value: 5000 }
|
||||
cpp_entry { key: "SIL" value: 1000 }
|
||||
cpp_entry { key: "SIR" value: 500 }
|
||||
@@ -1185,7 +1185,7 @@ CppConfig
|
||||
cpp_entry { key: "SR3" value: 2500 }
|
||||
cpp_entry { key: "SR5" value: 1000 }
|
||||
cpp_entry { key: "SRB" value: 1000 }
|
||||
cpp_entry { key: "SRT" value: 1600 }
|
||||
cpp_entry { key: "SRT" value: 1840 }
|
||||
cpp_entry { key: "SSI" value: 1000 }
|
||||
cpp_entry { key: "SSP" value: 5000 }
|
||||
cpp_entry { key: "SSW" value: 1000 }
|
||||
@@ -1197,31 +1197,31 @@ CppConfig
|
||||
cpp_entry { key: "SU" value: 250 }
|
||||
cpp_entry { key: "SUT" value: 250 }
|
||||
cpp_entry { key: "T01" value: 800 }
|
||||
cpp_entry { key: "T02" value: 800 }
|
||||
cpp_entry { key: "T03" value: 800 }
|
||||
cpp_entry { key: "T04" value: 800 }
|
||||
cpp_entry { key: "T05" value: 800 }
|
||||
cpp_entry { key: "T06" value: 800 }
|
||||
cpp_entry { key: "T07" value: 800 }
|
||||
cpp_entry { key: "T08" value: 800 }
|
||||
cpp_entry { key: "T09" value: 800 }
|
||||
cpp_entry { key: "T10" value: 800 }
|
||||
cpp_entry { key: "T11" value: 800 }
|
||||
cpp_entry { key: "T12" value: 800 }
|
||||
cpp_entry { key: "T13" value: 800 }
|
||||
cpp_entry { key: "T15" value: 800 }
|
||||
cpp_entry { key: "T16" value: 800 }
|
||||
cpp_entry { key: "T17" value: 800 }
|
||||
cpp_entry { key: "T18" value: 800 }
|
||||
cpp_entry { key: "T19" value: 800 }
|
||||
cpp_entry { key: "T1U" value: 1000 }
|
||||
cpp_entry { key: "T20" value: 800 }
|
||||
cpp_entry { key: "T21" value: 800 }
|
||||
cpp_entry { key: "T22" value: 800 }
|
||||
cpp_entry { key: "T23" value: 800 }
|
||||
cpp_entry { key: "T24" value: 800 }
|
||||
cpp_entry { key: "T25" value: 800 }
|
||||
cpp_entry { key: "T26" value: 800 }
|
||||
cpp_entry { key: "T27" value: 800 }
|
||||
cpp_entry { key: "T28" value: 800 }
|
||||
cpp_entry { key: "T29" value: 800 }
|
||||
cpp_entry { key: "T2B" value: 1000 }
|
||||
cpp_entry { key: "T2D" value: 1000 }
|
||||
cpp_entry { key: "T2M" value: 100 }
|
||||
cpp_entry { key: "T30" value: 800 }
|
||||
cpp_entry { key: "T31" value: 800 }
|
||||
cpp_entry { key: "T3B" value: 1000 }
|
||||
cpp_entry { key: "T3L" value: 80 }
|
||||
cpp_entry { key: "T4B" value: 1000 }
|
||||
@@ -1276,7 +1276,7 @@ CppConfig
|
||||
cpp_entry { key: "TT" value: 50000 }
|
||||
cpp_entry { key: "TTD" value: 24 }
|
||||
cpp_entry { key: "TTE" value: 10000 }
|
||||
cpp_entry { key: "TTF" value: 720 }
|
||||
cpp_entry { key: "TTF" value: 745 }
|
||||
cpp_entry { key: "TUB" value: 1000 }
|
||||
cpp_entry { key: "TUF" value: 1000 }
|
||||
cpp_entry { key: "TUL" value: 1000 }
|
||||
@@ -1299,7 +1299,7 @@ CppConfig
|
||||
cpp_entry { key: "UFU" value: 100 }
|
||||
cpp_entry { key: "UFV" value: 100 }
|
||||
cpp_entry { key: "UGG" value: 42000 }
|
||||
cpp_entry { key: "UKG" value: 300 }
|
||||
cpp_entry { key: "UKG" value: 310 }
|
||||
cpp_entry { key: "ULB" value: 42000 }
|
||||
cpp_entry { key: "UMB" value: 1000 }
|
||||
cpp_entry { key: "UMD" value: 1000 }
|
||||
@@ -1457,14 +1457,13 @@ CppConfig
|
||||
cpp_entry { key: "ZT" value: 2000 }
|
||||
cpp_entry { key: "ZW" value: 50 }
|
||||
cpp_entry { key: "ZWT" value: 50 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CmeSymbolMics
|
||||
{
|
||||
value
|
||||
CmeSymbolMics
|
||||
{
|
||||
value
|
||||
{
|
||||
cme_mic { key: "098" value: CBT }
|
||||
cme_mic { key: "0A" value: NYMEX }
|
||||
cme_mic { key: "0B" value: NYMEX }
|
||||
cme_mic { key: "0C" value: NYMEX }
|
||||
@@ -2265,6 +2264,7 @@ CmeSymbolMics
|
||||
cme_mic { key: "LIY" value: CBT }
|
||||
cme_mic { key: "LL" value: NYMEX }
|
||||
cme_mic { key: "LLR" value: NYMEX }
|
||||
cme_mic { key: "LNG" value: NYMEX }
|
||||
cme_mic { key: "LPE" value: NYMEX }
|
||||
cme_mic { key: "LPS" value: NYMEX }
|
||||
cme_mic { key: "LSF" value: COMEX }
|
||||
@@ -2618,10 +2618,12 @@ CmeSymbolMics
|
||||
cme_mic { key: "SFR" value: CME }
|
||||
cme_mic { key: "SG" value: CME }
|
||||
cme_mic { key: "SGB" value: NYMEX }
|
||||
cme_mic { key: "SGC" value: COMEX }
|
||||
cme_mic { key: "SGE" value: NYMEX }
|
||||
cme_mic { key: "SGF" value: NYMEX }
|
||||
cme_mic { key: "SGG" value: NYMEX }
|
||||
cme_mic { key: "SGT" value: CME }
|
||||
cme_mic { key: "SGU" value: COMEX }
|
||||
cme_mic { key: "SI" value: COMEX }
|
||||
cme_mic { key: "SIL" value: COMEX }
|
||||
cme_mic { key: "SIR" value: CME }
|
||||
@@ -2652,31 +2654,31 @@ CmeSymbolMics
|
||||
cme_mic { key: "SU" value: CME }
|
||||
cme_mic { key: "SUT" value: CME }
|
||||
cme_mic { key: "T01" value: NYMEX }
|
||||
cme_mic { key: "T02" value: NYMEX }
|
||||
cme_mic { key: "T03" value: NYMEX }
|
||||
cme_mic { key: "T04" value: NYMEX }
|
||||
cme_mic { key: "T05" value: NYMEX }
|
||||
cme_mic { key: "T06" value: NYMEX }
|
||||
cme_mic { key: "T07" value: NYMEX }
|
||||
cme_mic { key: "T08" value: NYMEX }
|
||||
cme_mic { key: "T09" value: NYMEX }
|
||||
cme_mic { key: "T10" value: NYMEX }
|
||||
cme_mic { key: "T11" value: NYMEX }
|
||||
cme_mic { key: "T12" value: NYMEX }
|
||||
cme_mic { key: "T13" value: NYMEX }
|
||||
cme_mic { key: "T15" value: NYMEX }
|
||||
cme_mic { key: "T16" value: NYMEX }
|
||||
cme_mic { key: "T17" value: NYMEX }
|
||||
cme_mic { key: "T18" value: NYMEX }
|
||||
cme_mic { key: "T19" value: NYMEX }
|
||||
cme_mic { key: "T1U" value: CBT }
|
||||
cme_mic { key: "T20" value: NYMEX }
|
||||
cme_mic { key: "T21" value: NYMEX }
|
||||
cme_mic { key: "T22" value: NYMEX }
|
||||
cme_mic { key: "T23" value: NYMEX }
|
||||
cme_mic { key: "T24" value: NYMEX }
|
||||
cme_mic { key: "T25" value: NYMEX }
|
||||
cme_mic { key: "T26" value: NYMEX }
|
||||
cme_mic { key: "T27" value: NYMEX }
|
||||
cme_mic { key: "T28" value: NYMEX }
|
||||
cme_mic { key: "T29" value: NYMEX }
|
||||
cme_mic { key: "T2B" value: NYMEX }
|
||||
cme_mic { key: "T2D" value: NYMEX }
|
||||
cme_mic { key: "T2M" value: NYMEX }
|
||||
cme_mic { key: "T30" value: NYMEX }
|
||||
cme_mic { key: "T31" value: NYMEX }
|
||||
cme_mic { key: "T3B" value: NYMEX }
|
||||
cme_mic { key: "T3L" value: NYMEX }
|
||||
cme_mic { key: "T4B" value: NYMEX }
|
||||
@@ -2912,5 +2914,5 @@ CmeSymbolMics
|
||||
cme_mic { key: "ZT" value: CBT }
|
||||
cme_mic { key: "ZW" value: CBT }
|
||||
cme_mic { key: "ZWT" value: CBT }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,339 @@ JetFIX {
|
||||
}
|
||||
}
|
||||
|
||||
JetFIX {
|
||||
rule: "CannedCertSession.name='gt_cert'"
|
||||
value {
|
||||
connection {
|
||||
name: "ice_cert"
|
||||
platform: ICE
|
||||
use_cme_secure_logon: false
|
||||
log_fix_stream: true
|
||||
socket_connect_host_ip: "63.247.113.154"
|
||||
socket_connect_port: 80
|
||||
sender_compid: "51251"
|
||||
target_compid: "ICE"
|
||||
start_time: "22:55:00"
|
||||
end_time: "22:05:00"
|
||||
sender_subid: "BTS"
|
||||
user_name: "bluetrd-os" #this is the order_server_id
|
||||
raw_data: "Starts123"
|
||||
max_order_msgs_per_window: 300
|
||||
auto_fix_seq_num: true
|
||||
|
||||
seconds_to_replay: 6000
|
||||
}
|
||||
connection: @JetFIXSimConnection.sim
|
||||
connection {
|
||||
name: "cme_mc7_99"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.199"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "99"
|
||||
cme_market_segment_id: 99
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_58"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.172"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "58"
|
||||
cme_market_segment_id: 58
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_54"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.191"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "54"
|
||||
cme_market_segment_id: 54
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_82"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.179"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "82"
|
||||
cme_market_segment_id: 82
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_56"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.174"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "56"
|
||||
cme_market_segment_id: 56
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_50"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.202"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "50"
|
||||
cme_market_segment_id: 50
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_52"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.181"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "52"
|
||||
cme_market_segment_id: 52
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_60"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.195"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "60"
|
||||
cme_market_segment_id: 60
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_76"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.204"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "76"
|
||||
cme_market_segment_id: 76
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_88"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.169"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "88"
|
||||
cme_market_segment_id: 88
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_74"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.197"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "74"
|
||||
cme_market_segment_id: 74
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_64"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.177"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "64"
|
||||
cme_market_segment_id: 64
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_72"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.189"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "72"
|
||||
cme_market_segment_id: 72
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_70"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.183"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "70"
|
||||
cme_market_segment_id: 70
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_68"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.168"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "68"
|
||||
cme_market_segment_id: 68
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_80"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.187"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "80"
|
||||
cme_market_segment_id: 80
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_84"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.198"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "84"
|
||||
cme_market_segment_id: 84
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
connection {
|
||||
name: "cme_mc7_78"
|
||||
platform: GLBX
|
||||
socket_connect_host_ip: "69.50.112.167"
|
||||
socket_connect_port: 61923
|
||||
sender_compid: "MC7004N"
|
||||
target_compid: "CME"
|
||||
target_subid: "78"
|
||||
cme_market_segment_id: 78
|
||||
start_time: "00:30:00"
|
||||
end_time: "23:30:00"
|
||||
start_day: "sunday"
|
||||
end_day: "friday"
|
||||
sender_subid: "BTS"
|
||||
raw_data: "S4PMA"
|
||||
auto_fix_seq_num: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
JetFIX {
|
||||
rule: "CannedCertSession.name='mc7'"
|
||||
value {
|
||||
|
||||
@@ -78,6 +78,13 @@ message OrderBookOptions {
|
||||
optional double status_color_opacity_when_selected = 2 [default = 0.35];
|
||||
}
|
||||
|
||||
message OrderAlerts {
|
||||
// when the client sees an order for the first time,
|
||||
// this time period is used to determine if the order is actually new,
|
||||
// or [presumably]just arrived bc shell is still initializing, or the server restarted.
|
||||
optional int32 stale_order_threshold_seconds = 1 [default = 60];
|
||||
}
|
||||
|
||||
message ShellPingOptions {
|
||||
optional bool shell_ping_is_enabled = 1 [default = true];
|
||||
optional int32 workloop_milliseconds_interval = 2 [default = 5000];
|
||||
|
||||
@@ -61,4 +61,10 @@ message EnsureOnScreenWithWorkAreaRect {
|
||||
|
||||
message IgnoreNansInDayPnL {
|
||||
optional bool enabled = 1 [default = true];
|
||||
}
|
||||
|
||||
message DynamicAlgos{
|
||||
optional bool enable_dorman = 1 [default = false];
|
||||
optional bool enable_pyramid = 2 [default = false];
|
||||
optional bool enable_tas = 3 [default = false];
|
||||
}
|
||||
@@ -43,6 +43,8 @@ inline api::Side other_side(api::Side side) {
|
||||
|
||||
inline double market_epislon() { 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:
|
||||
@@ -54,6 +56,7 @@ inline bool is_better(api::Side side, Price p1, Price p2) {
|
||||
}
|
||||
}
|
||||
|
||||
// like the above function
|
||||
inline Price improve_price(api::Side side, Price p, Price offset) {
|
||||
switch (side) {
|
||||
case api::SIDE_BUY:
|
||||
@@ -169,13 +172,30 @@ inline Price tick_size(api::Security const &sec) {
|
||||
|
||||
// Looking to round md::IntPrice? Look in IntPrice.h
|
||||
|
||||
// computes the difference between 2 prices in ticks.
|
||||
// the result is an a signed int so the order of args
|
||||
// matter, its second price - first price
|
||||
// computes price_2 - price_1 in ticks
|
||||
inline int price_diff_to_ticks(double price_1, double price_2,
|
||||
double tick_size) {
|
||||
return static_cast<int>(std::round((price_2 - price_1) / tick_size));
|
||||
}
|
||||
// returns the difference (in ticks) bw and order and a market.
|
||||
// if the order has crossed the market, returns -ve ticks
|
||||
// (ie the buy order price > market_price or
|
||||
// sell_order_price < market_price)
|
||||
inline int order_diff_market(double order_price,
|
||||
double market_price,
|
||||
api::Side side,
|
||||
double tick_size){
|
||||
switch (side) {
|
||||
case api::SIDE_BUY:
|
||||
return price_diff_to_ticks(order_price,market_price,tick_size);
|
||||
case api::SIDE_SELL:
|
||||
return price_diff_to_ticks(market_price,order_price,tick_size);
|
||||
default:
|
||||
bts_assert(false);
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
// because of prices not being completely rounded to the tick,
|
||||
// simple double equality can not be used to compare prices.
|
||||
// this function deems 2 prices to be equal if they are within
|
||||
|
||||
@@ -133,6 +133,13 @@ api::orderman::DynamicOrderParamDescriptor param_uint32(const std::string& name)
|
||||
return doap;
|
||||
}
|
||||
|
||||
api::orderman::DynamicOrderParamDescriptor param_security_key(const std::string& name) {
|
||||
api::orderman::DynamicOrderParamDescriptor doap;
|
||||
doap.set_param_name(name);
|
||||
doap.mutable_param_type()->set_primitive(api::orderman::DYNAMIC_ORDER_PRIMITIVE_SECURITY_KEY);
|
||||
return doap;
|
||||
}
|
||||
|
||||
api::orderman::DynamicOrderParamDescriptor param_double(const std::string& name) {
|
||||
api::orderman::DynamicOrderParamDescriptor doap;
|
||||
doap.set_param_name(name);
|
||||
|
||||
@@ -93,6 +93,7 @@ std::optional<T> get_if(const google::protobuf::Map<std::string,api::orderman::D
|
||||
|
||||
api::orderman::DynamicOrderParamDescriptor param_int32(const std::string& name);
|
||||
api::orderman::DynamicOrderParamDescriptor param_uint32(const std::string& name);
|
||||
api::orderman::DynamicOrderParamDescriptor param_security_key(const std::string& name);
|
||||
api::orderman::DynamicOrderParamDescriptor param_double(const std::string& name);
|
||||
api::orderman::DynamicOrderParamDescriptor param_string(const std::string& name);
|
||||
api::orderman::DynamicOrderParamDescriptor param_bool(const std::string& name);
|
||||
|
||||
@@ -76,6 +76,7 @@ struct ICENativeOrder::PImpl
|
||||
|
||||
bool buffered_cancel = false;
|
||||
std::optional<std::pair<Price, Quantity>> buffered_update;
|
||||
int buffered_tries = 0;
|
||||
};
|
||||
|
||||
ICENativeOrder::ICENativeOrder() : me(new PImpl()) {}
|
||||
@@ -626,6 +627,11 @@ OrderOpResult ICENativeOrder::on_update(api::orderman::Update const &update)
|
||||
if (me->state.last_msg().size())
|
||||
me->state.set_last_msg("");
|
||||
|
||||
LOG(OrderMan, Warn,
|
||||
"ICE may not honor this update if we recently issued an unacknowledged cancelreplace {}. Update: {}@{}. Buffering",
|
||||
key().value(), q.value(), p.value());
|
||||
me->buffered_update.emplace(p, q);
|
||||
|
||||
// early return so we skip report_state (for speed)
|
||||
return OrderOpResult{};
|
||||
}
|
||||
@@ -730,6 +736,11 @@ OrderOpResult ICENativeOrder::on_cancel()
|
||||
me->cancel_clordid_bm = encoding.overwrite( me->cancel_clordid_bm, cancel_clorid, count_digits(cancel_clorid) );
|
||||
|
||||
me->services->queue_fix(*me->session_services, jetfix::MessageTypes::Cancel,me->cancel_encoding);
|
||||
me->buffered_cancel = true;
|
||||
LOG(OrderMan, Warn,
|
||||
"ICE May not honor this cancel if we'd previously issued an unacknowledged cancelreplace "
|
||||
"Buffering cancel to be safe",
|
||||
key().value());
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -865,6 +876,8 @@ void ICENativeOrder::on_exe_report(const NewExecutionReportEvent &er)
|
||||
// terminal states, nothing can change them
|
||||
case ORDER_STATUS_COMPLETED:
|
||||
case ORDER_STATUS_CANCELED:
|
||||
me->buffered_cancel = false;
|
||||
me->buffered_update.reset();
|
||||
break;
|
||||
|
||||
// receiving an execution report does does not mean
|
||||
@@ -881,30 +894,27 @@ void ICENativeOrder::on_exe_report(const NewExecutionReportEvent &er)
|
||||
transition(external_status);
|
||||
if (external_status == ORDER_STATUS_WORKING)
|
||||
{
|
||||
if (me->buffered_cancel)
|
||||
{
|
||||
LOG(OrderMan, Warn,
|
||||
"Applying buffered cancel to order {}, on first ack",
|
||||
key().value());
|
||||
on_cancel();
|
||||
}
|
||||
else if (me->buffered_update.has_value())
|
||||
{
|
||||
auto [p, q] = me->buffered_update.value();
|
||||
me->buffered_update.reset();
|
||||
LOG(OrderMan, Warn,
|
||||
"Applying buffered update ({}@{}) to order {}, on "
|
||||
"first ack",
|
||||
q.value(),p.value(),
|
||||
key().value());
|
||||
on_update(p, q);
|
||||
}
|
||||
handle_buffered_state("first ack");
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case ORDER_STATUS_WORKING:
|
||||
transition(external_status);
|
||||
{
|
||||
transition(external_status);
|
||||
if(me->buffered_update)
|
||||
{
|
||||
auto [p, q] = me->buffered_update.value();
|
||||
if(er.data->price.value() == p.value() && er.data->quantity.value() == q.value())
|
||||
me->buffered_update.reset();
|
||||
if(me->buffered_tries++ > 10)
|
||||
{
|
||||
me->buffered_tries = 0;
|
||||
me->buffered_update.reset();
|
||||
}
|
||||
}
|
||||
handle_buffered_state("working");
|
||||
}
|
||||
break;
|
||||
|
||||
case ORDER_STATUS_INACTIVE:
|
||||
@@ -927,6 +937,27 @@ void ICENativeOrder::on_exe_report(const NewExecutionReportEvent &er)
|
||||
}
|
||||
}
|
||||
|
||||
void ICENativeOrder::handle_buffered_state(const std::string& condition)
|
||||
{
|
||||
if (me->buffered_cancel)
|
||||
{
|
||||
LOG(OrderMan, Warn,
|
||||
"Applying buffered cancel to order {}, on ",
|
||||
condition,
|
||||
key().value());
|
||||
on_cancel();
|
||||
}
|
||||
else if (me->buffered_update.has_value())
|
||||
{
|
||||
auto [p, q] = me->buffered_update.value();
|
||||
LOG(OrderMan, Warn,
|
||||
"Applying buffered update ({}@{}) to order {}, on ",
|
||||
condition,
|
||||
q.value(),p.value(), key().value());
|
||||
on_update(p, q);
|
||||
}
|
||||
}
|
||||
|
||||
void ICENativeOrder::on_cancel_reject(const orderman::CancelRejectEvent &cr)
|
||||
{
|
||||
auto &state = me->state;
|
||||
@@ -963,6 +994,7 @@ void ICENativeOrder::on_gateway_status(const GatewayStatusChangeEvent &ev)
|
||||
case ORDER_STATUS_CANCELED:
|
||||
case ORDER_STATUS_ERROR:
|
||||
case ORDER_STATUS_PENDING_WORKING:
|
||||
break;
|
||||
case ORDER_STATUS_WORKING:
|
||||
break;
|
||||
case ORDER_STATUS_DISCONNECTED:
|
||||
@@ -971,23 +1003,7 @@ void ICENativeOrder::on_gateway_status(const GatewayStatusChangeEvent &ev)
|
||||
transition(map_external_status(native_state.native_status()));
|
||||
if (me->state.status() == ORDER_STATUS_WORKING)
|
||||
{
|
||||
if (me->buffered_cancel)
|
||||
{
|
||||
LOG(OrderMan, Warn,
|
||||
"Applying buffered cancel to order {}, on re-connection",
|
||||
key().value());
|
||||
on_cancel();
|
||||
}
|
||||
else if (me->buffered_update.has_value())
|
||||
{
|
||||
auto [p, q] = me->buffered_update.value();
|
||||
me->buffered_update.reset();
|
||||
LOG(OrderMan, Warn,
|
||||
"Applying buffered update ({}@{}) to order {}, on "
|
||||
"re-connection",
|
||||
q.value(),p.value(), key().value());
|
||||
on_update(p, q);
|
||||
}
|
||||
handle_buffered_state("re-connection");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ class ICENativeOrder : public NativeOrderBase {
|
||||
void warm_up_activate(OrderServicesData &, OrderServices &) override;
|
||||
void init_recovered_common(const api::orderman::OrderRecord &,
|
||||
const PreActivationOrderServices &);
|
||||
void handle_buffered_state(const std::string& condition);
|
||||
|
||||
|
||||
private:
|
||||
|
||||
@@ -20,7 +20,7 @@ class IAlgoServices : public IBeforeLaunchAlgoServices {
|
||||
// Returns the filled quantity for this algo
|
||||
virtual Quantity total_fills() const = 0;
|
||||
|
||||
// Returns wheter the algo is in the process of shutting down (due
|
||||
// Returns whether the algo is in the process of shutting down (due
|
||||
// to a cancel, an error, or just having completed its work). When
|
||||
// an algo is shutting down any requests to create, update or
|
||||
// cancel child orders will be ignored.
|
||||
|
||||
@@ -0,0 +1,569 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/PyramidAlgo.h"
|
||||
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoServices.h"
|
||||
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h"
|
||||
#include "lib/os/os.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include <stdint.h>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
using namespace api::orderman;
|
||||
|
||||
enum class PyramidAlgo::Status { WAITING_FOR_MD, WAITING_FOR_MD_ATTACH, WORKING, DONE };
|
||||
enum class PyramidAlgo::ExitPath { CANCEL, ERR, COMPLETE };
|
||||
|
||||
struct LegData{
|
||||
const size_t index;
|
||||
const SecurityKey sec_key;
|
||||
const double tick_size;
|
||||
const api::Side side; //trading side
|
||||
ChildTemplate* order_template = nullptr;
|
||||
OrderKey order_key;
|
||||
ChildState order_state;
|
||||
bool recv_book;
|
||||
Book book;
|
||||
Quantity top_delete; // this will be DONTCARE for covers
|
||||
Quantity cover_payup;// this will be DONTCARE for top
|
||||
LegData (size_t index_a,
|
||||
SecurityKey sec_key_a,
|
||||
double tick_size_a,
|
||||
api::Side side_a,
|
||||
ChildTemplate* order_template_a,
|
||||
Quantity qty)
|
||||
:
|
||||
index (index_a),
|
||||
sec_key (sec_key_a),
|
||||
tick_size(tick_size_a),
|
||||
side (side_a),
|
||||
order_template (order_template_a),
|
||||
order_key(null_order_key()),
|
||||
order_state(),
|
||||
recv_book(false),
|
||||
book(),
|
||||
top_delete(index == 0 ? qty.value() : 0),
|
||||
cover_payup(index == 0 ? 0 : qty.value())
|
||||
{
|
||||
}
|
||||
const BookSide& opp_md() const{
|
||||
return book.side(SpreaderUtils::invert_side(side));
|
||||
}
|
||||
bool is_top_spread() const{
|
||||
return index == 0;
|
||||
}
|
||||
void set_book (const Book& book_a){
|
||||
recv_book = true;
|
||||
book = book_a;
|
||||
}
|
||||
};
|
||||
|
||||
struct PyramidAlgo::PImpl {
|
||||
Status state = Status::WAITING_FOR_MD;
|
||||
IAlgoServices* algo_services = nullptr;
|
||||
IBeforeLaunchAlgoServices* bl_algo_services;
|
||||
PyramidAlgo::PyramidAlgoParams params;
|
||||
std::vector<LegData> legs;
|
||||
api::Side side;
|
||||
Price out_price;
|
||||
Quantity out_qty;
|
||||
PImpl(){
|
||||
}
|
||||
LegData* find_leg_from_sec (SecurityKey sec_key){
|
||||
for (auto& l : legs)
|
||||
if(l.sec_key == sec_key)
|
||||
return &l;
|
||||
return nullptr;
|
||||
}
|
||||
LegData* find_leg_from_ord_key (OrderKey ord_key){
|
||||
for (auto& l : legs)
|
||||
if(l.order_key == ord_key)
|
||||
return &l;
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
PyramidAlgo::PyramidAlgo() : me(new PImpl()) {}
|
||||
PyramidAlgo::~PyramidAlgo() {}
|
||||
IAlgoServices& PyramidAlgo::_algo() const { return *me->algo_services; }
|
||||
|
||||
// initialization /////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
OrderOpResult PyramidAlgo::do_before_launch(IBeforeLaunchAlgoServices& serv) {
|
||||
me->bl_algo_services = &serv;
|
||||
auto& def = serv.definition();
|
||||
if (def.has_custom_def() && def.custom_def().has_dynamic()){
|
||||
auto conv_res = _get_creation_params(def.custom_def(), serv);
|
||||
if (std::get<1>(conv_res)){
|
||||
// params are good
|
||||
me->params = std::get<0>(conv_res);
|
||||
me->side = def.side();
|
||||
// top spread
|
||||
auto* t = _create_top_order_template(serv);
|
||||
double tick_size = serv.lookup_security(me->params.top_spread)->contract_spec().low_tick();
|
||||
me->legs.emplace_back(0, me->params.top_spread, tick_size, def.side(), t,
|
||||
me->params.top_delete);
|
||||
// covers
|
||||
size_t i = 0;
|
||||
for (SecurityKey key : me->params.cover_spreads){
|
||||
auto side = SpreaderUtils::invert_side(def.side());
|
||||
auto* t = _create_hedge_order_template(serv,i);
|
||||
double tick_size = serv.lookup_security(key)->contract_spec().low_tick();
|
||||
me->legs.emplace_back(i+1, key, tick_size, side, t, me->params.cover_payups[i]);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
else
|
||||
return {OrderError::ORDER_ERROR_INVALID_PARAMETER,
|
||||
std::get<2>(conv_res)};
|
||||
}
|
||||
else
|
||||
return {OrderError::ORDER_ERROR_INVALID_PARAMETER,
|
||||
std::string("Missing parameters")};
|
||||
return {};
|
||||
}
|
||||
void PyramidAlgo::do_launch(IAlgoServices& algo_services) {
|
||||
me->algo_services = &algo_services;
|
||||
me->out_price = algo_services.price();
|
||||
me->out_qty = algo_services.quantity();
|
||||
for (const LegData& l : me->legs)
|
||||
algo_services.subscribe_book(l.sec_key);
|
||||
}
|
||||
void PyramidAlgo::do_launch_attached(IAlgoServices& algo_services,
|
||||
OrderKey key,
|
||||
ChildState c) {
|
||||
me->algo_services = &algo_services;
|
||||
if(c.quantity != algo_services.quantity() ||
|
||||
!bts::securities::is_same(c.price, algo_services.price())){
|
||||
_cxl_and_end(ExitPath::ERR,
|
||||
"Pyramid price or quantity did not match the working order",
|
||||
true);
|
||||
}
|
||||
else{
|
||||
me->legs[0].order_key = key;
|
||||
me->out_price = c.price;
|
||||
me->out_qty = c.quantity;
|
||||
for (const LegData& l : me->legs)
|
||||
algo_services.subscribe_book(l.sec_key);
|
||||
_switch_state(PyramidAlgo::Status::WAITING_FOR_MD_ATTACH);
|
||||
on_child_change(key,c);
|
||||
}
|
||||
}
|
||||
// updates ////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void PyramidAlgo::do_update(const api::orderman::Update& update) {
|
||||
LOG(OrderMan, Diag, "PyramidAlgo::do_update order:{} state:{} "
|
||||
"receiving update: {}",
|
||||
_algo().order_key().value(),
|
||||
static_cast<int>(me->state),
|
||||
utils::pb::to_string(update));
|
||||
auto conv_res = _get_update_params(update, me->params);
|
||||
if (!std::get<1>(conv_res)){
|
||||
LOG(OrderMan, Warn, "PyramidAlgo::do_update order:{} bad update, ignoring",
|
||||
_algo().order_key().value());
|
||||
return;
|
||||
}
|
||||
const PyramidAlgoParams& algo_params = std::get<0>(conv_res);
|
||||
Price price = Price(update.price());
|
||||
Quantity qty = Quantity(update.quantity());
|
||||
switch(me->state){
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD:
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD_ATTACH:
|
||||
LOG(OrderMan, Info, "PyramidAlgo::do_update order:{} state:{} ignored",
|
||||
_algo().order_key().value(),static_cast<int>(me->state));
|
||||
break;
|
||||
case PyramidAlgo::Status::WORKING:{
|
||||
bool pq_changed = _copy_update(algo_params, price, qty);
|
||||
if(_target_reached()){
|
||||
_cxl_and_end (PyramidAlgo::ExitPath::CANCEL, "Target reached");
|
||||
return;
|
||||
}
|
||||
if (_working_state_met()){
|
||||
if(pq_changed)
|
||||
_modify_top_spread_order();
|
||||
}
|
||||
else
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order cancelled (Cover prices not met)");
|
||||
break;
|
||||
}
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// callbacks //////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void PyramidAlgo::on_book(SecurityKey key, const Book& book) {
|
||||
LegData* leg_data = me->find_leg_from_sec(key);
|
||||
if(leg_data != nullptr){
|
||||
leg_data->set_book(book);
|
||||
_analyze_md_and_act();
|
||||
}
|
||||
}
|
||||
// this only gets called on am explicit algo cancel
|
||||
// from the outside
|
||||
IAlgoOrder::OnCancelResponse PyramidAlgo::on_cancel(){
|
||||
// cancel top spread, let hedges work, cancel algo
|
||||
switch(me->state){
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD:
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD_ATTACH:
|
||||
case PyramidAlgo::Status::WORKING:
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,"Algo cancelled");
|
||||
break;
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
return IAlgoOrder::OnCancelResponse::IGNORE;
|
||||
}
|
||||
void PyramidAlgo::on_child_change(OrderKey key, const ChildState &st) {
|
||||
LOG(OrderMan, Info,
|
||||
"PyramidAlgo::on_child_change key:{} child_key:{} state:{} status:{} "
|
||||
"total_fills:{} last_price:{}",
|
||||
_algo().order_key(),
|
||||
key,
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(st.status),
|
||||
st.total_fills,
|
||||
st.last_price);
|
||||
LegData* leg = me->find_leg_from_ord_key (key);
|
||||
if (me->legs[0].order_key == key){
|
||||
switch(me->state){
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD:
|
||||
bts_assert_msg (false, "No child orders");
|
||||
break;
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD_ATTACH:
|
||||
leg->order_state = st;
|
||||
_algo().update_fill_state(st.total_fills, st.avg_price);
|
||||
_check_for_completion();
|
||||
break;
|
||||
case PyramidAlgo::Status::WORKING:
|
||||
leg->order_state = st;
|
||||
_algo().update_fill_state(st.total_fills, st.avg_price);
|
||||
_send_hedges(st.last_fills);
|
||||
_check_for_completion();
|
||||
break;
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// stateless conversion ///////////////////////////////////////////////////////
|
||||
|
||||
PyramidAlgo::PyramidAlgoParams PyramidAlgo::_convert_params (
|
||||
const CustomOrderDefinition& cod){
|
||||
const DynamicOrderDefinition& def = cod.dynamic();
|
||||
const auto& params = def.order_params();
|
||||
PyramidAlgoParams rv;
|
||||
// top contract
|
||||
auto r = get_if<uint32_t>(params, "Top Spread");
|
||||
rv.top_spread = r.has_value() ? SecurityKey(r.value()) : SecurityKey(0);
|
||||
// top delete
|
||||
r = get_if<uint32_t>(params, "Delete");
|
||||
rv.top_delete = Quantity(r.has_value() ? r.value() : UINT32_MAX);
|
||||
for (size_t i = 0; i < 5; i++){
|
||||
auto idx = std::to_string(i+1);
|
||||
auto b = get_if<uint32_t>(params, "Spread " + idx);
|
||||
if (b.has_value() && b.value() != 0){
|
||||
rv.cover_spreads.emplace_back(b.value());
|
||||
auto pu = get_if<uint32_t>(params, "Spread " + idx + " Payup");
|
||||
rv.cover_payups.emplace_back(pu.has_value() ? pu.value() : UINT32_MAX);
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
std::tuple<PyramidAlgo::PyramidAlgoParams,bool,std::string>
|
||||
PyramidAlgo::_get_creation_params(const CustomOrderDefinition& cod,
|
||||
IBeforeLaunchAlgoServices& serv){
|
||||
PyramidAlgo::PyramidAlgoParams rv = _convert_params(cod);
|
||||
// error checking
|
||||
if(rv.top_spread.value() != serv.security().id())
|
||||
return std::make_tuple(rv,false,
|
||||
"Algo can only be launched from the Top Spread");
|
||||
if(rv.cover_spreads.size() < 2)
|
||||
return std::make_tuple(rv,false,
|
||||
"Not enough cover spreads");
|
||||
if (rv.cover_spreads[0].value() == 0 ||
|
||||
rv.cover_spreads[1].value() == 0)
|
||||
return std::make_tuple(rv,false,
|
||||
"Not enough cover spreads");
|
||||
return std::make_tuple(rv,true,std::string(""));
|
||||
}
|
||||
|
||||
std::tuple<PyramidAlgo::PyramidAlgoParams,bool,std::string>
|
||||
PyramidAlgo::_get_update_params(const Update& update,
|
||||
const PyramidAlgoParams& curr_params){
|
||||
if(update.has_custom() && update.custom().has_dynamic()){
|
||||
// means there is a params change
|
||||
PyramidAlgoParams new_params = _convert_params(update.custom());
|
||||
if(new_params.top_spread != curr_params.top_spread ||
|
||||
new_params.cover_spreads.size() != curr_params.cover_spreads.size())
|
||||
return std::make_tuple(new_params,false,"Contracts can not be updated");
|
||||
for (size_t i=0; i < curr_params.cover_spreads.size(); i++)
|
||||
if(new_params.cover_spreads[i] != curr_params.cover_spreads[i])
|
||||
return std::make_tuple(new_params,false,"Contracts can not be updated");
|
||||
return std::make_tuple(new_params,true,std::string(""));
|
||||
}
|
||||
else
|
||||
return std::make_tuple(curr_params,true,std::string(""));
|
||||
}
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// order template creation/////////////////////////////////////////////////////
|
||||
|
||||
ChildTemplate* PyramidAlgo::_create_top_order_template (
|
||||
IBeforeLaunchAlgoServices& serv) const{
|
||||
OrderDefinition order_def = serv.definition();
|
||||
order_def.set_security_key (me->params.top_spread.value());
|
||||
order_def.set_side(me->side);
|
||||
order_def.mutable_custom_def()->mutable_limit()->MergeFrom (
|
||||
LimitOrderDefinition());
|
||||
return &(serv.make_child_template(order_def));
|
||||
}
|
||||
|
||||
ChildTemplate* PyramidAlgo::_create_hedge_order_template (
|
||||
IBeforeLaunchAlgoServices& serv,
|
||||
size_t hedge_idx) const{
|
||||
OrderDefinition order_def = serv.definition();
|
||||
order_def.set_attach_target_order_key(null_order_key().value());
|
||||
order_def.set_security_key (me->params.cover_spreads[hedge_idx].value());
|
||||
order_def.set_side(SpreaderUtils::invert_side(me->side));
|
||||
order_def.set_sub_type(ORDER_SUB_TYPE_HEDGING_LEG);
|
||||
LeanOrderDefinition lean;
|
||||
lean.mutable_cross_threshold()->set_absolute(me->params.cover_payups[hedge_idx].value());
|
||||
lean.set_max_chase_ticks(1);
|
||||
lean.set_trade_toward_behavior(LeanOrderDefinition::NO_CHANGE);
|
||||
lean.mutable_child()->mutable_limit()->MergeFrom (
|
||||
LimitOrderDefinition());
|
||||
order_def.mutable_custom_def()->mutable_lean()->MergeFrom (lean);
|
||||
return &(serv.make_child_template(order_def));
|
||||
}
|
||||
|
||||
void PyramidAlgo::_analyze_md_and_act(){
|
||||
switch(me->state){
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD:
|
||||
if(!_md_recvd())
|
||||
return;
|
||||
// received data for the first time
|
||||
if (_working_state_met()){
|
||||
// enter order, and switch state
|
||||
LegData& top_leg = me->legs[0];
|
||||
// this is the only place top spread order is sent (once)
|
||||
top_leg.order_key = _algo().launch_child(
|
||||
*top_leg.order_template,
|
||||
me->out_price,
|
||||
me->out_qty);
|
||||
LOG(OrderMan, Info,"PyramidAlgo::_analyze_md_and_act launching order key:{} "
|
||||
"child_key:{} price:{} qty:{}",
|
||||
_algo().order_key(),
|
||||
top_leg.order_key,
|
||||
me->out_price,
|
||||
me->out_qty);
|
||||
_switch_state(PyramidAlgo::Status::WORKING);
|
||||
}
|
||||
else
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order cancelled (Cover prices not met)");
|
||||
break;
|
||||
case PyramidAlgo::Status::WAITING_FOR_MD_ATTACH:
|
||||
if(!_md_recvd())
|
||||
return;
|
||||
// received data for the first time
|
||||
if (_working_state_met())
|
||||
_switch_state(PyramidAlgo::Status::WORKING);
|
||||
else
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order detached (Cover prices not met)",
|
||||
true);
|
||||
break;
|
||||
case PyramidAlgo::Status::WORKING:
|
||||
if (!_working_state_met())
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::CANCEL,
|
||||
"Order cancelled (Cover prices not met)");
|
||||
break;
|
||||
case PyramidAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidAlgo::_switch_state(Status new_state){
|
||||
switch(new_state){
|
||||
case (PyramidAlgo::Status::WAITING_FOR_MD):
|
||||
bts_assert(false);
|
||||
break;
|
||||
case (PyramidAlgo::Status::WAITING_FOR_MD_ATTACH):
|
||||
bts_assert(me->state == PyramidAlgo::Status::WAITING_FOR_MD);
|
||||
break;
|
||||
case (PyramidAlgo::Status::WORKING):
|
||||
bts_assert(me->state == PyramidAlgo::Status::WAITING_FOR_MD ||
|
||||
me->state == PyramidAlgo::Status::WAITING_FOR_MD_ATTACH);
|
||||
break;
|
||||
case (PyramidAlgo::Status::DONE):
|
||||
bts_assert(
|
||||
// when an order is entered and the criteria is not met
|
||||
me->state == PyramidAlgo::Status::WAITING_FOR_MD ||
|
||||
// when an order is attached and the market conditions are not met
|
||||
me->state == PyramidAlgo::Status::WAITING_FOR_MD_ATTACH ||
|
||||
// when the child order gets into an unexpected state
|
||||
me->state == PyramidAlgo::Status::WORKING);
|
||||
break;
|
||||
}
|
||||
LOG(OrderMan, Info,"PyramidAlgo::_switch_state key:{} {}->{} ",
|
||||
_algo().order_key(),
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(new_state));
|
||||
me->state = new_state;
|
||||
}
|
||||
|
||||
void PyramidAlgo::_send_hedges (Quantity filled_qty){
|
||||
if (filled_qty.value() > 0)
|
||||
for (auto& l : me->legs)
|
||||
if(!l.is_top_spread()){
|
||||
const auto& inside_md = l.opp_md()[0];
|
||||
Price hedge_price = Price(inside_md.price());
|
||||
uint32_t inside_md_qty = inside_md.size();
|
||||
if (l.cover_payup.value() != 0 &&
|
||||
inside_md_qty > l.cover_payup.value())
|
||||
hedge_price = securities::worsen_price(l.side, hedge_price, Price(l.tick_size));
|
||||
// check payup
|
||||
l.order_key = _algo().launch_child(
|
||||
*l.order_template,
|
||||
hedge_price,
|
||||
filled_qty);
|
||||
_algo().detach_child(l.order_key);
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidAlgo::_modify_top_spread_order(){
|
||||
Update child_update;
|
||||
child_update.set_price(me->out_price.value());
|
||||
child_update.set_quantity(me->out_qty.value());
|
||||
LOG(OrderMan, Info,"PyramidAlgo::_modify_top_spread_order modifying order key:{} "
|
||||
"child_key:{} price:{} qty:{}",
|
||||
_algo().order_key(),
|
||||
me->legs[0].order_key,
|
||||
me->out_price,
|
||||
me->out_qty);
|
||||
_algo().update_child(me->legs[0].order_key, child_update);
|
||||
}
|
||||
// copies the params, price and qty to the state
|
||||
// retuns true is the price or qty has changed
|
||||
bool PyramidAlgo::_copy_update(const PyramidAlgoParams& algo_params,
|
||||
Price price, Quantity qty){
|
||||
// copy input params (which are stored seperately)
|
||||
me->params.top_delete = algo_params.top_delete;
|
||||
size_t cov_idx = 0;
|
||||
for (Quantity payup : algo_params.cover_payups)
|
||||
me->params.cover_payups[cov_idx++] = payup;
|
||||
// copy leg data
|
||||
cov_idx = 0;
|
||||
me->legs[0].top_delete = algo_params.top_delete;
|
||||
for (Quantity pu : algo_params.cover_payups){
|
||||
LegData& cover_leg = me->legs[cov_idx+1];
|
||||
if(cover_leg.cover_payup != pu){
|
||||
cover_leg.cover_payup = pu;
|
||||
cover_leg.order_template = _create_hedge_order_template(
|
||||
*me->bl_algo_services,
|
||||
cov_idx);
|
||||
}
|
||||
cov_idx++;
|
||||
}
|
||||
// copy price and qty (returns true is they have changed)
|
||||
bool pq_changed = me->out_qty.value() != qty.value() ||
|
||||
!bts::securities::is_same(me->out_price, price);
|
||||
me->out_price = price;
|
||||
me->out_qty = qty;
|
||||
return pq_changed;
|
||||
}
|
||||
// if the top spread order is in a final state, the algo
|
||||
// algo goes to the final state
|
||||
void PyramidAlgo::_check_for_completion(){
|
||||
const ChildState& top_ord = me->legs[0].order_state;
|
||||
switch (top_ord.status){
|
||||
case (ChildStatus::WORKING):
|
||||
break;
|
||||
case (ChildStatus::COMPLETED):
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::ERR,"");
|
||||
break;
|
||||
case (ChildStatus::CANCELED):
|
||||
_cxl_and_end(PyramidAlgo::ExitPath::ERR,
|
||||
"Top spread unexpectedly cancelled");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void PyramidAlgo::_cxl_and_end(PyramidAlgo::ExitPath exit_path,
|
||||
const std::string& msg,
|
||||
bool detach_top){
|
||||
if(me->legs[0].order_key != null_order_key()){
|
||||
const ChildState& top_ord_state = me->legs[0].order_state;
|
||||
if(top_ord_state.status == ChildStatus::WORKING){
|
||||
if(detach_top)
|
||||
_algo().detach_child(me->legs[0].order_key);
|
||||
else
|
||||
_algo().cancel_child(me->legs[0].order_key);
|
||||
}
|
||||
}
|
||||
switch(exit_path){
|
||||
case PyramidAlgo::ExitPath::ERR:
|
||||
_algo().terminate_error(msg);
|
||||
break;
|
||||
case PyramidAlgo::ExitPath::CANCEL:
|
||||
_algo().cancel(msg);
|
||||
break;
|
||||
case PyramidAlgo::ExitPath::COMPLETE:
|
||||
_algo().complete();
|
||||
break;
|
||||
}
|
||||
_switch_state(PyramidAlgo::Status::DONE);
|
||||
}
|
||||
// true the out qty is changed it be below
|
||||
// the filled qty
|
||||
bool PyramidAlgo::_target_reached() const{
|
||||
return me->legs[0].order_state.total_fills.value() >=
|
||||
me->out_qty.value();
|
||||
}
|
||||
|
||||
bool PyramidAlgo::_md_recvd() const{
|
||||
for (const LegData& l : me->legs)
|
||||
if(!l.recv_book)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PyramidAlgo::_working_state_met() const{
|
||||
double cover_sum = 0.0;
|
||||
size_t i = 0;
|
||||
for (const auto& l : me->legs)
|
||||
if (i++ > 0)
|
||||
cover_sum += l.opp_md()[0].price();
|
||||
bool rv = !os::is_nan(cover_sum) &&
|
||||
((me->side == api::Side::SIDE_BUY &&
|
||||
me->out_price.value() <= cover_sum) ||
|
||||
(me->side == api::Side::SIDE_SELL &&
|
||||
me->out_price.value() >= cover_sum));
|
||||
if (!rv && !os::is_nan(cover_sum)){
|
||||
// price condition is not met, but if the
|
||||
// cum qty at the top leg price is > threashold
|
||||
// then the condition is met ()
|
||||
const auto& top_leg = me->legs[0];
|
||||
Quantity cumm_level_qty;
|
||||
SpreaderUtils::find_qty_upto_price_level(top_leg.book,
|
||||
top_leg.side,
|
||||
Price(me->out_price),
|
||||
0.0,
|
||||
cumm_level_qty);
|
||||
rv = cumm_level_qty.value() > top_leg.top_delete.value();
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,74 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
class ChildTemplate;
|
||||
class LegData;
|
||||
|
||||
class PyramidAlgo : public IAlgoOrder {
|
||||
public:
|
||||
PyramidAlgo();
|
||||
~PyramidAlgo();
|
||||
struct PyramidAlgoParams{
|
||||
// first one is the top
|
||||
SecurityKey top_spread;
|
||||
Quantity top_delete;
|
||||
std::vector<SecurityKey> cover_spreads;
|
||||
std::vector<Quantity> cover_payups;
|
||||
};
|
||||
public:
|
||||
OrderOpResult do_before_launch(IBeforeLaunchAlgoServices &) override;
|
||||
void do_launch(IAlgoServices &) override;
|
||||
void do_launch_attached(IAlgoServices &, OrderKey, ChildState) override;
|
||||
void do_update(api::orderman::Update const &) override;
|
||||
void on_book(SecurityKey, Book const &) override;
|
||||
void on_child_change(OrderKey, ChildState const &) override;
|
||||
IAlgoOrder::OnCancelResponse on_cancel() override;
|
||||
|
||||
private:
|
||||
enum class Status;
|
||||
enum class ExitPath;
|
||||
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me;
|
||||
private:
|
||||
IAlgoServices& _algo() const ;
|
||||
// converts parameters from the CustomOrderDefinition::dynamic
|
||||
// to PyramidAlgoParams
|
||||
static PyramidAlgoParams _convert_params (
|
||||
const bts::api::orderman::CustomOrderDefinition&);
|
||||
static std::tuple<PyramidAlgoParams,bool,std::string> _get_creation_params(
|
||||
const bts::api::orderman::CustomOrderDefinition&,
|
||||
IBeforeLaunchAlgoServices&);
|
||||
static std::tuple<PyramidAlgoParams,bool,std::string> _get_update_params(
|
||||
const api::orderman::Update&,
|
||||
const PyramidAlgoParams& curr_params);
|
||||
ChildTemplate* _create_top_order_template (IBeforeLaunchAlgoServices&) const;
|
||||
ChildTemplate* _create_hedge_order_template (IBeforeLaunchAlgoServices&,
|
||||
size_t) const;
|
||||
// acts on md, cognizant of the state
|
||||
void _analyze_md_and_act();
|
||||
void _switch_state (Status new_status);
|
||||
void _send_hedges (Quantity filled_qty);
|
||||
void _modify_top_spread_order();
|
||||
// returns true is the price, qty changes
|
||||
bool _copy_update(const PyramidAlgoParams& algo_params,
|
||||
Price price, Quantity qty);
|
||||
void _check_for_completion();
|
||||
// cxl's the working top leg order (if any), detaches
|
||||
// all hedges. if is_error then terminates algo, is
|
||||
// is_cxl then cxl's algo, otherwise completes it
|
||||
void _cxl_and_end(ExitPath exit_path,
|
||||
const std::string& err_txt,
|
||||
bool detach_top = false);
|
||||
bool _target_reached() const;
|
||||
// true is all legs have recvd md
|
||||
bool _md_recvd() const;
|
||||
bool _working_state_met() const;
|
||||
};
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -3,7 +3,11 @@
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/logging/logging.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/config/protos/toggles.pb.h"
|
||||
#include "lib/config/config.h"
|
||||
#include "DormanAlgo.h"
|
||||
#include "PyramidAlgo.h"
|
||||
#include "TASAlgo.h"
|
||||
|
||||
using namespace bts;
|
||||
using namespace api::orderman;
|
||||
@@ -11,7 +15,7 @@ using namespace orderman::dorman;
|
||||
using namespace orderman;
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
namespace orderman {
|
||||
namespace dorman {
|
||||
|
||||
template <class OK,class ERR>
|
||||
@@ -83,7 +87,7 @@ Result<AlgoParams,OrderOpResult> to_dorman_params(const api::orderman::DynamicOr
|
||||
return {res};
|
||||
}
|
||||
|
||||
extern "C" void init_spark_plugin(bts::SparkServices &srv) {
|
||||
extern "C" void init_dorman_plugin(bts::SparkServices &srv) {
|
||||
DynamicOrderDescriptor join_desc;
|
||||
|
||||
join_desc.set_dynamic_order_type("Dorman");
|
||||
@@ -115,4 +119,55 @@ extern "C" void init_spark_plugin(bts::SparkServices &srv) {
|
||||
srv.register_dynamic_algo_order(join_desc, []() { return std::make_unique<DormanAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_pyramid_plugin(bts::SparkServices& srv) {
|
||||
DynamicOrderDescriptor join_desc;
|
||||
|
||||
join_desc.set_dynamic_order_type("Pyramid");
|
||||
|
||||
*join_desc.add_param_desc() = param_security_key("Top Spread");
|
||||
*join_desc.add_param_desc() = param_uint32("Delete");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 1");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 1 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 1 Order");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 2");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 2 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 2 Order");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 3");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 3 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 3 Order");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 4");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 4 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 4 Order");
|
||||
*join_desc.add_param_desc() = param_security_key("Spread 5");
|
||||
*join_desc.add_param_desc() = param_uint32("Spread 5 Payup");
|
||||
*join_desc.add_param_desc() = param_order("Spread 5 Order");
|
||||
|
||||
srv.register_dynamic_algo_order(join_desc, []() { return std::make_unique<orderman::PyramidAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_tas_plugin(bts::SparkServices& srv) {
|
||||
DynamicOrderDescriptor tas_desc;
|
||||
|
||||
tas_desc.set_dynamic_order_type("TAS");
|
||||
|
||||
size_t n_child_ords = 8;
|
||||
for (size_t i=0; i< n_child_ords; i++){
|
||||
auto idx = std::to_string(i+1);
|
||||
*tas_desc.add_param_desc() = param_double("Order " + idx + " Price");
|
||||
*tas_desc.add_param_desc() = param_uint32("Order " + idx + " Quantity");
|
||||
*tas_desc.add_param_desc() = param_bool("Order " + idx + " Buy?");
|
||||
}
|
||||
srv.register_dynamic_algo_order(tas_desc, []() { return std::make_unique<orderman::TASAlgo>(); });
|
||||
}
|
||||
|
||||
extern "C" void init_spark_plugin(bts::SparkServices &srv) {
|
||||
auto dyn_algo_config = config::get<config::toggles::DynamicAlgos>();
|
||||
if(dyn_algo_config.enable_dorman())
|
||||
init_dorman_plugin(srv);
|
||||
if(dyn_algo_config.enable_pyramid())
|
||||
init_pyramid_plugin(srv);
|
||||
if(dyn_algo_config.enable_tas())
|
||||
init_tas_plugin(srv);
|
||||
}
|
||||
|
||||
}}}
|
||||
@@ -0,0 +1,222 @@
|
||||
#include "lib/orderman/orders/algo/algo_orders/TASAlgo.h"
|
||||
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/orderman/order_factory/IOrderFactory.h"
|
||||
#include "lib/orderman/orderman_utils/Book.h"
|
||||
#include "lib/orderman/orderman_utils/orderman_utils.h"
|
||||
#include "lib/orderman/orders/algo/IAlgoServices.h"
|
||||
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h"
|
||||
#include "lib/os/os.h"
|
||||
#include "lib/domain_model/Security.h"
|
||||
#include "lib/utils/PBUtils.h"
|
||||
#include "lib/utils/datetime.h"
|
||||
#include <stdint.h>
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
using namespace api::orderman;
|
||||
|
||||
enum class TASAlgo::Status { WAITING_FOR_SIGNAL, WORKING, DONE };
|
||||
|
||||
struct TASLegData{
|
||||
api::Side side; //trading side
|
||||
Price price;
|
||||
Quantity quantity;
|
||||
ChildTemplate* order_template = nullptr;
|
||||
OrderKey order_key;
|
||||
ChildState order_state;
|
||||
TASLegData (api::Side side,
|
||||
Price price,
|
||||
Quantity quantity,
|
||||
ChildTemplate* order_template)
|
||||
:
|
||||
side (side),
|
||||
price(price),
|
||||
quantity(quantity),
|
||||
order_template (order_template),
|
||||
order_key(null_order_key()),
|
||||
order_state(){
|
||||
order_state.status = ChildStatus::WORKING;
|
||||
}
|
||||
};
|
||||
|
||||
struct TASAlgo::PImpl {
|
||||
Status state = Status::WAITING_FOR_SIGNAL;
|
||||
IAlgoServices* algo_services = nullptr;
|
||||
SecurityKey sec_key = SecurityKey(0);
|
||||
TASAlgo::TASAlgoParams algo_params;
|
||||
std::vector<TASLegData> legs;
|
||||
bts::datetime::PTime start_time = bts::datetime::local_now();
|
||||
PImpl(){
|
||||
}
|
||||
TASLegData* find_leg_from_ord_key (OrderKey ord_key){
|
||||
for (auto& l : legs)
|
||||
if(l.order_key == ord_key)
|
||||
return &l;
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
TASAlgo::TASAlgo() : me(new PImpl()) {}
|
||||
TASAlgo::~TASAlgo() {}
|
||||
// initialization /////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
OrderOpResult TASAlgo::do_before_launch(IBeforeLaunchAlgoServices& serv) {
|
||||
auto& def = serv.definition();
|
||||
if (def.has_custom_def() && def.custom_def().has_dynamic()){
|
||||
const auto& sec = serv.security();
|
||||
me->sec_key = SecurityKey(sec.id());
|
||||
me->algo_params = _convert_params(def.custom_def());
|
||||
if (me->algo_params.child_orders.size() > 0){
|
||||
for (const auto& t : me->algo_params.child_orders){
|
||||
Price price = std::get<0>(t);
|
||||
Quantity quantity = std::get<1>(t);
|
||||
api::Side side = std::get<2>(t);
|
||||
auto* ordt = _create_order_template(
|
||||
serv,
|
||||
SecurityKey(sec.id()),
|
||||
side);
|
||||
me->legs.emplace_back(side,
|
||||
price,
|
||||
quantity,
|
||||
ordt);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return {OrderError::ORDER_ERROR_INVALID_PARAMETER,
|
||||
std::string("Parameters are not well formed")};
|
||||
}
|
||||
void TASAlgo::do_launch(IAlgoServices& algo_services) {
|
||||
me->algo_services = &algo_services;
|
||||
_algo().subscribe_status(me->sec_key);
|
||||
//_algo().subscribe_timer();
|
||||
}
|
||||
void TASAlgo::do_launch_attached(IAlgoServices& algo_services,
|
||||
OrderKey,
|
||||
ChildState) {
|
||||
me->algo_services = &algo_services;
|
||||
_cxl_and_end(true, "TAS Algo can not be attached to working orders");
|
||||
}
|
||||
// updates ////////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void TASAlgo::do_update(const api::orderman::Update&) {
|
||||
LOG(OrderMan, Warn, "TASAlgo::do_update order:{} state:{} update not allowed ",
|
||||
_algo().order_key().value(),
|
||||
static_cast<int>(me->state));
|
||||
}
|
||||
// callbacks //////////////////////////////////////////////////////////////////
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void TASAlgo::on_book(SecurityKey, const Book&) {
|
||||
}
|
||||
void TASAlgo::on_child_change(OrderKey key,
|
||||
ChildState const &st){
|
||||
LOG(OrderMan, Info,
|
||||
"TASAlgo::on_child_change key:{} child_key:{} state:{} status:{} "
|
||||
"total_fills:{} last_price:{}",
|
||||
_algo().order_key(),
|
||||
key,
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(st.status),
|
||||
st.total_fills,
|
||||
st.last_price);
|
||||
TASLegData* ld_ptr = me->find_leg_from_ord_key(key);
|
||||
if (ld_ptr != nullptr)
|
||||
ld_ptr->order_state = st;
|
||||
for (const auto& l : me->legs)
|
||||
if(l.order_state.status == ChildStatus::WORKING)
|
||||
return;
|
||||
_cxl_and_end(false,"");
|
||||
}
|
||||
void TASAlgo::on_status(SecurityKey key, co2::TradingStatus status){
|
||||
if(key == me->sec_key &&
|
||||
status == co2::TradingStatus::TRADINGSTATUS_PRE_OPEN){
|
||||
switch (me->state){
|
||||
case TASAlgo::Status::WAITING_FOR_SIGNAL:
|
||||
for (auto& l : me->legs){
|
||||
l.order_key = _algo().launch_child(
|
||||
*l.order_template,
|
||||
l.price,
|
||||
l.quantity);
|
||||
LOG(OrderMan, Info,"TASAlgo::on_status launching order key:{} "
|
||||
"child_key:{} price:{} qty:{}",
|
||||
_algo().order_key(),
|
||||
l.order_key,
|
||||
l.price,
|
||||
l.quantity);
|
||||
}
|
||||
_switch_state(TASAlgo::Status::WORKING);
|
||||
break;
|
||||
case TASAlgo::Status::WORKING:
|
||||
case TASAlgo::Status::DONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
void TASAlgo::on_timer(){
|
||||
if (bts::datetime::msecs_since(me->start_time) > 10000)
|
||||
on_status(me->sec_key,co2::TradingStatus::TRADINGSTATUS_PRE_OPEN);
|
||||
}
|
||||
IAlgoServices& TASAlgo::_algo() const { return *me->algo_services; }
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// stateless conversion ///////////////////////////////////////////////////////
|
||||
TASAlgo::TASAlgoParams TASAlgo::_convert_params (
|
||||
const bts::api::orderman::CustomOrderDefinition& cod){
|
||||
const DynamicOrderDefinition& def = cod.dynamic();
|
||||
const auto& params = def.order_params();
|
||||
TASAlgo::TASAlgoParams rv;
|
||||
for (size_t i = 0; i < 1024; i++){
|
||||
auto idx = std::to_string(i+1);
|
||||
auto price = get_if<double>(params, "Order " + idx + " Price");
|
||||
auto qty = get_if<uint32_t>(params, "Order " + idx + " Quantity");
|
||||
auto is_buy = get_if<bool>(params, "Order " + idx + " Buy?");
|
||||
if(price.has_value() && qty.has_value() && qty.value() != 0){
|
||||
api::Side side = is_buy.has_value() ?
|
||||
(is_buy.value() ? api::Side::SIDE_BUY : api::Side::SIDE_SELL) :
|
||||
api::Side::SIDE_SELL;
|
||||
rv.child_orders.emplace_back(Price(price.value()), Quantity(qty.value()), side);
|
||||
}
|
||||
}
|
||||
return rv;
|
||||
}
|
||||
ChildTemplate* TASAlgo::_create_order_template (
|
||||
IBeforeLaunchAlgoServices& serv,
|
||||
SecurityKey key,
|
||||
api::Side side){
|
||||
OrderDefinition order_def = serv.definition();
|
||||
order_def.set_security_key (key.value());
|
||||
order_def.set_side(side);
|
||||
order_def.mutable_custom_def()->mutable_limit()->MergeFrom (
|
||||
api::orderman::LimitOrderDefinition());
|
||||
return &(serv.make_child_template(order_def));
|
||||
}
|
||||
void TASAlgo::_switch_state(TASAlgo::Status new_state){
|
||||
switch (new_state){
|
||||
case (TASAlgo::Status::WAITING_FOR_SIGNAL):
|
||||
bts_assert(false);
|
||||
break;
|
||||
case (TASAlgo::Status::WORKING):
|
||||
bts_assert(me->state == TASAlgo::Status::WAITING_FOR_SIGNAL);
|
||||
break;
|
||||
case (TASAlgo::Status::DONE):
|
||||
bts_assert(me->state != TASAlgo::Status::DONE);
|
||||
break;
|
||||
}
|
||||
LOG(OrderMan, Info,"TASAlgo::_switch_state key:{} {}->{} ",
|
||||
_algo().order_key(),
|
||||
static_cast<int>(me->state),
|
||||
static_cast<int>(new_state));
|
||||
me->state = new_state;
|
||||
}
|
||||
|
||||
void TASAlgo::_cxl_and_end (bool is_error, const std::string& err_msg){
|
||||
if(is_error)
|
||||
_algo().terminate_error(err_msg);
|
||||
else
|
||||
_algo().complete();
|
||||
_switch_state(TASAlgo::Status::DONE);
|
||||
}
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -0,0 +1,45 @@
|
||||
#pragma once
|
||||
|
||||
#include "lib/orderman/orders/algo/IAlgoOrder.h"
|
||||
|
||||
namespace bts {
|
||||
namespace orderman {
|
||||
|
||||
class ChildTemplate;
|
||||
class LegData;
|
||||
|
||||
class TASAlgo : public IAlgoOrder {
|
||||
public:
|
||||
TASAlgo();
|
||||
~TASAlgo();
|
||||
struct TASAlgoParams{
|
||||
std::vector<std::tuple<Price, Quantity, api::Side>> child_orders;
|
||||
};
|
||||
public:
|
||||
OrderOpResult do_before_launch(IBeforeLaunchAlgoServices &) override;
|
||||
void do_launch(IAlgoServices &) override;
|
||||
void do_launch_attached(IAlgoServices &, OrderKey, ChildState) override;
|
||||
void do_update(api::orderman::Update const &) override;
|
||||
void on_book(SecurityKey, Book const &) override;
|
||||
void on_child_change(OrderKey, ChildState const &) override;
|
||||
void on_status(SecurityKey, co2::TradingStatus) override;
|
||||
void on_timer() override;
|
||||
|
||||
private:
|
||||
enum class Status;
|
||||
struct PImpl;
|
||||
std::unique_ptr<PImpl> me;
|
||||
private:
|
||||
IAlgoServices& _algo() const ;
|
||||
static TASAlgoParams _convert_params (
|
||||
const bts::api::orderman::CustomOrderDefinition&);
|
||||
static ChildTemplate* _create_order_template (IBeforeLaunchAlgoServices&,
|
||||
SecurityKey,
|
||||
api::Side);
|
||||
// acts on md, cognizant of the state
|
||||
void _switch_state (Status new_status);
|
||||
void _cxl_and_end (bool is_error, const std::string& err_msg);
|
||||
};
|
||||
|
||||
} // namespace orderman
|
||||
} // namespace bts
|
||||
@@ -463,6 +463,7 @@ void SpreaderOrder::_comp_and_report_state (){
|
||||
reported_state.set_quantity(bookkeeping_data.quantity().value());
|
||||
reported_state.set_filled_quantity(filled_qty.value());
|
||||
reported_state.set_avg_price(avg_filled_price.value());
|
||||
reported_state.set_last_price(avg_filled_price.value());
|
||||
}
|
||||
// if order is cons'ed from disc, with inited_new==false,
|
||||
// we just publish what was given to us
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "lib/orderman/orders/spreader_orders/spreader/quoter/QuoteLegDriver.h"
|
||||
#include "lib/api/services/OrderService.pb.h"
|
||||
#include "lib/orderman/actors/order_manager/OrderBuilder.h"
|
||||
#include "lib/os/os.h"
|
||||
#include <random>
|
||||
|
||||
namespace bts {
|
||||
@@ -238,11 +239,9 @@ public:
|
||||
}
|
||||
}
|
||||
// CRITICAL PATH (for hedging)
|
||||
// travserses the book to locate the target_price,
|
||||
// and finds the qty at the target price and the
|
||||
// cum qty upto that price.
|
||||
// returns false if the target_price is not found.
|
||||
// this is used to find the qty and cumm_qty
|
||||
// travserses the book to locate the target_price,
|
||||
// and finds the cumm qty up to that level (last our arg)
|
||||
// Returns false if the target_price is not found.
|
||||
static bool find_qty_upto_price_level (const Book& book,
|
||||
api::Side book_side,
|
||||
Price target_price,
|
||||
@@ -286,35 +285,66 @@ public:
|
||||
////////////////////////////////////////////////////
|
||||
// quoting params //////////////////////////////////
|
||||
////////////////////////////////////////////////////
|
||||
// given the new price, applies the quoting parameters (mute etc)
|
||||
// to see if quote needs to be repriced
|
||||
static bool reprice_quote_mute_zone(double current_price,
|
||||
double new_price,
|
||||
api::Side quote_side,
|
||||
double tick_size,
|
||||
const bts::spark::QuoteMuteZone& quote_mute_zone){
|
||||
int price_change_ticks = bts::securities::price_diff_to_ticks (current_price, new_price, tick_size);
|
||||
if (price_change_ticks != 0){
|
||||
bool price_increasing = price_change_ticks > 0;
|
||||
uint32_t abs_price_change_ticks = static_cast<uint32_t>(std::abs(price_change_ticks));
|
||||
LOG(OrderMan, Diag, "SpreaderUtils::reprice_quote_mute_zone current_price: {} new_price: {} price_change_ticks: {} abs_price_change_ticks: {} inside_limit_ticks: {} outside_limit_ticks: {}",
|
||||
current_price, new_price, price_change_ticks, abs_price_change_ticks, quote_mute_zone.inside_ticks(), quote_mute_zone.outside_ticks());
|
||||
if (quote_side == api::SIDE_BUY){
|
||||
if (price_increasing) // moving toward inside market
|
||||
return abs_price_change_ticks > quote_mute_zone.inside_ticks();
|
||||
else
|
||||
return abs_price_change_ticks > quote_mute_zone.outside_ticks();
|
||||
}
|
||||
else{
|
||||
if (!price_increasing) // moving toward inside market
|
||||
return abs_price_change_ticks > quote_mute_zone.inside_ticks();
|
||||
else
|
||||
return abs_price_change_ticks > quote_mute_zone.outside_ticks();
|
||||
}
|
||||
}
|
||||
else
|
||||
return false;
|
||||
static bool is_throttle_reject(double current_price,
|
||||
double new_price,
|
||||
const Book& qbook,
|
||||
api::Side quote_side,
|
||||
double tick_size,
|
||||
const bts::spark::QuoteParams& quote_params){
|
||||
|
||||
bool reject =
|
||||
(quote_params.has_quote_mute_zone() &&
|
||||
is_quote_mute_zone_reject(current_price,
|
||||
new_price,
|
||||
quote_side,
|
||||
tick_size,
|
||||
quote_params.quote_mute_zone())) ||
|
||||
(quote_params.has_quote_inside_market_distance_throttle() &&
|
||||
is_quote_inside_market_reject(current_price,
|
||||
new_price,
|
||||
qbook.side(quote_side)[0].price(),
|
||||
quote_side,
|
||||
tick_size,
|
||||
quote_params.quote_inside_market_distance_throttle()));
|
||||
return reject;
|
||||
}
|
||||
|
||||
// returns true is quote mute zone rejects the price change
|
||||
static bool is_quote_mute_zone_reject(double current_price,
|
||||
double new_price,
|
||||
api::Side quote_side,
|
||||
double tick_size,
|
||||
const bts::spark::QuoteMuteZone& quote_mute_zone){
|
||||
int ticks_moved = bts::securities::price_diff_to_ticks(current_price, new_price, tick_size);
|
||||
bool toward_inside = quote_side==api::Side::SIDE_BUY ? ticks_moved >=0 : ticks_moved <= 0;
|
||||
uint32_t throttle = toward_inside ? quote_mute_zone.inside_ticks() :
|
||||
quote_mute_zone.outside_ticks();
|
||||
uint32_t abs_ticks_moved = static_cast<uint32_t>(std::abs(ticks_moved));
|
||||
// has to move outside of the zone
|
||||
return abs_ticks_moved <= throttle;
|
||||
}
|
||||
// returns false if the param surpresses the quote
|
||||
static bool is_quote_inside_market_reject(
|
||||
double current_price,
|
||||
double new_price,
|
||||
double inside_market,
|
||||
api::Side quote_side,
|
||||
double tick_size,
|
||||
const bts::spark::QuoteInsideMarketDistanceThrottle& imd){
|
||||
bool toward = bts::securities::is_equal_or_better(quote_side,
|
||||
Price(new_price),
|
||||
Price(current_price));
|
||||
if(toward && !os::is_nan(inside_market)){
|
||||
int new_order_diff_market = bts::securities::order_diff_market(
|
||||
new_price,
|
||||
inside_market,
|
||||
quote_side,
|
||||
tick_size);
|
||||
return new_order_diff_market > static_cast<int>(imd.on_toward_distance());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool conditional_quoting(Price quoting_price,
|
||||
Quantity quoting_qty,
|
||||
size_t leg_idx,
|
||||
|
||||
+21
-9
@@ -73,19 +73,31 @@ void QuoterWorkingStateWorker::_longpath_analyze_and_act ()
|
||||
}
|
||||
else{
|
||||
// to adjust for what has already been filled
|
||||
Quantity order_quantity = Quantity (computed_quantity.value() + _quoter->_price_quantity_state.aggregated_filled_quantity().value());
|
||||
if (order_quantity.value() != _quoter->_price_quantity_state.out_quantity().value())
|
||||
Quantity order_quantity = Quantity (
|
||||
computed_quantity.value() +
|
||||
_quoter->_price_quantity_state.aggregated_filled_quantity().value()
|
||||
);
|
||||
if (order_quantity != _quoter->_price_quantity_state.out_quantity())
|
||||
// this is a size change, size changes always go through regardless of the
|
||||
// quoting parameters
|
||||
_modify_order (computed_price, order_quantity);
|
||||
else{
|
||||
bool reprice = true;
|
||||
if (_quoter->_this_leg_data.leg_params().quote_params().has_quote_mute_zone())
|
||||
reprice = SpreaderUtils::reprice_quote_mute_zone(_quoter->_price_quantity_state.out_price().value(), computed_price.value(), _quoter->_this_leg_data.trading_side(),
|
||||
_quoter->_this_leg_data.tick_size(), _quoter->_this_leg_data.leg_params().quote_params().quote_mute_zone());
|
||||
reprice=reprice && !(_quoter->_spreader.in_cache_warming_cycle());
|
||||
if (reprice)
|
||||
_modify_order (computed_price, order_quantity);
|
||||
Price curr_price = _quoter->_price_quantity_state.out_price();
|
||||
if(!bts::securities::is_same(curr_price,computed_price)){
|
||||
const Book& book =
|
||||
_quoter->_spreader.get_book(_quoter->_this_leg_data.leg_index());
|
||||
bool throttle_reject = SpreaderUtils::is_throttle_reject(
|
||||
curr_price.value(),
|
||||
computed_price.value(),
|
||||
book,
|
||||
_quoter->_this_leg_data.trading_side(),
|
||||
_quoter->_this_leg_data.tick_size(),
|
||||
_quoter->_this_leg_data.leg_params().quote_params());
|
||||
bool reprice = !throttle_reject;
|
||||
reprice = reprice && !(_quoter->_spreader.in_cache_warming_cycle());
|
||||
if (reprice)
|
||||
_modify_order (computed_price, order_quantity);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ message QuoteParams
|
||||
QuoteMaxOrderMove quote_max_order_move = 1040;
|
||||
QuoteHidden quote_hidden = 1050;
|
||||
ConditionalQuoting conditional_quoting=1060;
|
||||
QuoteInsideMarketDistanceThrottle quote_inside_market_distance_throttle = 1070;
|
||||
// default value 0, 1 means we will have one trailing
|
||||
// order behind the top level
|
||||
// (once we have ChildOrder's here, we can reture this
|
||||
@@ -129,6 +130,11 @@ message ConditionalQuoting{
|
||||
PriceType price_type=10;
|
||||
}
|
||||
|
||||
message QuoteInsideMarketDistanceThrottle
|
||||
{
|
||||
uint32 on_toward_distance = 10;
|
||||
}
|
||||
|
||||
/*************************************Quote RepriceThrottled (unused)*******************************
|
||||
(Stellar's sensitivity)
|
||||
This parameter defines regions (wrt their distance from the inside market)
|
||||
@@ -174,7 +180,8 @@ THEN
|
||||
message QuoteInsideMarketDistance
|
||||
{
|
||||
RulePriority priority = 1;
|
||||
uint32 ticks_from_inside = 10;
|
||||
uint32 on_toward_distance = 10;
|
||||
uint32 on_away_distance = 20;
|
||||
}
|
||||
|
||||
/******************************Quote InsideMarketDistanceWithTravel******************************
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <quickfix/SocketAcceptor.h>
|
||||
#include <quickfix/SessionSettings.h>
|
||||
#include <boost/algorithm/string/replace.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <iostream>
|
||||
@@ -34,8 +35,8 @@ auto cfg =
|
||||
"SocketReuseAddress=Y\n"
|
||||
"StartTime=06:02:00\n"
|
||||
"EndTime=06:01:00\n"
|
||||
"FileLogPath=$BTS_DATA_PATH/exchange_sim/log\n"
|
||||
"FileStorePath=$BTS_DATA_PATH/exchange_sim/store\n"
|
||||
"FileLogPath=$BTS_DATA_PATH/exchange_sim/$SIM_VENUE/log\n"
|
||||
"FileStorePath=$BTS_DATA_PATH/exchange_sim/$SIM_VENUE/store\n"
|
||||
"HeartBtInt=30\n"
|
||||
"[SESSION]\n"
|
||||
"BeginString=FIX.4.2\n"
|
||||
@@ -131,8 +132,9 @@ int start_exchange_sim(bts::api::Venue venue, md::MBOWriter* mbo)
|
||||
try
|
||||
{
|
||||
auto str_settings = std::string(cfg);
|
||||
auto venue_string = api::Venue_Name(venue);
|
||||
boost::replace_all(str_settings,"$BTS_DATA_PATH", os::bts_data_path() + "_" + venue_string);
|
||||
auto venue_string = boost::to_lower_copy(api::Venue_Name(venue));
|
||||
boost::replace_all(str_settings,"$BTS_DATA_PATH", os::bts_data_path());
|
||||
boost::replace_all(str_settings,"$SIM_VENUE", venue_string);
|
||||
std::string port_string;
|
||||
switch(venue)
|
||||
{
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "server/sparkd/exchange_sim/book/book_transform.h"
|
||||
|
||||
#include <boost/date_time/posix_time/posix_time.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
#include <fmt/format.h>
|
||||
#include <mutex>
|
||||
#include <quickfix/fix42/ExecutionReport.h>
|
||||
@@ -113,7 +114,7 @@ struct ExchangeSimApplication::PImpl {
|
||||
timer(utils::Dispatcher::background(), 200,
|
||||
[=]() { producer.heartbeat(); }),
|
||||
do_merge_with_md(config::get<config::ExchangeSim>().merge_with_md()),
|
||||
store(new persistence::SqliteStore("ex_sim_orders_" + api::Venue_Name(venue))),
|
||||
store( new persistence::SqliteStore("ex_sim_orders_" + boost::to_lower_copy(api::Venue_Name(venue)) )),
|
||||
lock(),
|
||||
mbo(mbo) {}
|
||||
|
||||
|
||||
@@ -55,6 +55,11 @@ void ShellLatencyServer::report(
|
||||
}
|
||||
}
|
||||
|
||||
void ShellLatencyServer::ping(
|
||||
const ::bts::metrics::BufferMessage& request,
|
||||
const api::Response<::bts::metrics::BufferMessage>& reply) {
|
||||
reply(request);
|
||||
}
|
||||
|
||||
|
||||
}}
|
||||
@@ -28,6 +28,10 @@ class ShellLatencyServer : public ShellLatencyServiceBase {
|
||||
void report(
|
||||
const ::bts::metrics::Report& request,
|
||||
const api::Response<::bts::api::VoidMessage>& reply) override;
|
||||
|
||||
void ping(
|
||||
const ::bts::metrics::BufferMessage& request,
|
||||
const api::Response<::bts::metrics::BufferMessage>& reply) override;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
This test verifies that users can only see accounts that are not marked as deleted
|
||||
|
||||
1. Start a client and the admin gui, choose a user other than bts
|
||||
|
||||
2. In the admin gui, create an account whose name is Prada
|
||||
|
||||
3. For the designated user, add permission to see the Prada account
|
||||
|
||||
4. Verify the you can see the account in the client
|
||||
|
||||
5. In the admin gui, delete the account
|
||||
|
||||
6. Verify the gui no longer shows the Prada (or DELETED_Prada, etc) account
|
||||
@@ -8,3 +8,7 @@ Make sure the following columns have filters when the window starts:
|
||||
|
||||
Cross an order, get a fill and make sure it shows up
|
||||
Test sorting and filtering
|
||||
|
||||
Root algo:
|
||||
Enter an iceberg order and force fills. Make sure the Root Algo column shows the Iceberg as the root algo.
|
||||
Now enter a speader order and force fills. Make sure the Root Algo column shows the name of the spreader.
|
||||
@@ -0,0 +1,16 @@
|
||||
Manual Fills Test*
|
||||
|
||||
Manual fills should now show Admin fills
|
||||
- Open the Trading -> Manual fills form
|
||||
- Verify the form is completely empty as there should be no manual fills yet
|
||||
|
||||
- In the Admin UI, click on Accounts -> Position Management
|
||||
- Add a fill for the bts user with a qty of 100
|
||||
- Click Save
|
||||
|
||||
- Back in the Manual fills form, verify the form is still empty
|
||||
- Open up Trading -> PnL
|
||||
- Verify the PnL shows a row for the trade you entered
|
||||
|
||||
|
||||
* = We need to flesh out this test as it currently only tests 1 narrow piece of functionality
|
||||
@@ -64,6 +64,12 @@ Filter/Collapse interaction
|
||||
- Now filter for order type = limit, you should only see the hedges and the currently working limit
|
||||
- Now filter for order type = IB and you should see nothing
|
||||
|
||||
Parent/Child
|
||||
------------
|
||||
- Enter an algo fo your choice. Verify that the parent child algos show correctly.
|
||||
- Enter a spreader. The spreader algo should show the name of the spreader as well. Verify
|
||||
that the filter works for this (the filter only shows the spreader badge).
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -82,8 +82,15 @@ Prepare: open a ladder for a security that has steady market data activity (in t
|
||||
- enter a limit order ACROSS the market to trigger an immediate fill.
|
||||
- this time, the "When order is filled" sound should play; the "When order is canceled" sound should not be heard.
|
||||
|
||||
7) "Any Order" sent sounds:
|
||||
Description:
|
||||
You will be verifying that the order sent sound trigger works
|
||||
|
||||
- in the settings dialog, change "When order is filled" to "When order is sent", then click apply
|
||||
- in the ladder, enter a limit order away from the market, so it is NOT liable to get filled.
|
||||
- you should hear the sound you selected for "When order is sent"
|
||||
|
||||
7) Saved with workspace:
|
||||
8) Saved with workspace:
|
||||
----Note----------------------------------------------------------------------------------------------------------------------
|
||||
- you have completed the sound-functionality testing, don't be alarmed by the 4 sound triggers you are about to add.
|
||||
- this step just requires comparing 2 images of the settings dialog to confirm that they were retained with the workspace.
|
||||
@@ -99,7 +106,7 @@ Prepare: open a ladder for a security that has steady market data activity (in t
|
||||
- close the shell, then restart it with the "Where I left off on" workspace.
|
||||
- open the OWSD to verify that the sound triggers were saved with the workspace.
|
||||
|
||||
8) Test shell does not crash when no sound card present
|
||||
9) Test shell does not crash when no sound card present
|
||||
- use a machine that does not have a sound card, such as eh-2 (check with Eric first)
|
||||
- update config to turn off sounds
|
||||
|
||||
|
||||
@@ -86,6 +86,16 @@ NOTES:
|
||||
|
||||
2005) Open a new ladder for any symbol and verify you can cross your own orders without being pestered by a confirmation dialog
|
||||
|
||||
-------------------------------------------------------------------------------
|
||||
Left Panel Scroll test
|
||||
-------------------------------------------------------------------------------
|
||||
The scroll position of the left-panel area is now remembered as part of the ladder settings
|
||||
|
||||
Resize your ladder window to make it vertically small so that the left panel displays a scrollviewer.
|
||||
Scroll the left panel down so that the qty box is the top-most visible control in the panel
|
||||
Exit and relaunch Spark, loading your last saved workspace
|
||||
Verify that the restored ladder window's left panel has been vertically scrolled so the the qty box is one again the top-most visble control
|
||||
|
||||
DONE!!!
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
This test makes sure the basic spreader works as child orders of different algos
|
||||
|
||||
Iceberg
|
||||
-------
|
||||
|
||||
0. Follow the instructions from docs/spreader_common_setup_2.txt
|
||||
|
||||
1. In the spreader ladder enter a 5 lot sell IB with a show qty of 2 at -0.275.
|
||||
|
||||
2. Verify that it completely fills.
|
||||
|
||||
3. Verify that the Filled Qty and Avg Filled in the OB is correct
|
||||
@@ -26,21 +26,22 @@ or more orders to get the unhedged delta < 1.
|
||||
|
||||
Force quoting fills based on the left most column (by lifting the quoting offer)
|
||||
and verify that hedge orders of size "hedge_order_sent" (3rd column) are sent as a result.
|
||||
The "unhedged_delta" column shows the unhedged delta immediately after the quoting fill
|
||||
Compare the cumulative qty (cum) to the quote and hedge badges for easy visual inspection.
|
||||
|
||||
(The "unhedged_delta" column shows the unhedged delta immediately after the quoting fill
|
||||
and the "remaining_unhedged_delta" column is the unhedged delta after the hedge order
|
||||
has been sent.
|
||||
You can check the hedge orders being sent easily by looking at the order reduction
|
||||
happening on the order sent in step 2 (or look at the hedges in the orderbook).
|
||||
has been sent)
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
quoting fill unhedged_delta hedge_order_sent remaining_unhedged_delta
|
||||
quoting fill(cum) unhedged_delta hedge_order_sent (cum) remaining_unhedged_delta
|
||||
------------------------------------------------------------------------------
|
||||
1 2/3 0 2/3
|
||||
1 4/3 1 1/3
|
||||
1 1 1 0
|
||||
4 8/3 2 2/3
|
||||
5 4 4 0
|
||||
2 4/3 1 1/3
|
||||
1 1 1 0
|
||||
1(1/15) 2/3 0(0/10) 2/3
|
||||
1(2/15) 4/3 1(1/10) 1/3
|
||||
1(3/15) 1 1(2/10) 0
|
||||
4(7/17) 8/3 2(4/10) 2/3
|
||||
5(12/15) 4 4(8/10) 0
|
||||
2(14/15) 4/3 1(9/10) 1/3
|
||||
1(15/15) 1 1(10/10) 0
|
||||
|
||||
6. At the end of this, the whole order should be filled. Check the order book
|
||||
to make sure the fill price is -9.900, there are 10 total fills in SIF1 and
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
This test makes sure the basic spreader functionality works for 2 legged synthetic spreads with one quoting leg
|
||||
|
||||
Iceberg
|
||||
-------
|
||||
|
||||
0. Follow the instructions from docs/spreader_common_setup_2.txt
|
||||
|
||||
1. In the spreader ladder enter a 5 lot sell IB with a show qty of 2 at -0.275.
|
||||
|
||||
2. Verify that it completely fills.
|
||||
|
||||
3. Verify that the Filled Qty and Avg Filled in the OB is correct
|
||||
@@ -29,5 +29,3 @@ Sym,Mult,Exch
|
||||
"BUB",1000,CBT
|
||||
"FNU",1000,CBT
|
||||
"SOM",500,CBT
|
||||
"OJ",150,IFU
|
||||
"KC",375,IFU
|
||||
|
Reference in new issue
Block a user