Compare commits

..
92 Commits
Author SHA1 Message Date
Taha Afzal b3369db7cc Release notes 2019-11-04 13:07:02 -05:00
Taha Afzal 279abcfefd Made the fractional_hedging test a bit easier to do 2019-11-04 10:41:27 -05:00
Taha Afzal 872e2465a6 Fixing the test 2019-11-01 14:15:11 -04:00
Taha Afzal 89fb79c556 Manual tests added 2019-10-29 18:53:17 -04:00
Taha Afzal c92e9df77b Merge branch 'hide_deleted_accounts' 2019-10-29 14:58:11 -04:00
Taha Afzal 81aaa853d5 Merging SP-248 2019-10-29 14:40:00 -04:00
Taha Afzal 4f308faba2 Merge branch 'SP-247-order-entry-sounds' 2019-10-29 14:09:30 -04:00
Taha Afzal 0b924033c2 Merge branch 'SP-252-root-spreader-security' 2019-10-29 14:08:29 -04:00
Taha Afzal 3606c707e4 Merge branch 'SP-204-local-fills' 2019-10-29 14:07:42 -04:00
Taha Afzal 5998107e6a Merge branch 'SP-225-investigate-the-lack-of-price-rep' 2019-10-29 14:03:12 -04:00
Taha Afzal 297982433b Merge branch 'SP-249' 2019-10-29 13:58:28 -04:00
Taha Afzal 15ac9d945b Modified tests 2019-10-29 13:53:20 -04:00
Jake Norbut a0f1d11692 adding manual fills don't show admin fills test 2019-10-28 16:15:01 -05:00
Jake Norbut e7890a459c test for order sent sound 2019-10-28 15:57:27 -05:00
Jake Norbut f7fb5ed400 adding test for deleted accounts 2019-10-28 15:48:47 -05:00
Jake Norbut 75e98d353f adding left panel test steps 2019-10-28 15:41:39 -05:00
John Colvin 5fc6ff4c00 SP-204 Local fills UI erroneously shows admin fills too 2019-10-26 16:40:32 -05:00
Taha Afzal 6951272633 Just has to update the last_price 2019-10-25 13:05:44 -04:00
Taha Afzal 548611b44f Adding tooltips, and hooking to the right proto 2019-10-24 18:04:54 -04:00
Taha Afzal 322da04723 Merge branch 'SP-249' of bitbucket.org:bluetradesys/spark into SP-249 2019-10-24 17:09:30 -04:00
Taha Afzal 3544eac815 Cleaned up the quote throttling code 2019-10-24 17:09:24 -04:00
John Colvin 7fef7d88ce refactor QuoteInsideMarketDistance to new proto type (cannot recycle old type bc it must be null in existing synths) 2019-10-24 15:41:30 -05:00
John Colvin 7531cf5eb4 remove unused synth param 'QuoteInsideMarketDistanceAwayModel' 2019-10-24 15:38:15 -05:00
John Colvin 5826a083c0 SyntheticEditor - remove Quote Inside Market Away Distance param 2019-10-24 12:44:47 -05:00
John Colvin ec0d5d22f1 SyntheticEditor - disable Quote Throttle/Inside Market Distance by default 2019-10-24 12:37:13 -05:00
John Colvin d433368795 fix typo in 'order sent alert' predicate (creation time comparison was backwards) 2019-10-24 11:51:39 -05:00
Jake Norbut dc4453e510 SP-256 - hide deleted accounts 2019-10-24 08:32:38 -05:00
Jake Norbut 1b1583abda removing OJ and KC IFU entries; regen cme ref data file 2019-10-23 09:24:28 -05:00
Jake Norbut 9d2f2888bb updating reference data with latest for RodrigoSGU and CNH 2019-10-23 09:08:20 -05:00
John Colvin 26816f0048 SP-249 New spreader quoting throttle parameter 2019-10-22 12:12:17 -05:00
Taha Afzal e97e6b93c7 Adding proto 2019-10-21 16:44:00 -04:00
John Colvin 0d8252e131 SP-252 Parent spreader badge should show the spreader name as well 2019-10-21 12:58:21 -05:00
Taha Afzal 801b9622f8 Merge branch 'master' of bitbucket.org:bluetradesys/spark 2019-10-17 15:09:19 -04:00
Taha Afzal a5df3f2da2 Dynamic Algos are now loaded only when explicitely enabled by name in Toggles config 2019-10-17 15:09:10 -04:00
John Colvin f03a413855 SP-247 Order Entry Sounds 2019-10-17 13:55:52 -05:00
Michael Harding f3a692bda2 Delete ex_sim_orders_venue_sim in system tests 2019-10-17 13:30:11 -04:00
Kevin Darby 6c3bdbdc34 make sim datapaths pretty again 2019-10-17 12:09:05 -04:00
Eric Harding d4749dd381 Tool to test c# api latency 2019-10-17 11:57:20 -04:00
Taha Afzal a008f4d0fd Hiding pyramid, ignoring timer (which was faking market status) 2019-10-17 11:38:35 -04:00
Taha Afzal 0ee9521454 Merge branch 'TAS' 2019-10-17 11:16:03 -04:00
Kevin Darby 62cef7c2f7 release notes 2019-10-17 09:44:53 -04:00
Taha Afzal 1b39f4e47c Merge branch 'master' into TAS 2019-10-16 21:54:24 -04:00
Taha Afzal 3212b2fcee Added logs for TAS Algo 2019-10-16 16:38:12 -04:00
Taha Afzal 1576541bd1 Clean up 2019-10-16 14:59:34 -04:00
Kevin Darby 62e97364fb Merged in ice_exec3 (pull request #230)
Ice execution

Approved-by: Kevin Darby <kevin.darby@bluetradesys.com>
2019-10-16 18:47:10 +00:00
Taha Afzal 93b954ce46 Some cleanup remains, mostlty done 2019-10-16 11:37:32 -04:00
John Colvin c9da022c58 SP-248 Ladder left panel scroll 2019-10-15 12:12:20 -05:00
John Colvin 0772a13b09 introduce ladder settings 'profile-equivalent' logic to avoid disconnecting ladders from profiles due to trivial settings changes 2019-10-14 20:55:00 -05:00
John Colvin c53f6ad906 finally remove mutable members from LadderSettings GetHashCode impl 2019-10-14 16:01:24 -05:00
Taha Afzal 1058c8a80e Merge branch 'pyramid' into TAS 2019-10-14 11:45:56 -04:00
Taha Afzal 7ecb106880 Done 2019-10-14 11:39:27 -04:00
Taha Afzal b0c1d93c50 Corner cases fixed 2019-10-14 11:08:29 -04:00
Kevin Darby 0c5a039226 tested buffered cancel and replace with lean_controlled.txt, verified in fix 2019-10-11 14:04:32 -04:00
Kevin Darby 2677144bfa buffer cancel and updates because ice sucks 2019-10-10 17:43:28 -04:00
Kevin Darby 6b30c42c0d added gt_cert jetfix rule to conf per Pete 2019-10-10 14:56:32 -04:00
Taha Afzal 828f867a80 Merge branch 'pyramid' of bitbucket.org:bluetradesys/spark into pyramid 2019-10-09 13:46:19 -04:00
Taha Afzal c9663cf7ab made the child orders configurable 2019-10-09 13:46:08 -04:00
John Colvin b85de51d0c bring back DefaultAlgoVM for 'None' case (dynamic children placeholders do not have specific impl) 2019-10-09 12:33:40 -05:00
Taha Afzal 562b9419bd hedge is a lean now 2019-10-04 17:32:03 -04:00
Taha Afzal 3e035197d0 Top Spread is also a security (forgot to mark it as a security, so it was showing as a uint in the ui) 2019-10-01 20:42:06 -04:00
Taha Afzal c9d8a640b6 Merge branch 'pyramid' of bitbucket.org:bluetradesys/spark into pyramid 2019-10-01 19:44:49 -04:00
John Colvin 1d97e620fd add security map to view's resources for dynamic security key param's ContractPickerPopup 2019-10-01 17:17:31 -05:00
John Colvin f6299ab5c3 clean up DynamicOrderParamVM - don't store value with the descriptor, make 'param' prop readonly 2019-10-01 17:08:32 -05:00
John Colvin eaff4774e0 refactor some Disposable.replace calls so that the prev hook is disposed before the new one is created 2019-10-01 17:08:32 -05:00
John Colvin bed52db98a dynamic order SecurityKey DataTemplate 2019-10-01 17:08:32 -05:00
John Colvin bd3ef33607 AlgoStudio dynamic order 'SecurityKey' template 2019-10-01 17:08:32 -05:00
John Colvin 25a594895e ladder/ticket/algo studio views store SecurityMap in resource dictionary 2019-10-01 17:08:31 -05:00
John Colvin 4d468c8026 DynamicOrderVM 'SecurityKey' descriptor impl 2019-10-01 17:08:31 -05:00
John Colvin 68ed90487b AlgoParamVM template key impl 2019-10-01 17:08:31 -05:00
John Colvin ddc2c67fea add SelectedSecurityKey dp to ContractPickerPopup 2019-10-01 17:08:31 -05:00
Taha Afzal 77459e9c64 price changes dont have the algo params filled in 2019-10-01 16:15:36 -04:00
John Colvin db64f2f45b migrate common ManualFills resource keys into central location 2019-09-30 18:21:06 -05:00
John Colvin 3f7ea71de7 [non-functional] normalize whitespace in AlgoContentTemplateSelector.xaml 2019-09-30 18:11:18 -05:00
John Colvin d334418655 PyramidAlgo uses 'SecurityKey' dynamic param descriptor 2019-09-30 18:09:11 -05:00
John Colvin d1844afeae server side impl for 'SecurityKey' dynamic param descriptor 2019-09-30 18:08:41 -05:00
John Colvin b6dc2bd4d4 add 'SecurityKey' dynamic param descriptor 2019-09-30 18:03:52 -05:00
Taha Afzal 58906eb3b6 Code complete 2019-09-30 16:55:35 -04:00
Taha Afzal 963a4207e2 Code complete, need cleanup and testing 2019-09-26 19:00:15 -04:00
Taha Afzal 4e58342677 Merge branch 'master' into Pyramid 2019-09-24 13:12:56 -04:00
Taha Afzal 5eeb5a534c Adding logs 2019-09-24 13:12:16 -04:00
Taha Afzal f20c53bced Organized the code better 2019-09-23 14:08:21 -04:00
Taha Afzal cf977befd1 odds and ends 2019-09-18 17:22:09 -04:00
Taha Afzal c9b2ac290c payup logic implemented 2019-09-17 22:10:53 -04:00
Taha Afzal 340d65a47c Merge branch 'master' into pyramid 2019-09-17 16:32:53 -04:00
Taha Afzal 1b3f5bc8a6 Fixed a bug with reporting 2019-09-11 13:44:37 -04:00
Taha Afzal 1b767f6e5e Cleaning up 2019-09-09 18:03:07 -04:00
Taha Afzal fe647044f8 Adding comments to SpreaderUtils 2019-09-09 15:03:52 -04:00
Taha Afzal 69dcfdb2bd Delete parameter added 2019-09-03 16:57:41 -04:00
Taha Afzal e179f7e5a4 Some error handling (originating the algo from the wrong top spread) 2019-08-28 12:27:36 -04:00
Taha Afzal dba3747d98 Need more robust error checking, functionality works 2019-08-28 11:17:33 -04:00
Taha Afzal 3e7627ac24 Almost done, need to hook up hedging (cover) and parameter change 2019-08-24 13:08:26 -04:00
Taha Afzal 1b043f77cb Basic scaffolding in place for pyramid algo, shows up in the UI 2019-08-22 11:01:42 -04:00
87 changed files with 2633 additions and 416 deletions

No files matched your search

+17
View File
@@ -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
+1 -1
View File
@@ -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
@@ -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);
}
}
}
}
@@ -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;
@@ -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()));
+11 -6
View File
@@ -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) {
+3 -1
View File
@@ -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;
});
}
}
+4 -2
View File
@@ -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();
+1
View File
@@ -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
+13 -2
View File
@@ -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);
}
}
+22 -2
View File
@@ -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);
}
}
}
}
+1
View File
@@ -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);
}
+39 -37
View File
@@ -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 }
}
}
}
+333
View File
@@ -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 {
+7
View File
@@ -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];
+6
View File
@@ -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];
}
+23 -3
View File
@@ -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);
+52 -36
View File
@@ -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;
}
+1
View File
@@ -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:
+1 -1
View File
@@ -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,
@@ -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);
}
}
}
}
+8 -1
View File
@@ -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
+4
View File
@@ -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.
+16
View File
@@ -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
+6
View File
@@ -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
-2
View File
@@ -29,5 +29,3 @@ Sym,Mult,Exch
"BUB",1000,CBT
"FNU",1000,CBT
"SOM",500,CBT
"OJ",150,IFU
"KC",375,IFU
1 Sym Mult Exch
29 BUB 1000 CBT
30 FNU 1000 CBT
31 SOM 500 CBT
OJ 150 IFU
KC 375 IFU