Compare commits

...
65 Commits
Author SHA1 Message Date
Mohammad Arham 914ac597cd Vesrsion: Update to v2.2.5 deploying to jame prod 2025-02-13 14:40:27 +00:00
Mohammad Arham 4aa6a77680 Fix: OKX execution works as long as spreader not to big 2025-02-13 06:09:43 +00:00
Mohammad Arham 6b59dd84ee Fix: Ordermanager will not try to remake orders from er after restart 2025-02-13 06:09:01 +00:00
Mohammad Arham 4e2c5f2d17 Fix: Okx USDT perps fix re added 2025-02-13 06:08:27 +00:00
Mohammad Arham 9a1b94a1b0 Add: fill id to okx execution report 2025-02-13 01:13:50 +00:00
Mohammad Arham 13b3c55771 Change: lst msg only entered if error 2025-02-12 23:13:59 +00:00
Mohammad Arham d0a6fcdb9c Add: Stricter Assert if websocket goes down okx execution 2025-02-12 22:23:53 +00:00
John Colvin 1eb13028c1 ui: Get qty display settings from md instead of config 2025-02-12 15:59:51 -05:00
John Colvin 424e5ff16b Get Spark version at runtime via reflection
So it doesn't need to be hardcoded per release
2025-02-12 15:59:51 -05:00
John Colvin bed389885b Support qty tick size 10e-9
Some spots at OKX need this (e.g., BTC-AED)
2025-02-12 20:42:28 +00:00
John Colvin 85cf25a57a fix: Get min qty and min increment from correct fields in okx json
"lotSz" represents min qty, and "minSz" represents min qty increment. We
had them backwards before.
2025-02-12 20:36:40 +00:00
John Colvin 61471d4675 diag: Show qty info fields in FixedSecurity json 2025-02-12 20:34:48 +00:00
John Colvin d83435d49d Move FixedSecurity to_json fn to cpp
Doesn't make sense to inline; it's too big.
2025-02-12 20:34:47 +00:00
John Colvin 582fc10dce Add get_binance_md.sh for quick/easy troubleshooting 2025-02-12 20:34:47 +00:00
John Colvin 5969985d24 cleanup: Remove unnecessary includes 2025-02-12 20:34:47 +00:00
John Colvin fabe7d665c devcontainer: Install clang-format extension 2025-02-12 20:34:47 +00:00
John Colvin 7ce75bb8e7 devcontainer: Increase ram and shared mem
In order to support large symbol sets for Alphaworx
2025-02-12 20:34:47 +00:00
Mohammad Arham b8e1c3cd60 Add: Fix build Issue 2025-02-12 19:48:52 +00:00
Taha 4909944694 Fixing PNL for perps (its base don mid market now) 2025-02-12 11:20:55 -05:00
Mohammad Arham 7258af09b4 Add: Basic Okx execution working 2025-02-12 11:55:50 +00:00
Mohammad Arham 7f2ce6c1a9 Add: Okx execution cfg 2025-02-12 11:06:45 +00:00
Mohammad Arham 4835725e1c Add: diag logging to gttp client 2025-02-12 11:06:09 +00:00
Mohammad Arham 6a9e8de475 Deployment: Updating version numbers 2025-02-11 05:06:21 +00:00
Taha Afzal 4639951acc Fixing Sprader's reportin of its filled qty 2025-02-11 03:04:30 +00:00
Taha d8de211aac Front end does NOT filter nay CBOE or CME expirations 2025-02-10 17:49:14 -05:00
John Colvin 87394bc60b ui: Increase Currency col width so filter icon isn't clipped 2025-02-10 15:31:31 -05:00
John Colvin beba30f65c fix (ui): Remove temp hack that assumed synth qty always notional 2025-02-10 15:31:31 -05:00
John Colvin 2d58424d8f ui (mview): Hook up currency filter in vm
Make it a discrete values filter (listbox) while we're at it. This seems
more practical than regex.
2025-02-10 15:31:31 -05:00
John Colvin d1bbfa3791 Revert batch subscribe change in Binance 2025-02-10 19:59:42 +00:00
John Colvin aab17f460f cleanup: Remove docker submodules again
They just keep coming back.
2025-02-10 19:59:32 +00:00
John Colvin 461ef53322 devcontainer: Clean up and simplify
* Remove mounted dirs (the repo is automatically persisted in a volume)
* Remove user specific shell env initialization scripts (they're more
  trouble than they're worth, often break the devcontainer build)
* Remove redundant settings already defined in settings.json
2025-02-10 16:55:53 +00:00
Taha Afzal 2486bc90b2 Spreader is hooked to the priceCal dropdown while creating the spreader 2025-02-10 03:10:11 +00:00
John Colvin 1c5cbaf60b Change: Update version to 2.2.4-1 2025-02-08 13:19:41 -05:00
John Colvin b8de447cfb fix (pnl ui): Assume rows w/ exp DateTime.MinValue are Spots
Not the most elegant fix, but it'll do for now.

Bugs (fixed):
  * The Pnl window groups perpetual futures with spots
2025-02-08 13:18:27 -05:00
Mohammad Arham 8bfc2d40f9 Update: V2.2.4 pnl working now 2025-02-08 12:43:13 -05:00
Taha c09fd71c94 Fixing PNL for spots 2025-02-08 06:55:07 -05:00
Mohammad Arham f3b037553e Add: Batch subs in binance sub 2025-02-08 06:44:10 +00:00
Taha Afzal d386d93166 SpreaderChildOrder::on_order_udpate, changing an asserrt to an error lgo to monitor condition 2025-02-07 15:06:16 +00:00
Mohammad Arham 5217742c71 Add: SendingTime tag to cboe execution 2025-02-07 12:26:11 +00:00
Taha c59ec95460 Sheets and spreadbox theos can now optionally be set to market mid 2025-02-06 17:42:12 -05:00
Mohammad Arham 70f01583ec ADD: CBOE TIME and SALES working 2025-02-06 20:42:58 +00:00
Mohammad Arham 66d2f9eb63 Add: Quote algo updates, theos removed 2025-02-06 03:08:46 +00:00
John Colvin 4625bed8b8 Change: Update version to v2.2.3-1 2025-02-05 17:01:11 -05:00
John Colvin f18fb332b5 fix (orderbook): Make ColumnKeys enum 64bit
We rely on bit flags to indicate "dirty" columns in the vm. Need to do
this now that we have > 31 columns.
2025-02-05 17:01:11 -05:00
John Colvin 8746a6f5ca fix (orderbook): Refresh Notional Value view when it changes in vm 2025-02-05 16:30:13 -05:00
John Colvin 702d47abb5 fix (orderbook): Fix typos in default settings for Notional Value 2025-02-05 16:29:29 -05:00
John Colvin bf2a54717c ui (orderbook): Add Filled Notional Value column 2025-02-05 16:13:34 -05:00
John Colvin 9db779fb25 fix (hack): Assume synth quantities are always notional values 2025-02-05 16:07:07 -05:00
Mohammad Arham f55ddec708 Add: Vesrion logs to bothserver and client 2025-02-05 09:50:33 -05:00
Mohammad Arham 6e942dd2d4 Change: Updated version to v2.2.3 2025-02-05 08:38:31 -05:00
Mohammad Arham 142310a17a Add: SPXW to default for cboe 2025-02-05 12:16:47 +00:00
John Colvin 7f99a89a95 ui (fills): Add notional value column 2025-02-05 00:00:41 -05:00
John Colvin bfce663b4b fix (orderbook): Hook up notional val filter in vm, persist settings 2025-02-04 23:42:58 -05:00
John Colvin 33c743643d fix (orderbook): Include fractional qty in notional val 2025-02-04 23:34:47 -05:00
Taha Afzal 858e0a389b Fixed the issue where a notioanal quoter would send a 0 qty order 2025-02-05 04:16:53 +00:00
Taha Afzal 9c3bb85674 Notional qty handling in spreader 2025-02-05 04:16:53 +00:00
Taha Afzal f4930e8cd4 Added notional to PriceCalc type for the sprader, added code to compute notional books in SpreaderUtils 2025-02-05 04:16:53 +00:00
John Colvin f21e7f3602 ui (orderbook): Show synth notional value in terms of first leg 2025-02-04 16:54:37 -05:00
John Colvin b56c35aca4 ui (ladder): Show synth notional value in terms of first leg 2025-02-04 16:54:37 -05:00
John Colvin e6ac83196e fix (benign): Actually dispose _vm_sub in LadderGridHost 2025-02-04 16:54:37 -05:00
John Colvin 7b6f74fc4a cleanup: Get rid of 'mimic tt' ladder config
This config was leftover from custom DJ work. We no longer need it.
2025-02-04 16:54:37 -05:00
John Colvin f754953fac cleanup: Remove duplicate equality check 2025-02-04 16:54:37 -05:00
Taha 2dd1143d9f Fixed spx spreadbox theos 2025-02-04 16:31:17 -05:00
Mohammad Arham e1ee3275d8 Add: Filter secs upto n months databento 2025-01-31 21:46:11 +00:00
John Colvin 2549e406f3 fix: Compute notional value correctly when ticks per row > 1
It's the sum of price * qty for each level in the row. Previously, we
were multiplying the total qty by the same price (smallest in the row).
2025-01-29 16:28:28 -05:00
82 changed files with 3826 additions and 2139 deletions

No files matched your search

+6 -18
View File
@@ -1,15 +1,10 @@
{
"dockerFile": "Dockerfile",
// Creates local dir to mount to HOME in docker
"initializeCommand": "mkdir -p ${localEnv:HOME}/.bts/devcontainer-mounts/${devcontainerId}",
"hostRequirements": {
"memory": "4gb",
"storage": "6gb"
"memory": "8gb",
"storage": "10gb"
},
"postCreateCommand": "/home/bts/spark/docker/sparkd/dev/devcontainer_post_create.sh",
"containerUser": "bts",
"workspaceMount": "source=${localWorkspaceFolder},target=/home/bts/spark,type=bind",
"workspaceFolder": "/home/bts/spark",
"updateRemoteUserUID": true,
"shutdownAction": "none",
// Note: If running multiple containers on the same host, each must use a
@@ -17,7 +12,7 @@
// example, if 8000-8003 is taken, go with 9000-9003, then 10000-10003, etc.
"runArgs": [
"--shm-size",
"2gb",
"8gb",
"-p",
"8000:8000",
"-p",
@@ -29,22 +24,15 @@
"-p",
"12345:12345"
],
"mounts": [
"source=${localEnv:HOME}/.bts/devcontainer-mounts/${devcontainerId},target=/home/bts,type=bind"
],
"customizations": {
"vscode": {
"settings": {
"clangd.arguments": [
"--compile-commands-dir=${workspaceFolder}/build/clang/debug"
]
},
"extensions": [
"eamodio.gitlens",
"llvm-vs-code-extensions.vscode-clangd",
"ms-vscode.cmake-tools",
"ms-vscode.cpptools"
"ms-vscode.cpptools",
"xaver.clang-format"
]
}
}
}
}
+43
View File
@@ -8,6 +8,7 @@ using MathNet.Numerics.Providers.FourierTransform;
using BCG;
using System.Windows.Automation;
using bts.collections;
using mbts.co2;
namespace mbts.spark {
// Publishes theos for all options (on the side, also publishes the theos for TC2).
@@ -114,11 +115,53 @@ namespace mbts.spark {
}
sym_theos.set_data(k, tp);
}
_override_theos(ref rv);
foreach (var kv in rv) {
_bb.post(SparkTags.TC2Tags.LiveTheosTag(kv.Key), kv.Value);
}
}
private void _override_theos(ref Dictionary<(string, Venue), ExpStrikeKeyedData<TheoPoint>> theos) {
if (_compute_midmarket) {
SystemContractData sc_data = _bb.read_state(SparkTags.SystemContractsTag);
foreach (var kv in theos) {
string sym = kv.Key.Item1;
Venue exchange = kv.Key.Item2;
foreach (var t in kv.Value.get_data_linearly()) {
DateTime exp = t.first;
double strike = t.second;
TheoPoint tp = t.third;
var call_sec_md = sc_data.get_option(exchange, sym, exp, strike, PutOrCall.Call);
{
NativePriceSize call_bid, call_ask;
ulong time = 0;
call_sec_md.Value.md.get_bbo(ref time, out call_bid, out call_ask);
double call_mid = (call_bid.price + call_ask.price) / 2;
tp.call = call_mid;
}
{
var put_sec_md = sc_data.get_option(exchange, sym, exp, strike, PutOrCall.Put);
NativePriceSize put_bid, put_ask;
ulong time = 0;
put_sec_md.Value.md.get_bbo(ref time, out put_bid, out put_ask);
double put_mid = (put_bid.price + put_ask.price) / 2;
tp.put = put_mid;
}
}
}
//replace theos with market bid
// get it from the bus and override
} else {
// do nothing, passes unchanged
}
}
private bool _compute_midmarket {
get {
return true;
}
}
private void _interp_and_publish() {
var timer = Stopwatch.StartNew();
+11
View File
@@ -281,5 +281,16 @@ namespace mbts.spark {
else
return tp.put + (tp.pdelta * driver_diff) + (0.5 * tp.pgamma * driver_diff * driver_diff);
}
public static TheoPoint extrapolate(TheoPoint tp, double driver, bool is_call) {
TheoPoint rv = tp.Clone() as TheoPoint;
double h = driver - tp.driver;
if (is_call) {
rv.call = tp.call + h * (tp.cdelta);
} else {
rv.put = tp.put + h * (tp.cdelta);
}
return rv;
}
}
}
@@ -13,7 +13,7 @@ namespace mbts.analytics {
// prices (specified in the vs settings).
// It has public setters that get
// 1. contracts
// 2. vs inout
// 2. vs input
// 3. exp params(drivers, interst rate etc)
// 4. driver prices
// 5. vol time
+22 -8
View File
@@ -133,17 +133,31 @@ namespace bts {
// set 'unit quantity' for each sec
foreach (var smd in exch_secs.Values) {
var meta = _security_metadata.get(smd.security);
if (meta.HasValue) {
exch_secs[smd.id] = new(
smd.security,
smd.md,
meta.Value.HasPriceDisplay ? meta.Value.PriceDisplay : null,
meta.Value.HasQuantityDisplay ? meta.Value.QuantityDisplay : null);
}
var (priceDisplay, qtyDisplay) = GetDisplaySettings(smd.security);
exch_secs[smd.id] = new(smd.security, smd.md, priceDisplay, qtyDisplay);
}
_bus.post(SparkTags.ServerSecuritiesTag, exch_secs);
}
(TickDisplaySettingsImmutable? Price, TickDisplaySettingsImmutable? Quantity) GetDisplaySettings(SecurityImmutable sec) {
var meta = _security_metadata.get(sec);
var priceDisplay = meta?.PriceDisplay ?? Security.Defaults.price_display_settingsi;
var qtyDisplay = meta?.QuantityDisplay ?? Security.Defaults.quantity_display_settingsi;
if (sec.HasContractSpec) {
var pd = priceDisplay.CloneToMutable();
pd.TickFactors = TickUtils.get_tick(sec, 0).to_api_tick_factors();
priceDisplay = new(pd);
if (sec.ContractSpec.QuantityInfos.TryGetFirst(out var qtyInfo)) {
var qd = qtyDisplay.CloneToMutable();
qd.ShowDecimalPart = qtyInfo.TickBase.is_real_non_zero() && !(qtyInfo.TickUnit / qtyInfo.TickBase).is_int();
qd.TickFactors ??= new();
qd.TickFactors.TickBase = qtyInfo.TickBase;
qd.TickFactors.TickUnit = qtyInfo.TickUnit;
qtyDisplay = new(qd);
}
}
return (priceDisplay, qtyDisplay);
}
}
public void Dispose() =>
@@ -12,8 +12,13 @@ namespace bts.shell.market_view {
class MarketViewFilterStates {
[ProtoMember(37)]
public Venue[] venue { get; set; }
[ProtoMember(38)]
public string[] currency { get; set; }
[ProtoMember(1)]
public string security { get; set; }
[ProtoMember(2)]
public DoubleMinMaxState bid_price { get; set; }
@@ -128,6 +133,7 @@ namespace bts.shell.market_view {
var pb = new ColumnPredicateBuilder<MarketViewVM.Row>();
// security
pb.add_discrete_values(venue, r => r.venue);
pb.add_discrete_values(currency, r => r.currency);
pb.add_regex(security, r => r.security.ToString());
pb.add_regex(security_key, r => r.security_key.ToString());
@@ -98,7 +98,14 @@ namespace mbts.spark {
_sec_meta_store = sec_meta_store;
_settings_hub = settings_hub;
security_filter = new RegexFilterVM<Row>(r => r.security.ToString(), nameof(Row.security));
currency_filter =new RegexFilterVM<Row>(r => r.currency, nameof(Row.currency));
currency_filter = DiscreteValuesFilterVM.Create<Row, string>(
bus.subscribe(SparkTags.SecuritiesTag)
.Sample(TimeSpan.FromSeconds(1))
.Select(sec_mds => sec_mds.Values.Select(smd => smd.security.Currency).Distinct().ToList()),
r => r.currency,
nameof(Row.currency),
EqualityComparer<string>.Default,
Comparer<string>.Default);
// rows
_wrapped_rows = _rows.WrapSelector(r => r.security_md, s => {
var r = new Row(_bus, s, _orders_model, _position_model);
@@ -124,7 +131,7 @@ namespace mbts.spark {
add_col(ColKey.Venue, r => r.venue.ToString(), nameof(Row.venue), venue_filter, FilterType.DiscreteValues, ColWidth.Exchange);
add_col(ColKey.Security, r => r.security.ToString(), nameof(Row.security), security_filter, FilterType.Regex, ColWidth.Security);
add_col(ColKey.Currency, r => r.currency, nameof(Row.currency), currency_filter, FilterType.Regex, ColWidth.Currency);
add_col(ColKey.Currency, r => r.currency, nameof(Row.currency), currency_filter, FilterType.DiscreteValues, ColWidth.Currency);
add_col(ColKey.Volume, r => r.volume_text, nameof(Row.volume), volume_filter, FilterType.ULongMinMax, ColWidth.Volume);
// last
@@ -391,7 +398,7 @@ namespace mbts.spark {
public IDataGridColumnPersistentFilter<Row, Venue[]> venue_filter { get; } = DiscreteValuesFilterVM.Common.exchange<Row>(r => r.venue, nameof(Row.venue));
public IDataGridColumnPersistentFilter<Row, string> security_filter { get; }
public IDataGridColumnPersistentFilter<Row, string> currency_filter { get; }
public IDataGridColumnPersistentFilter<Row, string[]> currency_filter { get; }
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> bid_price_filter { get; } = new DoubleMinMax<Row>(r => r.bid_price, nameof(Row.bid_price));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> bid_qty_filter { get; } = new DoubleMinMax<Row>(r => r.bid_qty_display, nameof(Row.bid_qty_display));
public IDataGridColumnPersistentFilter<Row, DoubleMinMaxState> dbid_qty_filter { get; } = new DoubleMinMax<Row>(r => r.dbid_qty_display, nameof(Row.dbid_qty_display));
@@ -538,6 +545,7 @@ namespace mbts.spark {
public MarketViewFilterStates get_filter_states() =>
new() {
venue = venue_filter.get_state(),
currency = currency_filter.get_state(),
security = security_filter.get_state(),
bid_price = bid_price_filter.get_state(),
bid_merged_size = bid_qty_filter.get_state(),
@@ -584,6 +592,7 @@ namespace mbts.spark {
using var _ = rows_view.DeferRefresh();
venue_filter.load_state(filters.venue);
currency_filter.load_state(filters.currency);
security_filter.load_state(filters.security);
bid_price_filter.load_state(filters.bid_price);
bid_qty_filter.load_state(filters.bid_merged_size);
@@ -237,6 +237,17 @@ namespace mbts.spark {
DefaultColumnWidths.Qty,
get_fg: r => r.side == Side.Buy ? BTSColors.MarketData.bid : BTSColors.MarketData.ask);
add_col(
Columns.NotionalValue,
DefaultColumnHeaders.NotionalValue,
r => _get_row(r).security.spec_or_default().format(r.fill.notional_value()),
new DoubleMinMax<FillTuple>(r => r.fill.notional_value()),
DataGridFilterKnownTemplates.DoubleMinMax,
fs => fs.notional_value,
(fs, cf) => fs.notional_value = cf,
DefaultColumnWidths.NotionalValue,
get_fg: r => r.side == Side.Buy ? BTSColors.MarketData.bid : BTSColors.MarketData.ask);
add_col(
Columns.Price,
DefaultColumnHeaders.Price,
@@ -21,6 +21,7 @@ namespace mbts.ui.spark {
Side,
Price,
Size,
NotionalValue,
FillKind,
FillStatus,
FillKey,
@@ -61,6 +61,10 @@ namespace bts.shell.fills {
[DIYGridColumnID(nameof(Columns.Size))]
public DoubleMinMaxState size { get; set; }
[ProtoMember(18)]
[DIYGridColumnID(nameof(Columns.NotionalValue))]
public DoubleMinMaxState notional_value { get; set; }
[ProtoMember(11)]
[DIYGridColumnID(nameof(Columns.FillKey))]
public string fill_key { get; set; }
@@ -113,6 +117,7 @@ namespace bts.shell.fills {
pb.add_discrete_values(fs.fill_status, ft => ft.fill.FillStatus);
// numeric
pb.add_numeric(fs.size, ft => ft.fill.LastShares);
pb.add_numeric(fs.notional_value, ft => ft.fill.notional_value());
pb.add_numeric(fs.price, ft => ft.fill.LastPrice);
if (fs.exp_date.HasValue) {
@@ -408,18 +408,18 @@ namespace mbts.spark {
[ProtoContract]
class LadderPanelPricePointSettings : IEquatable<LadderPanelPricePointSettings> {
public static LadderPanelPricePointSettings historic_default =>
public static LadderPanelPricePointSettings Default =>
new LadderPanelPricePointSettings {
day_change = new DaySettings {
// (neutral)
font = _default_font,
background_color = Colors.Transparent.to_uint(),
font = _default_font.With(font_color: Colors.Black.to_uint()),
background_color = Color.FromRgb(113, 201, 69).to_uint(),
// negative
negative_font_color = Colors.White.to_uint(),
negative_background_color = Colors.Red.to_uint(),
// positive
positive_font_color = Colors.White.to_uint(),
positive_background_color = Colors.Green.to_uint()
positive_background_color = Colors.Green.to_uint(),
},
session_low = _default_item.With(_default_item.font.With(font_color: BTSColors.MarketData.session_low)),
session_high = _default_item.With(_default_item.font.With(font_color: BTSColors.MarketData.session_high)),
@@ -431,21 +431,9 @@ namespace mbts.spark {
theo = _default_item,
delta = _default_item,
vega = _default_item,
volume = _default_item
volume = _default_item,
};
public static LadderPanelPricePointSettings tt_default {
get {
var rv = historic_default;
rv.day_change.background_color = Color.FromRgb(113, 201, 69).to_uint();
rv.day_change.font.font_color = Colors.Black.to_uint();
return rv;
}
}
public static LadderPanelPricePointSettings Default => LadderSettings.mimic_tt ?
tt_default : historic_default;
[ProtoMember(1)]
public DaySettings day_change { get; set; }
[ProtoMember(2)]
@@ -607,11 +595,6 @@ namespace mbts.spark {
uint _ticks_per_row;
List<FairValueLine> _fair_value_lines;
static LadderSettings() {
var knobs = config.Configuration.get<bts.config.LadderKnobs>();
mimic_tt = knobs.MimicTt;
}
// Default proto constructor
// Added this so that when clients upgrade to a version with new settings,
// the default values are applied correctly; thx to John for this tip
@@ -643,9 +626,7 @@ namespace mbts.spark {
status_message_visible = true;
}
public static bool mimic_tt { get; }
public static LadderSettings historic_default {
public static LadderSettings Default {
get {
LadderColumnSettings
orders_column = LadderColumnSettings.default_orders,
@@ -713,11 +694,6 @@ namespace mbts.spark {
}
}
public static LadderSettings tt_default => historic_default;
public static LadderSettings Default => mimic_tt ?
tt_default : historic_default;
public enum BadgeStyle { Normal, Alternate }
// TODO: rename to PriceDisplayFormat (today is merge monday, since this isn't a functional change, it is not worth the potential merge headaches right now)
@@ -1039,7 +1015,6 @@ namespace mbts.spark {
a.ladder_order_limits.safe_equals(b.ladder_order_limits) &&
a._ticks_per_row == b._ticks_per_row &&
a.panel_price_point_settings.safe_equals(b.panel_price_point_settings) &&
a._ticks_per_row == b._ticks_per_row &&
EqualityComparer.safe_sequence_equals(a.fair_value_lines, b.fair_value_lines);
public bool Equals(LadderSettings other) =>
@@ -107,6 +107,7 @@ namespace mbts.spark {
ImmutableDictionary<AlgoKey, LadderFavoriteAlgoMetadata> _favorite_algo_metadata_map =
ImmutableDictionary<AlgoKey, LadderFavoriteAlgoMetadata>.Empty;
RegionalTickFactors? _spec;
IReadOnlyDictionary<uint, SecurityMD> _securities = new Dictionary<uint, SecurityMD>(0);
LadderVM(
IBus bus,
@@ -329,7 +330,8 @@ namespace mbts.spark {
}),
WorkLoop.Start($"Ladder '{security}'", _tick),
ObservableAutoWrapper.dispose_on_removed(orders, true),
Disposable.Create(() => _on_disposed_impl?.Invoke(true)));
Disposable.Create(() => _on_disposed_impl?.Invoke(true)),
bus.subscribe(SparkTags.SecuritiesTag).Subscribe(secs => _securities = secs));
PropertyAutoWrapper<LadderSettings, bool> bool_prop(
string name, Func<LadderSettings, bool> get_bool) =>
@@ -405,7 +407,9 @@ namespace mbts.spark {
event Action<bool>? _on_disposed_impl;
public IMDConsumerHandle md => _model.md;
public SecurityMD security_md => _model.security_md;
public IBus bus => _bus;
public IReadOnlyDictionary<uint, SecurityMD> securities => _securities;
// we return only entries who have piqs and whose piqs are not uint.Max
// uint.Max happens when the server is restarted but the client is still up
@@ -17,7 +17,6 @@ using Spark.Core.Disposables;
using Spark.Core.Math;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
@@ -38,9 +37,6 @@ namespace mbts.spark {
partial class LadderGridHost : Control {
static readonly int _initial_row_count;
static readonly bool _show_scrollbar;
static readonly bool _lighten_market_cells;
static readonly Color? _bid_market_cell_background;
static readonly Color? _ask_market_cell_background;
readonly VisualHost _visual_host = new();
readonly LadderOrdersHost _ordershost;
@@ -84,6 +80,7 @@ namespace mbts.spark {
LadderTactileAdorner _tactile_adorner;
ulong _prices_updated;
NativePriceSize[] _bids = new NativePriceSize[NativePriceHelpers.COCONUT_MAX_DEPTH], _asks = new NativePriceSize[NativePriceHelpers.COCONUT_MAX_DEPTH];
SecurityMDExtensions.Book _depth = SecurityMDExtensions.Book.Empty;
bool _price_dirty = false, _piq_dirty = false;
LadderPriceList _price_list = new() {
length = _initial_row_count,
@@ -96,6 +93,8 @@ namespace mbts.spark {
Func<NativePriceSize, string> _fmt_mkt_depth_item_impl = MDDisplayHelper.get_formatter_nps(
MarketSizeDisplay.get_default(), Security.Defaults.quantity_display_settingsi, Security.Defaults.price_display_settingsi);
Func<double, double, string> _fmt_mkt_quantity_impl = (v, p) => v.ToString();
Func<LadderPriceLevel, string> _fmt_price_level_impl = MDDisplayHelper.get_formatter_lpl(
MarketSizeDisplay.get_default(), Security.Defaults.quantity_display_settingsi, Security.Defaults.price_display_settingsi);
int _old_tooltip_row = int.MaxValue;
NativePriceSize? _old_last_trade;
string _direction_arrow = string.Empty;
@@ -107,17 +106,7 @@ namespace mbts.spark {
static LadderGridHost() {
var knobs = config.Configuration.get<bts.config.LadderKnobs>();
_show_scrollbar = knobs.ShowScrollbar;
_lighten_market_cells = knobs.LightenMarketCells;
_initial_row_count = (int)knobs.ScrollableTickWindowSize;
// For market cell backgrounds, 0 means null. Note that the full
// range of colors can still be expressed, as zero is merely one of
// 2^24 possible representations of 'transparent.'
if (knobs.BidMarketCellBackground != 0) {
_bid_market_cell_background = BTSColors.Ladder.bid_column_background;
}
if (knobs.AskMarketCellBackground != 0) {
_ask_market_cell_background = BTSColors.Ladder.ask_column_background;
}
}
public LadderGridHost() {
@@ -872,13 +861,13 @@ namespace mbts.spark {
switch (column.column_type) {
case ColumnType.Order: _ordershost.set_col_width(column.settings.width); break;
case ColumnType.Bid:
render_market_col(_bids, _bid_market_cell_background);
render_market_col(_depth.Bids);
break;
case ColumnType.Price:
_refresh_prices(column);
break;
case ColumnType.Ask:
render_market_col(_asks, _ask_market_cell_background);
render_market_col(_depth.Asks);
break;
case ColumnType.Volume:
_volume_container.mark_dirty();
@@ -891,9 +880,9 @@ namespace mbts.spark {
if (_mkt_break_lines.high_render_index.HasValue)
_render_session_line(_session_high_vis, _mkt_break_lines.high_render_index.Value, _settings.session_high_line.color);
}
void render_market_col(NativePriceSize[] depth, Color? market_cell_background) {
void render_market_col(IEnumerable<(double Price, double Volume, double NotionalValue)> depth) {
var mid = LadderRender.index_of(_price_list, _last_origin_price, _price_comparer);
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, _spec, column.settings, _fmt_mkt_depth_item, _lighten_market_cells, market_cell_background);
LadderRender.render_sizes(column.visual_tup.visual, column.visual_tup.glyph_context, mid, _price_list, depth, _settings.row_height, _spec, column.settings, _fmt_price_level);
}
}
_apply_column_visual_offset();
@@ -939,6 +928,7 @@ namespace mbts.spark {
// **use local vm to protect against re-assignment,
// so the events are unhooked from the same instance to which they were added
// when the subs are disposed.
Disposable.dispose(ref _vm_sub);
var local_vm = _vm;
local_vm.on_tick += _tick;
local_vm.request_recenter += on_request_recenter;
@@ -1110,6 +1100,7 @@ namespace mbts.spark {
ulong unused = 0;
bool last_trade_changed = _vm.md.get_last_trade(ref unused, out _last_trade, out DateTime _, out ulong __);
bool depth_changed = _vm.md.get_depth(ref _prices_updated, ref _bids, ref _asks, NativePriceHelpers.COCONUT_MAX_DEPTH);
_depth = _vm.security_md.Depth(_bids, _asks, _vm.securities);
_mkt_break_lines.refresh();
if (_price_list is null) {
Log.assert(_price_dirty);
@@ -1322,8 +1313,8 @@ namespace mbts.spark {
void _refresh_sizes() {
Log.assert_d(_price_list != null);
int origin_index = LadderRender.index_of(_price_list, _mkt_break_lines.origin, _price_comparer);
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _bids, _settings.row_height, _spec, _settings.bid_column, _fmt_mkt_depth_item, _lighten_market_cells, _bid_market_cell_background);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _asks, _settings.row_height, _spec, _settings.ask_column, _fmt_mkt_depth_item, _lighten_market_cells, _ask_market_cell_background);
LadderRender.render_sizes(_bid_vis_tup.visual, _bid_vis_tup.glyph_context, origin_index, _price_list, _depth.Bids, _settings.row_height, _spec, _settings.bid_column, _fmt_price_level);
LadderRender.render_sizes(_ask_vis_tup.visual, _ask_vis_tup.glyph_context, origin_index, _price_list, _depth.Asks, _settings.row_height, _spec, _settings.ask_column, _fmt_price_level);
}
/// <summary>
@@ -1506,11 +1497,13 @@ namespace mbts.spark {
static GlyphContext glyph_context(LadderColumnSettings cs) =>
GlyphContext.create(cs.font_family, cs.font_weight);
var price_display = _spec.tick_factors(0).to_api_tick_display_settingsi();
var price_display = security.regional_ticks().tick_factors(0).to_api_tick_display_settingsi();
_fmt_mkt_depth_item_impl = MDDisplayHelper.get_formatter_nps(
_settings.size_display, _qty_display, price_display);
_fmt_mkt_quantity_impl = MDDisplayHelper.get_value_formatter(
_settings.size_display, _qty_display, price_display);
_fmt_price_level_impl = MDDisplayHelper.get_formatter_lpl(
_settings.size_display, _qty_display, price_display);
_set_columns();
_apply_column_visual_offset();
if (prev_row_height != _settings.row_height)
@@ -1520,18 +1513,16 @@ namespace mbts.spark {
_mkt_break_lines.fair_value_line_views.Clear();
// Clear and re-add the columns to the viewer to ensure offset/visibility is correct
_visual_host.Clear();
if (LadderSettings.mimic_tt) {
// The session_range_vis sits beneath the grid backgrounds (on the
// z-axis) to avoid obfuscating the font color (dj wants it to be
// pure white). Historically, it sat atop the price column, with a
// low opacity to slightly darken the range of prices inside of the
// session range (i.e., the price-band of the day's trades). In
// conjuction with this change, the price column's background
// opacity is made slightly less than 1 (in the default settings),
// while the font color remains fully opaque, so this layer will
// darken the background without affecting the font color.
_visual_host.AddVisualChild(_session_range_vis);
}
// The session_range_vis sits beneath the grid backgrounds (on the
// z-axis) to avoid obfuscating the font color (dj wants it to be
// pure white). Historically, it sat atop the price column, with a
// low opacity to slightly darken the range of prices inside of the
// session range (i.e., the price-band of the day's trades). In
// conjuction with this change, the price column's background
// opacity is made slightly less than 1 (in the default settings),
// while the font color remains fully opaque, so this layer will
// darken the background without affecting the font color.
_visual_host.AddVisualChild(_session_range_vis);
// Renders the grid lines and column backgrounds
_visual_host.AddVisualChild(_gridvis);
// Orders column
@@ -1571,8 +1562,6 @@ namespace mbts.spark {
with_vis => _visual_host.RemoveVisualChild(with_vis.vis));
_visual_host.AddVisualChild(_session_low_vis);
_visual_host.AddVisualChild(_session_high_vis);
if (!LadderSettings.mimic_tt)
_visual_host.AddVisualChild(_session_range_vis);
_render_grid_background();
_init_tactile_adorner();
// this effectively sets the background of the "Last" column //deleted to fix color
@@ -1594,6 +1583,16 @@ namespace mbts.spark {
return impl(value);
}
// This is designed to be a redundant null check for _fmt_mkt_depth_item_impl.
string _fmt_price_level(LadderPriceLevel value) {
// Assign the impl delegate to a local variable, just in case it is reassigned on another thread.
// This shouldn't happen, but better to avoid the potential null ref just in case it does. Expect the unexpected.
var impl = _fmt_price_level_impl;
impl ??= MDDisplayHelper.get_formatter_lpl(
MarketSizeDisplay.get_default(), _qty_display, security.regional_ticks().tick_factors(0).to_api_tick_display_settingsi());
return impl(value);
}
// This is designed to be a redundant null check for _fmt_mkt_quantity_impl.
string _fmt_mkt_quantity(double value, double price) {
// Assign the impl delegate to a local variable, just in case it is reassigned on another thread.
@@ -0,0 +1,98 @@
namespace mbts.spark;
using bts;
using bts.utils;
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
readonly struct LadderPriceLevel : IEquatable<LadderPriceLevel> {
public LadderPriceLevel(double price = double.NaN, double volume = 0, double notionalValue = double.NaN) {
Price = price;
Volume = volume;
NotionalValue = notionalValue.is_nan() ? volume * price : notionalValue;
}
public static LadderPriceLevel Empty => new();
public double Price { get; }
public double Volume { get; }
public double NotionalValue { get; }
public LadderPriceLevel Add(double price, double volume) {
if (!(volume > 0)) {
return this;
}
if (!(Volume > 0)) {
return new(price, volume);
}
Log.assert(Price.is_real());
Log.assert(price.is_real());
var newVolume = Volume + volume;
var newPrice = (Price * Volume + price * volume) / newVolume;
return new(newPrice, newVolume);
}
public LadderPriceLevel Add(IEnumerable<(double Price, double Volume)> items) {
var (totalPrice, totalVolume) = items.Sum();
return totalVolume > 0
? Add(totalPrice / totalVolume, totalVolume)
: this;
}
public Builder ToBuilder() =>
new() { Cost = Price, Volume = Volume };
public LadderPriceLevel With(double price = double.NaN, double volume = double.NaN, double notionalValue = double.NaN) =>
new(price.if_nan(Price), volume.if_nan(Volume), notionalValue.if_nan(NotionalValue));
public bool Equals(LadderPriceLevel other) =>
MathUtils.epsilon_eq_comparer.Equals(Price, other.Price)
&& MathUtils.epsilon_eq_comparer.Equals(Volume, other.Volume);
public override bool Equals([NotNullWhen(true)] object obj) =>
obj is LadderPriceLevel other && Equals(other);
public override int GetHashCode() =>
HashCode.Combine(
MathUtils.epsilon_eq_comparer.GetHashCode(Price),
MathUtils.epsilon_eq_comparer.GetHashCode(Volume));
public class Builder {
double notionalValue = double.NaN;
public double Cost { get; set; } = double.NaN;
public double Volume { get; set; } = 0;
public double NotionalValue => notionalValue.is_nan() ? Volume * Cost : notionalValue;
public void Add(double price, double volume) {
if (!(volume > 0)) {
return;
}
Log.assert(price.is_real());
Cost += price * volume;
Volume += volume;
}
public void Add(double price, double volume, double notionalValue) {
if (!(volume > 0)) {
return;
}
Log.assert(price.is_real());
Cost += price * volume;
Volume += volume;
if (this.notionalValue.is_nan()) {
this.notionalValue = notionalValue;
} else{
this.notionalValue += notionalValue;
}
}
public LadderPriceLevel ToImmutable() =>
Volume > 0
? new(Cost / Volume, Volume, NotionalValue)
: Empty;
}
}
@@ -57,8 +57,7 @@ namespace mbts.spark {
static class LadderRender {
// Note: the orders-column has a separate "row_padding" value: Badge.DefaultMargin
// (see how it is incorporated in measure_row_height() below)
public static Thickness row_padding => LadderSettings.mimic_tt ?
new Thickness(4, 2, 4, 2) : new Thickness(6);
public static Thickness row_padding => new Thickness(4, 2, 4, 2);
const string tick_separator_period = ".";
const string tick_separator_dash = "-";
@@ -214,70 +213,78 @@ namespace mbts.spark {
}
}
public static void RenderSizes(
DrawingVisual dv,
GlyphContext glyphContext,
int originIndex,
IList<double> prices,
IEnumerable<LadderPriceLevel> levels,
double rowHeight,
RegionalTickFactors spec,
LadderColumnSettings settings,
Func<LadderPriceLevel, string> fmt) {
var bg = settings.background.rgb_color().blend((BTSColor)0x2AFFFFFF).to_brush();
using (var dc = dv.RenderOpen())
using (_draw_with_clip(dc, settings, rowHeight, prices.Count))
using (var levelsEn = (levels ?? Enumerable.Empty<LadderPriceLevel>()).GetEnumerator()) {
var fg = settings.foreground.rgb_brush();
var currIdx = originIndex;
var currPrice = prices[currIdx];
while (levelsEn.MoveNext()) {
var level = levelsEn.Current;
var ticks = spec.count_ticks(level.Price);
var price = ticks.HasValue ? spec.tickup(0, ticks.Value) : level.Price;
// prices are in descending order, so get diff from curr_price - price
var tickDiff = spec.tickdiff(currPrice, price).assert_value();
if (!MathUtils.try_increment(ref currIdx, tickDiff) || currIdx < 0 || currIdx >= prices.Count) {
continue;
}
currPrice = price;
if (bg is not null) {
var y = currIdx * rowHeight + GRID_LINE_HEIGHT;
var height = rowHeight - GRID_LINE_HEIGHT;
dc.DrawRectangle(bg, null, new Rect(0, y, settings.width, height));
}
var gr = format_text(fmt(level), currIdx, rowHeight, settings, glyphContext);
dc.DrawGlyphRun(fg, gr);
}
}
}
public static IEnumerable<LadderPriceLevel> GetLevels(IEnumerable<(double Price, double Volume, double NotionalValue)> depth, RegionalTickFactors spec) =>
depth.TakeWhile(nps => nps.Volume > 0 && nps.Price.is_real())
.AggregateConsecutiveGroups(
LadderPriceLevel.Empty.ToBuilder,
(bd, rung) => {
bd.Add(rung.Price, rung.Volume, rung.NotionalValue);
return bd;
},
rung => {
var ticks = spec.count_ticks(rung.Price);
return ticks.HasValue ? spec.tickup(0, ticks.Value) : rung.Price;
})
.Select(bd => bd.ToImmutable());
public static void render_sizes(
DrawingVisual dv,
GlyphContext glyph_context,
int origin_index,
IList<double> prices,
NativePriceSize[] rungs,
IEnumerable<(double Price, double Volume, double NotionalValue)> rungs,
double row_height,
RegionalTickFactors spec,
LadderColumnSettings settings,
Func<NativePriceSize, string> fmt,
bool lighten_market_cells,
Color? market_cell_background) {
//var background = new SolidColorBrush(settings.background.rgb_color());
Brush market_cell_background_brush;
if (market_cell_background.HasValue) {
market_cell_background_brush = new SolidColorBrush(market_cell_background.Value);
} else if (lighten_market_cells) {
market_cell_background_brush = settings.background.rgb_color()
.blend((BTSColor)0x2AFFFFFF)
.to_brush();
} else {
market_cell_background_brush = null;
}
// market_cell_background_brush = settings.background.rgb_color().blend((BTSColor)0xFF000000).to_brush();
using (var dc = dv.RenderOpen())
using (_draw_with_clip(dc, settings, row_height, prices.Count)) {
//dc.DrawRectangle(background, null, new Rect(0, 0, settings.width, prices.Count * row_height));
if (rungs.is_null_or_empty()) {
return;
}
var foreground = settings.foreground.rgb_brush();
int curr_index = origin_index;
double curr_price = prices[curr_index];
NativePriceSize curr_size = NativePriceSize.NO_MARKET;
var row_tick_groups = rungs
.TakeWhile(nps => !nps.is_nan)
.AggregateConsecutiveGroups(() => NativePriceSize.NO_MARKET,
(aggr, rung) => rung.with(
direct_size: aggr.direct_size + rung.direct_size,
implied_size: aggr.implied_size + rung.implied_size),
rung => {
var ticks = spec.count_ticks(rung.price);
return ticks.HasValue ? spec.tickup(0, ticks.Value) : rung.price;
});
foreach (var rung in row_tick_groups) {
//double price = spec.round_to_tick(rung.price);
var ticks = spec.count_ticks(rung.price);
var price = ticks.HasValue ? spec.tickup(0, ticks.Value) : rung.price;
// prices are in descending order, so get diff from curr_price - price
var tick_diff = spec.tickdiff(curr_price, price).assert_value();
if (!MathUtils.try_increment(ref curr_index, tick_diff)) {
continue;
}
curr_price = price;
if (curr_index >= 0 && curr_index < prices.Count) {
if (market_cell_background_brush != null)
dc.DrawRectangle(market_cell_background_brush, null,
new Rect(0, curr_index * row_height + GRID_LINE_HEIGHT,
settings.width, row_height - GRID_LINE_HEIGHT));
var gr = format_text(fmt(rung), curr_index, row_height, settings, glyph_context);
dc.DrawGlyphRun(foreground, gr);
} else break;
}
}
Func<LadderPriceLevel, string> fmt) {
RenderSizes(
dv,
glyph_context,
origin_index,
prices,
GetLevels(rungs, spec),
row_height,
spec,
settings,
fmt);
}
public static void render_volumes(
@@ -182,7 +182,7 @@
</Style>
<Style x:Key="CancelAll" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Grid.Row" Value="2" />
<Setter Property="Grid.Row" Value="0"/>
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center">Cancel All</TextBlock>
@@ -220,10 +220,7 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TT_CancelAll" TargetType="Button" BasedOn="{StaticResource CancelAll}">
<Setter Property="Grid.Row" Value="0"/>
</Style>
<Style x:Key="TT_CancelBuys" TargetType="Button" BasedOn="{StaticResource CancelAll}">
<Style x:Key="CancelBuys" TargetType="Button" BasedOn="{StaticResource CancelAll}">
<Setter Property="Grid.Row" Value="1" />
<Setter Property="Content">
<Setter.Value>
@@ -231,7 +228,7 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="TT_CancelSells" TargetType="Button" BasedOn="{StaticResource CancelAll}">
<Style x:Key="CancelSells" TargetType="Button" BasedOn="{StaticResource CancelAll}">
<Setter Property="Grid.Row" Value="2" />
<Setter Property="Content">
<Setter.Value>
@@ -239,83 +236,6 @@
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CancelBuys" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Grid.Row" Value="0" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center" Foreground="{Binding bid_foreground}">
Cancel Buys
</TextBlock>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Border"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{u:BTSColor UI.Button.normal_border}"
Background="{Binding bid_background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True">
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"
Name="button_content" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#99167ed3" />
<Setter
TargetName="Border"
Property="BorderBrush"
Value="{u:BTSColor UI.Button.mouseover_border}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="CancelSells" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Background" Value="{u:BTSColor MarketData.ask}" />
<Setter Property="Grid.Row" Value="1" />
<Setter Property="Content">
<Setter.Value>
<TextBlock VerticalAlignment="Center" Foreground="{Binding ask_foreground}">
Cancel Sells
</TextBlock>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border
x:Name="Border"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{u:BTSColor UI.Button.normal_border}"
Background="{Binding ask_background}"
Padding="{TemplateBinding Padding}"
SnapsToDevicePixels="True" >
<ContentPresenter
HorizontalAlignment="Center"
VerticalAlignment="Center"
RecognizesAccessKey="True"
Name="button_content" />
</Border>
<ControlTemplate.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter TargetName="Border" Property="Background" Value="#99c00000" />
<Setter
TargetName="Border"
Property="BorderBrush"
Value="{u:BTSColor UI.Button.mouseover_border}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style x:Key="SettingsButton" TargetType="Button" BasedOn="{StaticResource LadderMenuButton}">
<Setter Property="Content">
<Setter.Value>
@@ -1177,19 +1097,19 @@
<Button
Command="{Binding cancel_cmd}"
CommandParameter="Bids"
Style="{StaticResource {x:Static sp:LadderView.CancelBuysButtonStyleKey}}"
Style="{StaticResource CancelBuys}"
Visibility="{Binding
show_cancel_buys, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button
Command="{Binding cancel_cmd}"
CommandParameter="Asks"
Style="{StaticResource {x:Static sp:LadderView.CancelSellsButtonStyleKey}}"
Style="{StaticResource CancelSells}"
Visibility="{Binding
show_cancel_sells, Converter={StaticResource BooleanToVisibilityConverter}}" />
<Button
Command="{Binding cancel_cmd}"
CommandParameter="All"
Style="{StaticResource {x:Static sp:LadderView.CancelAllButtonStyleKey}}"
Style="{StaticResource CancelAll}"
Visibility="{Binding
show_cancel_all, Converter={StaticResource BooleanToVisibilityConverter}}" />
</Grid>
@@ -27,22 +27,6 @@ namespace mbts.spark {
MinZoomFactor = 0.5,
MaxZoomFactor = 1.5;
static LadderView() {
var knobs = config.Configuration.get<bts.config.LadderKnobs>();
MimicTT = knobs.MimicTt;
}
public static bool MimicTT { get; }
public static string CancelBuysButtonStyleKey =>
MimicTT ? "TT_CancelBuys" : "CancelBuys";
public static string CancelSellsButtonStyleKey =>
MimicTT ? "TT_CancelSells" : "CancelSells";
public static string CancelAllButtonStyleKey =>
MimicTT ? "TT_CancelAll" : "CancelAll";
public static IMultiValueConverter PositionToolTipConverter =
ValueConverter.create<AccountTuple?, bool, bool, bool, string>((account, show_day_pos, show_raw_day_pos, show_total_pos) => {
const string day_pos_text = "day", raw_day_pos_text = "raw day", total_pos_text = "total";
@@ -1,4 +1,4 @@
using bts;
using bts;
using bts.api;
using bts.api.core;
using bts.api.orderman;
@@ -223,6 +223,10 @@ namespace mbts.spark {
Add(qty_column = new SizeColumn(
new DoubleMinMax<OrderRecordTupleWithRoot>(
ort => ort.order.order.quantity(), nameof(OrderBookRowVM.quantity))));
// notional value
Add(notional_value_column = new NotionalValueColumn(
new DoubleMinMax<OrderRecordTupleWithRoot>(
ort => ort.notional_value, nameof(OrderBookRowVM.notional_value))));
// edit quantity column
// not usually part of the main order book, but it's useful to add it
// rather than debugging the popup window
@@ -232,6 +236,10 @@ namespace mbts.spark {
new DoubleMinMax<OrderRecordTupleWithRoot>(
ort => ort.order.order.filled_quantity(),
nameof(OrderBookRowVM.filled_quantity))));
// filled notional value
Add(filled_notional_value_column = new FilledNotionalValueColumn(
new DoubleMinMax<OrderRecordTupleWithRoot>(
ort => ort.filled_notional_value, nameof(OrderBookRowVM.filled_notional_value))));
// price
Add(price_column = new SimpleTextColumn<DoubleMinMaxState>(
Columns.Price,
@@ -363,9 +371,11 @@ namespace mbts.spark {
public SimpleTextColumn<OrderSubType[]> sub_type_column { get; }
public SideColumn side_column { get; }
public SizeColumn qty_column { get; }
public NotionalValueColumn notional_value_column { get; }
public EditableQtyColumn edit_qty_column { get; }
public SimpleTextColumn<DoubleMinMaxState> price_column { get; }
public FilledColumn filled_qty_column { get; }
public FilledNotionalValueColumn filled_notional_value_column { get; }
public SimpleTextColumn<DoubleMinMaxState> last_price_column { get; }
public SimpleTextColumn<DoubleMinMaxState> avg_price_column { get; }
public SimpleTextColumn<OrderStatus[]> status_column { get; }
@@ -391,8 +401,10 @@ namespace mbts.spark {
order_sub_types = sub_type_column.filter.get_state(),
sides = side_column.filter.get_state(),
qty = qty_column.filter.get_state(),
notional_value = notional_value_column.filter.get_state(),
price = price_column.filter.get_state(),
qty_filled = filled_qty_column.filter.get_state(),
filled_notional_value = filled_notional_value_column.filter.get_state(),
avg_price = avg_price_column.filter.get_state(),
last_price = last_price_column.filter.get_state(),
statuses = status_column.filter.get_state(),
@@ -415,7 +427,9 @@ namespace mbts.spark {
account_column.filter.load_state(fs.accounts);
price_column.filter.load_state(fs.price);
qty_column.filter.load_state(fs.qty);
notional_value_column.filter.load_state(fs.notional_value);
filled_qty_column.filter.load_state(fs.qty_filled);
filled_notional_value_column.filter.load_state(fs.filled_notional_value);
side_column.filter.load_state(fs.sides);
order_type_column.filter.load_state(fs.order_types);
root_algo_type_column.filter.load_state(fs.root_algo_types);
@@ -645,6 +659,32 @@ namespace mbts.spark {
DefaultCellPadding);
}
public class NotionalValueColumn : OrderbookColumnBase<DoubleMinMaxState>, IRenderColumn<OrderBookRowVM> {
public NotionalValueColumn(IDataGridColumnPersistentFilter<OrderRecordTupleWithRoot, DoubleMinMaxState> filter)
: base(
Columns.NotionalValue,
DefaultColumnHeaders.NotionalValue,
r => r.notional_value_text,
DefaultColumnWidths.NotionalValue,
filter,
DataGridFilterKnownTemplates.DoubleMinMax,
_wrap_comparer(r => r.notional_value)) { }
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
CellRender.draw_text(
dc,
_get_glyph_context(row),
font_size,
row.notional_value_text,
position,
row_height,
RenderUtils.get_side_brush(row),
background_brush,
horizontal_alignment,
VerticalAlignment.Center,
DefaultCellPadding);
}
public class FilledColumn : OrderbookColumnBase<DoubleMinMaxState>, IRenderColumn<OrderBookRowVM> {
public FilledColumn(IDataGridColumnPersistentFilter<OrderRecordTupleWithRoot, DoubleMinMaxState> filter)
: base(Columns.Filled, "Filled Qty", r => r.display_filled_quantity.ToString(), DefaultColumnWidths.Qty, filter, DataGridFilterKnownTemplates.DoubleMinMax, _wrap_comparer(r => r.filled_quantity)) { }
@@ -664,6 +704,32 @@ namespace mbts.spark {
DefaultCellPadding);
}
public class FilledNotionalValueColumn : OrderbookColumnBase<DoubleMinMaxState>, IRenderColumn<OrderBookRowVM> {
public FilledNotionalValueColumn(IDataGridColumnPersistentFilter<OrderRecordTupleWithRoot, DoubleMinMaxState> filter)
: base(
Columns.FilledNotionalValue,
DefaultColumnHeaders.FilledNotionalValue,
r => r.filled_notional_value_text,
DefaultColumnWidths.FilledNotionalValue,
filter,
DataGridFilterKnownTemplates.DoubleMinMax,
_wrap_comparer(r => r.filled_notional_value)) { }
public void draw(OrderBookRowVM row, DrawingContext dc, ColumnGeometry position, double row_height) =>
CellRender.draw_text(
dc,
_get_glyph_context(row),
font_size,
row.filled_notional_value_text,
position,
row_height,
RenderUtils.get_side_brush(row),
background_brush,
horizontal_alignment,
VerticalAlignment.Center,
DefaultCellPadding);
}
public class OrderBookOrderTypeColumn : OrderTypeColumn<Columns>, ICVOrderBookColumn {
readonly BindingBase _foreground_binding;
ListSortDirection? _cached_sort_direction;
@@ -1,4 +1,4 @@
using bts;
using bts;
using bts.api;
using bts.api.orderman;
using bts.collections;
@@ -108,6 +108,8 @@ namespace mbts.spark {
public double display_quantity => quantity.safe_divide(_display_qty_divisor);
public string display_quantity_text =>
quantity_display_settings.to_tick_display_settings().format(display_quantity);
public double notional_value => order_tuple.notional_value;
public string notional_value_text => _fmt_price(notional_value);
public TickDisplaySettingsImmutable quantity_display_settings {
get => _quantity_display_settings;
set {
@@ -128,6 +130,8 @@ namespace mbts.spark {
filled_quantity.safe_divide(_display_qty_divisor);
public string display_filled_quantity_text =>
quantity_display_settings.to_tick_display_settings().format(display_filled_quantity);
public double filled_notional_value => order_tuple.filled_notional_value;
public string filled_notional_value_text => _fmt_price(filled_notional_value);
public string last_msg => order.last_msg();
// piq
public uint piq => order_tuple.order.piq ?? uint.MaxValue;
@@ -355,9 +359,15 @@ namespace mbts.spark {
if (_prospective_qty_translator_backing != null)
_prospective_qty_translator_backing.actual_value = quantity;
}
if ((Columns.NotionalValue & dirty) == Columns.NotionalValue) {
RaisePropertyChanged(nameof(notional_value), nameof(notional_value_text));
}
if ((Columns.Filled & dirty) != 0) {
RaisePropertyChanged(nameof(filled_quantity), nameof(display_filled_quantity));
}
if ((Columns.FilledNotionalValue & dirty) == Columns.FilledNotionalValue) {
RaisePropertyChanged(nameof(filled_notional_value), nameof(filled_notional_value_text));
}
// badge fill
if (old_order_tuple.order.meta?.badge_color != order_tuple.order.meta?.badge_color) {
// invalidate by setting to null (will be re-instantiated in the public getter)
@@ -521,6 +531,12 @@ namespace mbts.spark {
if (oldval.order.piq != newval.order.piq) {
dirty |= Columns.Piq;
}
if (oldval.notional_value != newval.notional_value) {
dirty |= Columns.NotionalValue;
}
if (oldval.filled_notional_value != newval.filled_notional_value) {
dirty |= Columns.FilledNotionalValue;
}
return dirty;
}
}
@@ -1,160 +1,170 @@
using bts.api;
using bts.api.orderman;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.utility;
using ProtoBuf;
using Spark.Api.Orders;
using System;
namespace bts.shell.orderbook {
[ProtoContract]
class FilterStates : IEquatable<FilterStates> {
[ProtoMember(1)]
public SecurityCriteria security_criteria;
[ProtoMember(2)]
public string order_key_regex;
[ProtoMember(3)]
public TimeFilterTuple time_created;
[ProtoMember(4)]
public string[] users;
[ProtoMember(5)]
public string[] accounts;
[ProtoMember(6)]
public Venue[] exchanges;
[ProtoMember(7)]
public TimeInForce[] tifs;
//[ProtoMember(8)]
//public CustomOrderDefinition.CustomOneofCase[] order_types;
[ProtoMember(23)]
public AlgoID[] order_types;
[ProtoMember(24)]
public AlgoID[] root_algo_types;
[ProtoMember(9)]
public Side[] sides;
[ProtoMember(10)]
public DoubleMinMaxState qty;
[ProtoMember(11)]
public DoubleMinMaxState price;
[ProtoMember(12)]
public DoubleMinMaxState qty_filled;
[ProtoMember(13)]
public DoubleMinMaxState avg_price;
[ProtoMember(14)]
public DoubleMinMaxState last_price;
[ProtoMember(15)]
public OrderStatus[] statuses;
[ProtoMember(16)]
public string exchange_message_regex;
[ProtoMember(17)]
public TimeFilterTuple last_change;
//[ProtoMember(18)]
//public string security_regex;
[ProtoMember(19)]
public api.orderman.OrderSubType[] order_sub_types;
[ProtoMember(20)]
public string[] symbols;
[ProtoMember(21)]
public string parent_key_regex;
[ProtoMember(22)]
public string attached_target_order_key_regex;
[ProtoMember(25)]
public UIntMinMaxState piq;
public bool Equals(FilterStates other) =>
other != null &&
security_criteria.safe_equals(other.security_criteria) &&
order_key_regex == other.order_key_regex &&
parent_key_regex == other.parent_key_regex &&
attached_target_order_key_regex == other.attached_target_order_key_regex &&
time_created.Equals(other.time_created) &&
EqualityComparer.safe_sequence_equals(users, other.users) &&
EqualityComparer.safe_sequence_equals(accounts, other.accounts) &&
EqualityComparer.safe_sequence_equals(exchanges, other.exchanges) &&
EqualityComparer.safe_sequence_equals(tifs, other.tifs) &&
EqualityComparer.safe_sequence_equals(order_types, other.order_types) &&
EqualityComparer.safe_sequence_equals(root_algo_types, other.root_algo_types) &&
EqualityComparer.safe_sequence_equals(sides, other.sides) &&
qty.safe_equals(other.qty) &&
price.safe_equals(other.price) &&
qty_filled.safe_equals(other.qty_filled) &&
avg_price.safe_equals(other.avg_price) &&
last_price.safe_equals(other.last_price) &&
EqualityComparer.safe_sequence_equals(statuses, other.statuses) &&
exchange_message_regex == other.exchange_message_regex &&
last_change.Equals(other.last_change) &&
EqualityComparer.safe_sequence_equals(order_sub_types, other.order_sub_types) &&
EqualityComparer.safe_sequence_equals(symbols, other.symbols) &&
piq.safe_equals(other.piq);
public override bool Equals(object obj) => Equals(obj as FilterStates);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
security_criteria?.GetHashCode() ?? 0,
order_key_regex?.GetHashCode() ?? 0,
parent_key_regex?.GetHashCode() ?? 0,
attached_target_order_key_regex?.GetHashCode() ?? 0,
time_created.GetHashCode(),
EqualityComparer.safe_aggregate_hash(users),
EqualityComparer.safe_aggregate_hash(accounts),
EqualityComparer.safe_aggregate_hash(exchanges),
EqualityComparer.safe_aggregate_hash(tifs),
EqualityComparer.safe_aggregate_hash(order_types),
EqualityComparer.safe_aggregate_hash(root_algo_types),
EqualityComparer.safe_aggregate_hash(sides),
qty?.GetHashCode() ?? 0,
price?.GetHashCode() ?? 0,
qty_filled?.GetHashCode() ?? 0,
avg_price?.GetHashCode() ?? 0,
last_price?.GetHashCode() ?? 0,
EqualityComparer.safe_aggregate_hash(statuses),
exchange_message_regex?.GetHashCode() ?? 0,
last_change.GetHashCode(),
EqualityComparer.safe_aggregate_hash(order_sub_types),
EqualityComparer.safe_aggregate_hash(symbols),
piq?.GetHashCode() ?? 0);
}
static class FilterStatesExtensions {
public static Func<OrderRecordTupleWithRoot, bool> build_predicate(this FilterStates fs) {
var pb = new ColumnPredicateBuilder<OrderRecordTupleWithRoot>();
// security
pb.add_security_criteria(fs.security_criteria, o => o.order.security);
// regex
pb.add_regex(fs.order_key_regex, o => o.order.order.Key.ToString());
pb.add_regex(fs.exchange_message_regex, o => o.order.order.State.LastMsg);
pb.add_regex(fs.attached_target_order_key_regex, o => o.order.order.attached_target_order_key().ToString());
pb.add_regex(fs.parent_key_regex, o => o.order.order.ParentKey.ToString());
// time
pb.add_time_filter(fs.time_created, o => o.order.order.created());
pb.add_time_filter(fs.last_change, o => o.order.order.last_change());
// discrete values
pb.add_discrete_values(fs.users, o => o.order.order.user());
pb.add_discrete_values(fs.accounts, o => o.order.order.account());
pb.add_discrete_values(fs.exchanges, o => o.order.security?.Exchange ?? Venue.Unset);
pb.add_discrete_values(fs.tifs, o => o.order.order.time_in_force());
pb.add_discrete_values(fs.order_types, o => AlgoID.from_custom_def(o.order.order.Def.CustomDef));
pb.add_discrete_values(fs.root_algo_types, o => AlgoID.from_algo_key(o.root.algo?.key ?? default));
pb.add_discrete_values(fs.order_sub_types, o => o.order.order.sub_type());
pb.add_discrete_values(fs.sides, o => o.order.order.side());
pb.add_discrete_values(fs.statuses, o => o.order.order.status());
pb.add_discrete_values(fs.symbols, o => o.order.security?.Symbol);
// numeric
pb.add_numeric(fs.qty, o => o.order.order.quantity());
pb.add_price(fs.price, o => o.order.order.ui_price(), o => o.order.security);
pb.add_numeric(fs.qty_filled, o => o.order.order.filled_quantity());
pb.add_numeric(fs.avg_price, o => o.order.order.avg_price());
pb.add_numeric(fs.last_price, o => o.order.order.last_price());
pb.add_numeric(fs.piq, o => o.order.piq ?? uint.MaxValue);
return pb.to_predicate();
}
}
}
using bts.api;
using bts.api.orderman;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.spark.filters;
using mbts.ui;
using mbts.ui.utility;
using ProtoBuf;
using Spark.Api.Orders;
using System;
namespace bts.shell.orderbook {
[ProtoContract]
class FilterStates : IEquatable<FilterStates> {
[ProtoMember(1)]
public SecurityCriteria security_criteria;
[ProtoMember(2)]
public string order_key_regex;
[ProtoMember(3)]
public TimeFilterTuple time_created;
[ProtoMember(4)]
public string[] users;
[ProtoMember(5)]
public string[] accounts;
[ProtoMember(6)]
public Venue[] exchanges;
[ProtoMember(7)]
public TimeInForce[] tifs;
//[ProtoMember(8)]
//public CustomOrderDefinition.CustomOneofCase[] order_types;
[ProtoMember(23)]
public AlgoID[] order_types;
[ProtoMember(24)]
public AlgoID[] root_algo_types;
[ProtoMember(9)]
public Side[] sides;
[ProtoMember(10)]
public DoubleMinMaxState qty;
[ProtoMember(26)]
public DoubleMinMaxState notional_value;
[ProtoMember(11)]
public DoubleMinMaxState price;
[ProtoMember(12)]
public DoubleMinMaxState qty_filled;
[ProtoMember(27)]
public DoubleMinMaxState filled_notional_value;
[ProtoMember(13)]
public DoubleMinMaxState avg_price;
[ProtoMember(14)]
public DoubleMinMaxState last_price;
[ProtoMember(15)]
public OrderStatus[] statuses;
[ProtoMember(16)]
public string exchange_message_regex;
[ProtoMember(17)]
public TimeFilterTuple last_change;
//[ProtoMember(18)]
//public string security_regex;
[ProtoMember(19)]
public api.orderman.OrderSubType[] order_sub_types;
[ProtoMember(20)]
public string[] symbols;
[ProtoMember(21)]
public string parent_key_regex;
[ProtoMember(22)]
public string attached_target_order_key_regex;
[ProtoMember(25)]
public UIntMinMaxState piq;
public bool Equals(FilterStates other) =>
other != null &&
security_criteria.safe_equals(other.security_criteria) &&
order_key_regex == other.order_key_regex &&
parent_key_regex == other.parent_key_regex &&
attached_target_order_key_regex == other.attached_target_order_key_regex &&
time_created.Equals(other.time_created) &&
EqualityComparer.safe_sequence_equals(users, other.users) &&
EqualityComparer.safe_sequence_equals(accounts, other.accounts) &&
EqualityComparer.safe_sequence_equals(exchanges, other.exchanges) &&
EqualityComparer.safe_sequence_equals(tifs, other.tifs) &&
EqualityComparer.safe_sequence_equals(order_types, other.order_types) &&
EqualityComparer.safe_sequence_equals(root_algo_types, other.root_algo_types) &&
EqualityComparer.safe_sequence_equals(sides, other.sides) &&
qty.safe_equals(other.qty) &&
notional_value.safe_equals(other.notional_value) &&
price.safe_equals(other.price) &&
qty_filled.safe_equals(other.qty_filled) &&
filled_notional_value.safe_equals(other.filled_notional_value) &&
avg_price.safe_equals(other.avg_price) &&
last_price.safe_equals(other.last_price) &&
EqualityComparer.safe_sequence_equals(statuses, other.statuses) &&
exchange_message_regex == other.exchange_message_regex &&
last_change.Equals(other.last_change) &&
EqualityComparer.safe_sequence_equals(order_sub_types, other.order_sub_types) &&
EqualityComparer.safe_sequence_equals(symbols, other.symbols) &&
piq.safe_equals(other.piq);
public override bool Equals(object obj) => Equals(obj as FilterStates);
public override int GetHashCode() =>
EqualityComparer.aggregate_hash(
security_criteria?.GetHashCode() ?? 0,
order_key_regex?.GetHashCode() ?? 0,
parent_key_regex?.GetHashCode() ?? 0,
attached_target_order_key_regex?.GetHashCode() ?? 0,
time_created.GetHashCode(),
EqualityComparer.safe_aggregate_hash(users),
EqualityComparer.safe_aggregate_hash(accounts),
EqualityComparer.safe_aggregate_hash(exchanges),
EqualityComparer.safe_aggregate_hash(tifs),
EqualityComparer.safe_aggregate_hash(order_types),
EqualityComparer.safe_aggregate_hash(root_algo_types),
EqualityComparer.safe_aggregate_hash(sides),
qty?.GetHashCode() ?? 0,
notional_value?.GetHashCode() ?? 0,
price?.GetHashCode() ?? 0,
qty_filled?.GetHashCode() ?? 0,
filled_notional_value?.GetHashCode() ?? 0,
avg_price?.GetHashCode() ?? 0,
last_price?.GetHashCode() ?? 0,
EqualityComparer.safe_aggregate_hash(statuses),
exchange_message_regex?.GetHashCode() ?? 0,
last_change.GetHashCode(),
EqualityComparer.safe_aggregate_hash(order_sub_types),
EqualityComparer.safe_aggregate_hash(symbols),
piq?.GetHashCode() ?? 0);
}
static class FilterStatesExtensions {
public static Func<OrderRecordTupleWithRoot, bool> build_predicate(this FilterStates fs) {
var pb = new ColumnPredicateBuilder<OrderRecordTupleWithRoot>();
// security
pb.add_security_criteria(fs.security_criteria, o => o.order.security);
// regex
pb.add_regex(fs.order_key_regex, o => o.order.order.Key.ToString());
pb.add_regex(fs.exchange_message_regex, o => o.order.order.State.LastMsg);
pb.add_regex(fs.attached_target_order_key_regex, o => o.order.order.attached_target_order_key().ToString());
pb.add_regex(fs.parent_key_regex, o => o.order.order.ParentKey.ToString());
// time
pb.add_time_filter(fs.time_created, o => o.order.order.created());
pb.add_time_filter(fs.last_change, o => o.order.order.last_change());
// discrete values
pb.add_discrete_values(fs.users, o => o.order.order.user());
pb.add_discrete_values(fs.accounts, o => o.order.order.account());
pb.add_discrete_values(fs.exchanges, o => o.order.security?.Exchange ?? Venue.Unset);
pb.add_discrete_values(fs.tifs, o => o.order.order.time_in_force());
pb.add_discrete_values(fs.order_types, o => AlgoID.from_custom_def(o.order.order.Def.CustomDef));
pb.add_discrete_values(fs.root_algo_types, o => AlgoID.from_algo_key(o.root.algo?.key ?? default));
pb.add_discrete_values(fs.order_sub_types, o => o.order.order.sub_type());
pb.add_discrete_values(fs.sides, o => o.order.order.side());
pb.add_discrete_values(fs.statuses, o => o.order.order.status());
pb.add_discrete_values(fs.symbols, o => o.order.security?.Symbol);
// numeric
pb.add_numeric(fs.qty, o => o.order.order.quantity());
pb.add_numeric(fs.notional_value, o => o.notional_value);
pb.add_price(fs.price, o => o.order.order.ui_price(), o => o.order.security);
pb.add_numeric(fs.qty_filled, o => o.order.order.filled_quantity());
pb.add_numeric(fs.filled_notional_value, o => o.filled_notional_value);
pb.add_numeric(fs.avg_price, o => o.order.order.avg_price());
pb.add_numeric(fs.last_price, o => o.order.order.last_price());
pb.add_numeric(fs.piq, o => o.order.piq ?? uint.MaxValue);
return pb.to_predicate();
}
}
}
@@ -29,7 +29,7 @@ namespace bts.shell.orderbook {
static OrderBookGridSettingsManager() {
// make font-weight immutable for all columns (the font-weight rule is hard-coded: Regular for root-orders, Light for all others)
// TODO: Remove the immutable font-weight flags when cell animation is ready
var col_settings_flags = EnumHelper.get_values<Columns>().ToDictionary(
var col_settings_flags = Enum.GetValuesAsUnderlyingType<Columns>().Cast<Columns>().ToDictionary(
c => c.ToString(), c => GridColumnSettingsFeatureFlags.ImmutableFontWeight);
// make font-color immutable for Side, Size, and Filled columns (the font-color rule is hard-coded: MarketData.bid for Buy orders, MarketData.ask for Sells)
// TODO: Remove the immutable font-color flags when cell animation is ready
@@ -1,270 +1,277 @@
using bts.api.orderman;
using bts.shell.fills;
using mbts;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.settings;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using Columns = mbts.spark.OrderBookGridView.ColumnKeys;
using GridSettings = mbts.ui.settings.GridSettings;
namespace bts.shell.orderbook {
using ColHeader = mbts.ui.controls.DIYGrid.DefaultColumnHeaders;
using ColWidth = mbts.ui.controls.DIYGrid.DefaultColumnWidths;
[ProtoContract]
class OrderBookStartupData {
const bool
_default_show_right_panel = true,
_default_show_order_params = true;
const double
_default_right_panel_width = 400,
_default_order_params_height = 250,
_default_fills_height = 300,
_default_ladder_height = 350;
[ProtoMember(16)]
OrderBookRightPanelSettings _right_panel;
// ALREADY USED TAGS
// [ProtoMember(5)]
// public readonly List<SortingCriteria> sort_criteria;
public OrderBookRightPanelSettings right_panel => _right_panel ?? (_right_panel = new OrderBookRightPanelSettings());
/// TODO: remove properties below, and redirect callers to <see cref="right_panel"/>
// right panel (host)
public bool show_right_panel { get => right_panel.show; set => right_panel.show = value; }
public double right_panel_width { get => right_panel.width; set => right_panel.width = value; }
// order params
public bool show_order_params { get => right_panel.stack.show_order_params; set => right_panel.stack.show_order_params = value; }
public double order_params_height { get; set; }
// ladder
public bool show_ladder { get; set; }
public double ladder_height { get => right_panel.stack.ladder_height; set => right_panel.stack.ladder_height = value; }
// fills
public bool show_fills { get => right_panel.stack.show_fills; set => right_panel.stack.show_fills = value; }
public double fills_height { get => right_panel.stack.fills_height; set => right_panel.stack.fills_height = value; }
// filters
[ProtoMember(10)]
public FilterStates filters;
// grid
[ProtoMember(15)]
[Obsolete]
public GridSettings grid { get; set; }
// orderbook grid
[ProtoMember(17)]
public OrderBookGridSettings orderbook_grid { get; set; }
// fills settings
[ProtoMember(26)]
public OrderBookFillsGridSettings fills_settings { get; set; }
// predicate animations
[ProtoMember(20)]
[Obsolete]
List<OrderRecordTuplePredicateAnimations> _predicate_animations;
// use defaults
[ProtoMember(25)]
public bool use_defaults;
// menu display
[ProtoMember(27)]
public SparkMenuDisplaySettings menu_display { get; set; }
/// <summary>
/// Please use <see cref="Default"/> to get a new instance (the only reason the default constructor still exists is for ProtoBuf deserialization).
/// </summary>
[Obsolete]
public OrderBookStartupData() {
show_right_panel = _default_show_right_panel;
show_order_params = _default_show_order_params;
right_panel_width = _default_right_panel_width;
order_params_height = _default_order_params_height;
fills_height = _default_fills_height;
ladder_height = _default_ladder_height;
}
public OrderBookStartupData(FilterStates filters, OrderBookGridSettings orderbook_grid = default, bool show_right_panel = false, double right_panel_width = 0, double fills_height = 0, bool show_fills = false, bool use_defaults = false) {
this.filters = filters;
this.use_defaults = use_defaults;
this.show_right_panel = show_right_panel;
this.right_panel_width = right_panel_width;
this.fills_height = fills_height;
this.show_fills = show_fills;
this.orderbook_grid = orderbook_grid;
#pragma warning disable CS0612 // Type or member is obsolete
_predicate_animations = new List<OrderRecordTuplePredicateAnimations>();
if (predicate_animations != null)
_predicate_animations.AddRange(predicate_animations);
#pragma warning restore CS0612 // Type or member is obsolete
}
#pragma warning disable CS0612 // Type or member is obsolete
[Obsolete]
public IReadOnlyList<OrderRecordTuplePredicateAnimations> predicate_animations => _predicate_animations ?? (_predicate_animations = new List<OrderRecordTuplePredicateAnimations>());
public OrderBookGridSettings grid_settings_wrapper() {
if (use_defaults)
return null;
if (orderbook_grid == null) {
if (grid == null)
grid = _default_general_grid;
orderbook_grid = new OrderBookGridSettings(grid.Clone(), _predicate_animations);
}
return orderbook_grid;
}
#pragma warning restore CS0612 // Type or member is obsolete
public static OrderBookStartupData Default =>
new OrderBookStartupData(
new FilterStates { statuses = new[] { OrderStatus.Working } },
DefaultGrid,
_default_show_right_panel,
_default_right_panel_width,
_default_fills_height,
use_defaults: true) {
show_order_params = _default_show_order_params,
order_params_height = _default_order_params_height,
fills_settings = DefaultFillsGrid
};
public static OrderBookGridSettings DefaultGrid => new OrderBookGridSettings(_default_general_grid) {
algo_tree_display_mode = AlgoTreeDisplayMode.RootAndLeafNodes
};
public static OrderBookFillsGridSettings DefaultFillsGrid {
get {
var default_fills = FillsSettings.Default();
var rv = new OrderBookFillsGridSettings {
sorting_criteria = default_fills.sorting_criteria.ToList(),
filters = default_fills.filters,
general = FillsSettings.MiniGridSettings
};
return rv;
}
}
static GridSettings _default_general_grid => GridSettings.Default.with(DefaultColumns.standard, DefaultColumns.sorting_criteria);
public static OrderBookGridSettings default_mini_grid => new OrderBookGridSettings(GridSettings.Default.with(DefaultColumns.mini, DefaultColumns.sorting_criteria)) {
algo_tree_display_mode = AlgoTreeDisplayMode.RootAndLeafNodes
};
static class DefaultColumns {
// standard
public static GridColumnSettings security => new GridColumnSettings(Columns.Security.ToString(), DIYGrid.DefaultColumnHeaders.Security, width: DIYGrid.DefaultColumnWidths.Security);
public static GridColumnSettings symbol => new GridColumnSettings(Columns.Symbol.ToString(), DIYGrid.DefaultColumnHeaders.Symbol, width: DIYGrid.DefaultColumnWidths.Symbol, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings time_created => new GridColumnSettings(Columns.Time.ToString(), "Time", width: DIYGrid.DefaultColumnWidths.Time, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings last_change => new GridColumnSettings(Columns.LastChange.ToString(), "Last Change", width: DIYGrid.DefaultColumnWidths.Time, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings user => new GridColumnSettings(Columns.User.ToString(), DIYGrid.DefaultColumnHeaders.User, width: DIYGrid.DefaultColumnWidths.User, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings account => new GridColumnSettings(Columns.Account.ToString(), DIYGrid.DefaultColumnHeaders.Account, width: DIYGrid.DefaultColumnWidths.Account, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings exchange => new GridColumnSettings(Columns.Exchange.ToString(), DIYGrid.DefaultColumnHeaders.Exchange, width: DIYGrid.DefaultColumnWidths.Exchange, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings time_in_force => new GridColumnSettings(Columns.TimeInForce.ToString(), DIYGrid.DefaultColumnHeaders.TimeInForce, width: DIYGrid.DefaultColumnWidths.TimeInForce);
public static GridColumnSettings order_type => new GridColumnSettings(Columns.OrderType.ToString(), DIYGrid.DefaultColumnHeaders.OrderType, width: DIYGrid.DefaultColumnWidths.OrderType);
public static GridColumnSettings root_algo_type => new GridColumnSettings(Columns.RootAlgoType.ToString(), DIYGrid.DefaultColumnHeaders.RootAlgoType, width: DIYGrid.DefaultColumnWidths.OrderType);
public static GridColumnSettings sub_type => new GridColumnSettings(Columns.SubType.ToString(), "Sub Type", width: 80, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings side => new(
Columns.Side.ToString(), ColHeader.Side,
width: ColWidth.Side,
horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings quantity => new GridColumnSettings(Columns.Size.ToString(), "Qty", width: DIYGrid.DefaultColumnWidths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings price => new GridColumnSettings(Columns.Price.ToString(), DIYGrid.DefaultColumnHeaders.Price, width: DIYGrid.DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings filled_quantity => new(
Columns.Filled.ToString(),
ColHeader.FilledQuantity,
width: ColWidth.Filled,
horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings avg_price => new(
Columns.AvgFilled.ToString(),
ColHeader.AvgFilled,
width: ColWidth.AveragePrice,
horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings last_price => new GridColumnSettings(Columns.LastPrice.ToString(), "Last Filled", width: DIYGrid.DefaultColumnWidths.LastPrice, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings status => new GridColumnSettings(Columns.Status.ToString(), "Status", width: 80, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings last_message => new GridColumnSettings(Columns.LastMessage.ToString(), "Last Message", width: DIYGrid.DefaultColumnWidths.LastMessage, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings security_key => new GridColumnSettings(Columns.SecurityKey.ToString(), DIYGrid.DefaultColumnHeaders.SecurityKey, is_visible: false, width: DIYGrid.DefaultColumnWidths.SecurityKey, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings order_key => new GridColumnSettings(Columns.OrderKey.ToString(), DIYGrid.DefaultColumnHeaders.OrderKey, width: DIYGrid.DefaultColumnWidths.OrderKey, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings parent_key => new GridColumnSettings(Columns.ParentKey.ToString(), "Parent Key", width: DIYGrid.DefaultColumnWidths.ParentKey, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings attached_target_order_key => new GridColumnSettings(Columns.AttachedTargetOrderKey.ToString(), DIYGrid.DefaultColumnHeaders.AttachedTargetOrderKey, width: DIYGrid.DefaultColumnWidths.ParentKey, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings piq => new GridColumnSettings(Columns.Piq.ToString(), "PIQ", width: DIYGrid.DefaultColumnWidths.Qty, horizontal_alignment: HorizontalAlignment.Right);
// editable
public static GridColumnSettings cancel => new GridColumnSettings(Columns.Cancel.ToString(), DIYGrid.DefaultColumnHeaders.Cancel, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.Cancel);
public static GridColumnSettings attach_detach => new GridColumnSettings(Columns.Attach.ToString(), DIYGrid.DefaultColumnHeaders.AttachDetach, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.AttachDetach);
public static GridColumnSettings update => new GridColumnSettings(Columns.Update.ToString(), DIYGrid.DefaultColumnHeaders.Update, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.Update);
public static GridColumnSettings edit_quantity => new GridColumnSettings(Columns.EditableQty.ToString(), DIYGrid.DefaultColumnHeaders.Qty, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.EditableSize);
public static GridColumnSettings edit_price => new GridColumnSettings(Columns.EditablePrice.ToString(), DIYGrid.DefaultColumnHeaders.Price, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.EditablePrice);
public static GridColumnSettings commit => new GridColumnSettings(Columns.Commit.ToString(), width: DIYGrid.DefaultColumnWidths.MiniOrderBook.Commit);
public static IEnumerable<SortingCriteria> sorting_criteria {
get {
yield return new SortingCriteria(time_created.id, false);
}
}
public static IEnumerable<GridColumnSettings> standard {
get {
yield return security;
yield return symbol;
yield return time_created;
yield return last_change;
yield return user;
yield return account;
yield return exchange;
yield return time_in_force;
yield return order_type;
yield return root_algo_type;
yield return sub_type;
yield return side;
yield return quantity;
yield return price;
yield return piq;
yield return filled_quantity;
yield return avg_price;
yield return last_price;
yield return status;
yield return last_message;
yield return security_key;
yield return order_key;
yield return parent_key;
yield return attached_target_order_key;
}
}
public static IEnumerable<GridColumnSettings> editable {
get {
yield return cancel;
yield return update;
yield return edit_quantity;
yield return edit_price;
yield return commit;
yield return attach_detach;
}
}
private static HashSet<GridColumnSettings> _mini_mode_columns_to_hide = new HashSet<GridColumnSettings> {
symbol, last_change, exchange, sub_type, quantity, price, avg_price, last_price, status, last_message, security_key, order_key, parent_key, attached_target_order_key
};
public static IEnumerable<GridColumnSettings> mini {
get {
foreach (var col_setting in editable)
yield return col_setting;
foreach (var col_setting in standard) {
if (_mini_mode_columns_to_hide.Contains(col_setting)) {
yield return col_setting.with(is_visible: false);
} else {
yield return col_setting;
}
}
}
}
}
}
}
using bts.api.orderman;
using bts.shell.fills;
using mbts;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.settings;
using ProtoBuf;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
using Columns = mbts.spark.OrderBookGridView.ColumnKeys;
using GridSettings = mbts.ui.settings.GridSettings;
namespace bts.shell.orderbook {
using ColHeader = mbts.ui.controls.DIYGrid.DefaultColumnHeaders;
using ColWidth = mbts.ui.controls.DIYGrid.DefaultColumnWidths;
[ProtoContract]
class OrderBookStartupData {
const bool
_default_show_right_panel = true,
_default_show_order_params = true;
const double
_default_right_panel_width = 400,
_default_order_params_height = 250,
_default_fills_height = 300,
_default_ladder_height = 350;
[ProtoMember(16)]
OrderBookRightPanelSettings _right_panel;
// ALREADY USED TAGS
// [ProtoMember(5)]
// public readonly List<SortingCriteria> sort_criteria;
public OrderBookRightPanelSettings right_panel => _right_panel ?? (_right_panel = new OrderBookRightPanelSettings());
/// TODO: remove properties below, and redirect callers to <see cref="right_panel"/>
// right panel (host)
public bool show_right_panel { get => right_panel.show; set => right_panel.show = value; }
public double right_panel_width { get => right_panel.width; set => right_panel.width = value; }
// order params
public bool show_order_params { get => right_panel.stack.show_order_params; set => right_panel.stack.show_order_params = value; }
public double order_params_height { get; set; }
// ladder
public bool show_ladder { get; set; }
public double ladder_height { get => right_panel.stack.ladder_height; set => right_panel.stack.ladder_height = value; }
// fills
public bool show_fills { get => right_panel.stack.show_fills; set => right_panel.stack.show_fills = value; }
public double fills_height { get => right_panel.stack.fills_height; set => right_panel.stack.fills_height = value; }
// filters
[ProtoMember(10)]
public FilterStates filters;
// grid
[ProtoMember(15)]
[Obsolete]
public GridSettings grid { get; set; }
// orderbook grid
[ProtoMember(17)]
public OrderBookGridSettings orderbook_grid { get; set; }
// fills settings
[ProtoMember(26)]
public OrderBookFillsGridSettings fills_settings { get; set; }
// predicate animations
[ProtoMember(20)]
[Obsolete]
List<OrderRecordTuplePredicateAnimations> _predicate_animations;
// use defaults
[ProtoMember(25)]
public bool use_defaults;
// menu display
[ProtoMember(27)]
public SparkMenuDisplaySettings menu_display { get; set; }
/// <summary>
/// Please use <see cref="Default"/> to get a new instance (the only reason the default constructor still exists is for ProtoBuf deserialization).
/// </summary>
[Obsolete]
public OrderBookStartupData() {
show_right_panel = _default_show_right_panel;
show_order_params = _default_show_order_params;
right_panel_width = _default_right_panel_width;
order_params_height = _default_order_params_height;
fills_height = _default_fills_height;
ladder_height = _default_ladder_height;
}
public OrderBookStartupData(FilterStates filters, OrderBookGridSettings orderbook_grid = default, bool show_right_panel = false, double right_panel_width = 0, double fills_height = 0, bool show_fills = false, bool use_defaults = false) {
this.filters = filters;
this.use_defaults = use_defaults;
this.show_right_panel = show_right_panel;
this.right_panel_width = right_panel_width;
this.fills_height = fills_height;
this.show_fills = show_fills;
this.orderbook_grid = orderbook_grid;
#pragma warning disable CS0612 // Type or member is obsolete
_predicate_animations = new List<OrderRecordTuplePredicateAnimations>();
if (predicate_animations != null)
_predicate_animations.AddRange(predicate_animations);
#pragma warning restore CS0612 // Type or member is obsolete
}
#pragma warning disable CS0612 // Type or member is obsolete
[Obsolete]
public IReadOnlyList<OrderRecordTuplePredicateAnimations> predicate_animations => _predicate_animations ?? (_predicate_animations = new List<OrderRecordTuplePredicateAnimations>());
public OrderBookGridSettings grid_settings_wrapper() {
if (use_defaults)
return null;
if (orderbook_grid == null) {
if (grid == null)
grid = _default_general_grid;
orderbook_grid = new OrderBookGridSettings(grid.Clone(), _predicate_animations);
}
return orderbook_grid;
}
#pragma warning restore CS0612 // Type or member is obsolete
public static OrderBookStartupData Default =>
new OrderBookStartupData(
new FilterStates { statuses = new[] { OrderStatus.Working } },
DefaultGrid,
_default_show_right_panel,
_default_right_panel_width,
_default_fills_height,
use_defaults: true) {
show_order_params = _default_show_order_params,
order_params_height = _default_order_params_height,
fills_settings = DefaultFillsGrid
};
public static OrderBookGridSettings DefaultGrid => new OrderBookGridSettings(_default_general_grid) {
algo_tree_display_mode = AlgoTreeDisplayMode.RootAndLeafNodes
};
public static OrderBookFillsGridSettings DefaultFillsGrid {
get {
var default_fills = FillsSettings.Default();
var rv = new OrderBookFillsGridSettings {
sorting_criteria = default_fills.sorting_criteria.ToList(),
filters = default_fills.filters,
general = FillsSettings.MiniGridSettings
};
return rv;
}
}
static GridSettings _default_general_grid => GridSettings.Default.with(DefaultColumns.standard, DefaultColumns.sorting_criteria);
public static OrderBookGridSettings default_mini_grid => new OrderBookGridSettings(GridSettings.Default.with(DefaultColumns.mini, DefaultColumns.sorting_criteria)) {
algo_tree_display_mode = AlgoTreeDisplayMode.RootAndLeafNodes
};
static class DefaultColumns {
// standard
public static GridColumnSettings security => new GridColumnSettings(Columns.Security.ToString(), DIYGrid.DefaultColumnHeaders.Security, width: DIYGrid.DefaultColumnWidths.Security);
public static GridColumnSettings symbol => new GridColumnSettings(Columns.Symbol.ToString(), DIYGrid.DefaultColumnHeaders.Symbol, width: DIYGrid.DefaultColumnWidths.Symbol, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings time_created => new GridColumnSettings(Columns.Time.ToString(), "Time", width: DIYGrid.DefaultColumnWidths.Time, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings last_change => new GridColumnSettings(Columns.LastChange.ToString(), "Last Change", width: DIYGrid.DefaultColumnWidths.Time, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings user => new GridColumnSettings(Columns.User.ToString(), DIYGrid.DefaultColumnHeaders.User, width: DIYGrid.DefaultColumnWidths.User, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings account => new GridColumnSettings(Columns.Account.ToString(), DIYGrid.DefaultColumnHeaders.Account, width: DIYGrid.DefaultColumnWidths.Account, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings exchange => new GridColumnSettings(Columns.Exchange.ToString(), DIYGrid.DefaultColumnHeaders.Exchange, width: DIYGrid.DefaultColumnWidths.Exchange, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings time_in_force => new GridColumnSettings(Columns.TimeInForce.ToString(), DIYGrid.DefaultColumnHeaders.TimeInForce, width: DIYGrid.DefaultColumnWidths.TimeInForce);
public static GridColumnSettings order_type => new GridColumnSettings(Columns.OrderType.ToString(), DIYGrid.DefaultColumnHeaders.OrderType, width: DIYGrid.DefaultColumnWidths.OrderType);
public static GridColumnSettings root_algo_type => new GridColumnSettings(Columns.RootAlgoType.ToString(), DIYGrid.DefaultColumnHeaders.RootAlgoType, width: DIYGrid.DefaultColumnWidths.OrderType);
public static GridColumnSettings sub_type => new GridColumnSettings(Columns.SubType.ToString(), "Sub Type", width: 80, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings side => new(
Columns.Side.ToString(), ColHeader.Side,
width: ColWidth.Side,
horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings quantity => new GridColumnSettings(Columns.Size.ToString(), "Qty", width: DIYGrid.DefaultColumnWidths.Qty, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings notional_value => new GridColumnSettings(Columns.NotionalValue.ToString(), DIYGrid.DefaultColumnHeaders.NotionalValue, width: DIYGrid.DefaultColumnWidths.NotionalValue, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings price => new GridColumnSettings(Columns.Price.ToString(), DIYGrid.DefaultColumnHeaders.Price, width: DIYGrid.DefaultColumnWidths.Price, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings filled_quantity => new(
Columns.Filled.ToString(),
ColHeader.FilledQuantity,
width: ColWidth.Filled,
horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings filled_notional_value => new(
Columns.FilledNotionalValue.ToString(),
ColHeader.FilledNotionalValue,
width: ColWidth.FilledNotionalValue,
horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings avg_price => new(
Columns.AvgFilled.ToString(),
ColHeader.AvgFilled,
width: ColWidth.AveragePrice,
horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings last_price => new GridColumnSettings(Columns.LastPrice.ToString(), "Last Filled", width: DIYGrid.DefaultColumnWidths.LastPrice, horizontal_alignment: HorizontalAlignment.Right);
public static GridColumnSettings status => new GridColumnSettings(Columns.Status.ToString(), "Status", width: 80, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings last_message => new GridColumnSettings(Columns.LastMessage.ToString(), "Last Message", width: DIYGrid.DefaultColumnWidths.LastMessage, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings security_key => new GridColumnSettings(Columns.SecurityKey.ToString(), DIYGrid.DefaultColumnHeaders.SecurityKey, is_visible: false, width: DIYGrid.DefaultColumnWidths.SecurityKey, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings order_key => new GridColumnSettings(Columns.OrderKey.ToString(), DIYGrid.DefaultColumnHeaders.OrderKey, width: DIYGrid.DefaultColumnWidths.OrderKey, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings parent_key => new GridColumnSettings(Columns.ParentKey.ToString(), "Parent Key", width: DIYGrid.DefaultColumnWidths.ParentKey, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings attached_target_order_key => new GridColumnSettings(Columns.AttachedTargetOrderKey.ToString(), DIYGrid.DefaultColumnHeaders.AttachedTargetOrderKey, width: DIYGrid.DefaultColumnWidths.ParentKey, horizontal_alignment: HorizontalAlignment.Left);
public static GridColumnSettings piq => new GridColumnSettings(Columns.Piq.ToString(), "PIQ", width: DIYGrid.DefaultColumnWidths.Qty, horizontal_alignment: HorizontalAlignment.Right);
// editable
public static GridColumnSettings cancel => new GridColumnSettings(Columns.Cancel.ToString(), DIYGrid.DefaultColumnHeaders.Cancel, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.Cancel);
public static GridColumnSettings attach_detach => new GridColumnSettings(Columns.Attach.ToString(), DIYGrid.DefaultColumnHeaders.AttachDetach, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.AttachDetach);
public static GridColumnSettings update => new GridColumnSettings(Columns.Update.ToString(), DIYGrid.DefaultColumnHeaders.Update, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.Update);
public static GridColumnSettings edit_quantity => new GridColumnSettings(Columns.EditableQty.ToString(), DIYGrid.DefaultColumnHeaders.Qty, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.EditableSize);
public static GridColumnSettings edit_price => new GridColumnSettings(Columns.EditablePrice.ToString(), DIYGrid.DefaultColumnHeaders.Price, width: DIYGrid.DefaultColumnWidths.MiniOrderBook.EditablePrice);
public static GridColumnSettings commit => new GridColumnSettings(Columns.Commit.ToString(), width: DIYGrid.DefaultColumnWidths.MiniOrderBook.Commit);
public static IEnumerable<SortingCriteria> sorting_criteria {
get {
yield return new SortingCriteria(time_created.id, false);
}
}
public static IEnumerable<GridColumnSettings> standard {
get {
yield return security;
yield return symbol;
yield return time_created;
yield return last_change;
yield return user;
yield return account;
yield return exchange;
yield return time_in_force;
yield return order_type;
yield return root_algo_type;
yield return sub_type;
yield return side;
yield return quantity;
yield return price;
yield return piq;
yield return filled_quantity;
yield return avg_price;
yield return last_price;
yield return status;
yield return last_message;
yield return security_key;
yield return order_key;
yield return parent_key;
yield return attached_target_order_key;
}
}
public static IEnumerable<GridColumnSettings> editable {
get {
yield return cancel;
yield return update;
yield return edit_quantity;
yield return edit_price;
yield return commit;
yield return attach_detach;
}
}
private static HashSet<GridColumnSettings> _mini_mode_columns_to_hide = new HashSet<GridColumnSettings> {
symbol, last_change, exchange, sub_type, quantity, price, avg_price, last_price, status, last_message, security_key, order_key, parent_key, attached_target_order_key
};
public static IEnumerable<GridColumnSettings> mini {
get {
foreach (var col_setting in editable)
yield return col_setting;
foreach (var col_setting in standard) {
if (_mini_mode_columns_to_hide.Contains(col_setting)) {
yield return col_setting.with(is_visible: false);
} else {
yield return col_setting;
}
}
}
}
}
}
}
@@ -1,107 +1,109 @@
using bts.api.orderman;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.converters;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace mbts.spark {
internal partial class OrderBookGridView : DIYGrid {
public const string
SubTypeFilterTemplateKey = "SubTypeFilterTemplate",
ExpandCollapseToggleStyleKey = "ExpandCollapseToggleStyle",
RowStyleKey = "RowStyle",
RowTemplateKey = "RowTemplateKey",
CVRowStyleKey = "CVRowStyle",
CVRowTemplateKey = "CVRowTemplateKey";
// IMPORTANT: these enums are stored as strings in the general-purpose GridColumnSettings proto, and are used to ID THE COLUMNS!
// Do NOT RENAME them! However, if you must, be sure to write an IShellWindowMigrationRule or something to account for backwards compatability.
public enum ColumnKeys {
Security = 1,
Symbol = 1 << 1,
Time = 1 << 2,
LastChange = 1 << 3,
User = 1 << 4,
Account = 1 << 5,
Exchange = 1 << 6,
TimeInForce = 1 << 7,
OrderType = 1 << 8,
SubType = 1 << 9,
Side = 1 << 10,
Size = 1 << 11,
Price = 1 << 12,
Filled = 1 << 13,
AvgFilled = 1 << 14,
LastPrice = 1 << 15,
Status = 1 << 16,
LastMessage = 1 << 17,
SecurityKey = 1 << 18,
OrderKey = 1 << 19,
ParentKey = 1 << 20,
AttachedTargetOrderKey = 1 << 21,
Cancel = 1 << 22,
EditableQty = 1 << 23,
EditablePrice = 1 << 24,
Attach = 1 << 25,
Detach = 1 << 26,
Update = 1 << 27,
Commit = 1 << 28,
RootAlgoType = 1 << 29,
Piq = 1 << 30,
Check = 1 << 31
}
public OrderBookGridView() {
InitializeComponent();
var row_template_factory = new FrameworkElementFactory(typeof(OrderBookRowView));
row_template_factory.SetValue(OrderBookRowView.BackgroundProperty, RowBackgroundHelper.row_background_binding);
RowTemplate = new DataTemplate { VisualTree = row_template_factory };
}
static class RowBackgroundHelper {
public static readonly BindingBase row_background_binding;
static RowBackgroundHelper() {
var bg_binding = new MultiBinding();
bg_binding.Bindings.Add(new Binding(nameof(OrderBookRowVM.status)));
bg_binding.Bindings.Add(new Binding(nameof(ListBoxItem.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(ListBoxItem), 1) });
double overlay_opacity = config.Configuration.get<bts.config.OrderBookOptions>().StatusColorOpacityWhenSelected;
if (overlay_opacity > 0.1 && overlay_opacity < 0.9) {
bg_binding.Converter = ValueConverter.create<OrderStatus, bool, Brush>((status, is_selected) => {
var color = _status_color(status);
return color.HasValue ? new SolidColorBrush(color.Value) { Opacity = is_selected ? overlay_opacity : 1.0 } : null;
});
} else {
bg_binding.Converter = ValueConverter.create<OrderStatus, bool, Brush>((status, is_selected) => {
if (is_selected)
return null;
var color = _status_color(status);
return color.HasValue ? new SolidColorBrush(color.Value) : null;
});
}
row_background_binding = bg_binding;
}
static Color? _status_color(OrderStatus status) {
switch (status) {
case OrderStatus.Canceled: return BTSColors.Orders.canceled_order_background;
case OrderStatus.Completed: return BTSColors.Orders.completed_order_background;
case OrderStatus.Disconnected:
case OrderStatus.RejectedBySpark:
case OrderStatus.Error:
return BTSColors.Orders.error_order_background;
default: return null;
}
}
}
}
class OrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM> {
public OrderBookRowView() : base() { }
}
}
using bts.api.orderman;
using mbts.ui;
using mbts.ui.controls;
using mbts.ui.converters;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Media;
namespace mbts.spark {
internal partial class OrderBookGridView : DIYGrid {
public const string
SubTypeFilterTemplateKey = "SubTypeFilterTemplate",
ExpandCollapseToggleStyleKey = "ExpandCollapseToggleStyle",
RowStyleKey = "RowStyle",
RowTemplateKey = "RowTemplateKey",
CVRowStyleKey = "CVRowStyle",
CVRowTemplateKey = "CVRowTemplateKey";
// IMPORTANT: these enums are stored as strings in the general-purpose GridColumnSettings proto, and are used to ID THE COLUMNS!
// Do NOT RENAME them! However, if you must, be sure to write an IShellWindowMigrationRule or something to account for backwards compatability.
public enum ColumnKeys : long {
Security = 1,
Symbol = 1 << 1,
Time = 1 << 2,
LastChange = 1 << 3,
User = 1 << 4,
Account = 1 << 5,
Exchange = 1 << 6,
TimeInForce = 1 << 7,
OrderType = 1 << 8,
SubType = 1 << 9,
Side = 1 << 10,
Size = 1 << 11,
Price = 1 << 12,
Filled = 1 << 13,
AvgFilled = 1 << 14,
LastPrice = 1 << 15,
Status = 1 << 16,
LastMessage = 1 << 17,
SecurityKey = 1 << 18,
OrderKey = 1 << 19,
ParentKey = 1 << 20,
AttachedTargetOrderKey = 1 << 21,
Cancel = 1 << 22,
EditableQty = 1 << 23,
EditablePrice = 1 << 24,
Attach = 1 << 25,
Detach = 1 << 26,
Update = 1 << 27,
Commit = 1 << 28,
RootAlgoType = 1 << 29,
Piq = 1 << 30,
Check = 1 << 31,
NotionalValue = 1L << 32,
FilledNotionalValue = 1L << 33,
}
public OrderBookGridView() {
InitializeComponent();
var row_template_factory = new FrameworkElementFactory(typeof(OrderBookRowView));
row_template_factory.SetValue(OrderBookRowView.BackgroundProperty, RowBackgroundHelper.row_background_binding);
RowTemplate = new DataTemplate { VisualTree = row_template_factory };
}
static class RowBackgroundHelper {
public static readonly BindingBase row_background_binding;
static RowBackgroundHelper() {
var bg_binding = new MultiBinding();
bg_binding.Bindings.Add(new Binding(nameof(OrderBookRowVM.status)));
bg_binding.Bindings.Add(new Binding(nameof(ListBoxItem.IsSelected)) { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(ListBoxItem), 1) });
double overlay_opacity = config.Configuration.get<bts.config.OrderBookOptions>().StatusColorOpacityWhenSelected;
if (overlay_opacity > 0.1 && overlay_opacity < 0.9) {
bg_binding.Converter = ValueConverter.create<OrderStatus, bool, Brush>((status, is_selected) => {
var color = _status_color(status);
return color.HasValue ? new SolidColorBrush(color.Value) { Opacity = is_selected ? overlay_opacity : 1.0 } : null;
});
} else {
bg_binding.Converter = ValueConverter.create<OrderStatus, bool, Brush>((status, is_selected) => {
if (is_selected)
return null;
var color = _status_color(status);
return color.HasValue ? new SolidColorBrush(color.Value) : null;
});
}
row_background_binding = bg_binding;
}
static Color? _status_color(OrderStatus status) {
switch (status) {
case OrderStatus.Canceled: return BTSColors.Orders.canceled_order_background;
case OrderStatus.Completed: return BTSColors.Orders.completed_order_background;
case OrderStatus.Disconnected:
case OrderStatus.RejectedBySpark:
case OrderStatus.Error:
return BTSColors.Orders.error_order_background;
default: return null;
}
}
}
}
class OrderBookRowView : DIYGrid.HighPerformanceRow<OrderBookRowVM> {
public OrderBookRowView() : base() { }
}
}
@@ -1,378 +1,380 @@
using bts.api;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using Spark.Core.Collections;
using Spark.Core.Data;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
using PNLKey = bts.AccountSecurityKey;
namespace bts.shell.pnl {
class PNLRowVM : DisposableViewModelBase, IEquatable<PNLRowVM> {
public static readonly IComparer<PNLRowVM> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowVM>(r => r.key);
static readonly Func<bool, string> _fmt_has_day_trades = hdt => hdt ? "Yes" : "No";
//
readonly PNLCellIndexBuilder<PNLRowData> _cell_map = new PNLCellIndexBuilder<PNLRowData>();
readonly PNLCellEmbeddedAnimationModel<PNLRowData> _animator;
readonly ISecurityMetadataStore _sec_meta_store;
bool _refresh_normalized_data = true, _is_selected;
PNLNormalizedData _normalized_data;
api.domain.TickDisplaySettingsImmutable? _quantity_display_backing;
public PNLRowVM(
PNLRowData data,
ISecurityMetadataStore sec_meta_store,
ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances,
IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers,
Func<double, string> fmt_fill_price,
Func<double, string> fmt_market_price) {
//
this.data = data;
_sec_meta_store = sec_meta_store;
_animator = new PNLCellEmbeddedAnimationModel<PNLRowData>(default_appearances, triggers);
_animator.on_row(data);
pnl_price = _cell_map.add_price_double(ColumnKeys.PNLPrice, d => d.pnl_price);
account_cell = _cell_map.add(ColumnKeys.Account, d => d.account);
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
expiration_cell = _cell_map.add(
ColumnKeys.Expiration,
d => d.expiration,
e => SecurityFormatter.format_expiration(e));
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.symbol);
security_cell = _cell_map.add(
ColumnKeys.Security,
d => d.security,
null,
EqualityComparer<SecurityImmutable?>.Default,
Comparer<SecurityImmutable?>.Default);
security_key = _cell_map.add(ColumnKeys.SecurityKey, d => d.key.security_id);
cpp = _cell_map.add(ColumnKeys.CPP, d => d.effective_cpp);
// sod
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(
ColumnKeys.SODNetPos, d => d.display_sod_net_pos, fmt_qty);
sod_long_pos = _cell_map.add_long_pos(
ColumnKeys.SODLongPos, d => d.display_sod_long_pos, fmt_qty);
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price);
sod_short_pos = _cell_map.add_short_pos(
ColumnKeys.SODShortPos, d => d.display_sod_short_pos, fmt_qty);
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price);
sod_delta = _cell_map.add_pnl_double(ColumnKeys.SODDelta, d => d.sod_delta, fmt_greek);
sod_gamma = _cell_map.add_pnl_double(ColumnKeys.SODGamma, d => d.sod_gamma, fmt_greek);
sod_vega = _cell_map.add_pnl_double(ColumnKeys.SODVega, d => d.sod_vega, fmt_greek);
sod_theta = _cell_map.add_pnl_double(ColumnKeys.SODTheta, d => d.sod_theta, fmt_greek);
// day
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(
ColumnKeys.DayNetPos, d => d.display_day_net_pos, fmt_qty);
day_long_pos = _cell_map.add_long_pos(
ColumnKeys.DayLongPos, d => d.display_day_long_pos, fmt_qty);
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price);
day_short_pos = _cell_map.add_short_pos(
ColumnKeys.DayShortPos, d => d.display_day_short_pos, fmt_qty);
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price);
day_delta = _cell_map.add_pnl_double(ColumnKeys.DayDelta, d => d.day_delta, fmt_greek);
day_gamma = _cell_map.add_pnl_double(ColumnKeys.DayGamma, d => d.day_gamma, fmt_greek);
day_vega = _cell_map.add_pnl_double(ColumnKeys.DayVega, d => d.day_vega, fmt_greek);
day_theta = _cell_map.add_pnl_double(ColumnKeys.DayTheta, d => d.day_theta, fmt_greek);
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
// working
working_net_pos = _cell_map.add_net_pos(
ColumnKeys.WorkingNetPos, d => d.display_working_net_pos, fmt_qty);
var qty_display = _sec_meta_store.get_qty_display(security);
working_long_pos = _cell_map.add_working_long(
ColumnKeys.WorkingLongPos,
d => d.working.long_data,
d => d.bid_price,
qty_display);
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price);
working_short_pos = _cell_map.add_working_short(
ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price, qty_display);
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price);
// total
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
total_net_pos = _cell_map.add_net_pos(
ColumnKeys.TotalNetPos, d => d.display_total_net_pos, fmt_qty);
total_long_pos = _cell_map.add_long_pos(
ColumnKeys.TotalLongPos, d => d.display_total_long_pos, fmt_qty);
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price);
total_short_pos = _cell_map.add_short_pos(
ColumnKeys.TotalShortPos, d => d.display_total_short_pos, fmt_qty);
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price);
total_delta = _cell_map.add_pnl_double(
ColumnKeys.TotalDelta, d => d.total_delta, fmt_greek);
total_gamma = _cell_map.add_pnl_double(
ColumnKeys.TotalGamma, d => d.total_gamma, fmt_greek);
total_vega = _cell_map.add_pnl_double(ColumnKeys.TotalVega, d => d.total_vega, fmt_greek);
total_theta = _cell_map.add_pnl_double(
ColumnKeys.TotalTheta, d => d.total_theta, fmt_greek);
// market
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
ask_price = _cell_map.add_price_double(ColumnKeys.AskPrice, d => d.ask_price);
mid_price = _cell_map.add_price_double(ColumnKeys.MidPrice, d => d.mid_price);
last_price = _cell_map.add_price_double(ColumnKeys.LastPrice, d => d.last_price);
settlement_price = _cell_map.add_price_double(ColumnKeys.SettlementPrice, d => d.settlement_price);
_set_price_formatting_delegates(fmt_fill_price, fmt_market_price);
_cell_map.on_row(data);
_cell_map.set_appearances(_animator.cell_appearances);
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
animations = new_animations.Select(kv => new mbts.ui.spark.CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
RaisePropertyChanged(nameof(animations));
});
static string fmt_greek(double v) =>
v.is_nan() ? "-" : v.ToString("#,0.00");
string fmt_qty(double v) =>
quantity_display.format(v);
}
// events
public event Action<PNLNormalizedData> updated;
public event Action<bool> is_selected_changed;
// data
public PNLKey key => data.key;
public PNLRowData data { get; private set; }
public PNLNormalizedData normalized_data {
get {
if (_refresh_normalized_data) {
_refresh_normalized_data = false;
// Only count outrights (spread positions should be normalized into outrights)
var sec_type = security?.ContractCase ?? Security.ContractOneofCase.None;
var (
sod_long,
sod_short,
sod_delta,
sod_gamma,
sod_vega,
sod_theta,
day_long,
day_short,
day_delta,
day_gamma,
day_vega,
day_theta) = sec_type switch {
Security.ContractOneofCase.Future or Security.ContractOneofCase.Option => (
data.sod.long_pos.round_to_decimal(),
data.sod.short_pos.round_to_decimal(),
data.sod.delta.round_to_decimal(),
data.sod.gamma.round_to_decimal(),
data.sod.vega.round_to_decimal(),
data.sod.theta.round_to_decimal(),
data.day.long_pos.round_to_decimal(),
data.day.short_pos.round_to_decimal(),
data.day.delta.round_to_decimal(),
data.day.gamma.round_to_decimal(),
data.day.vega.round_to_decimal(),
data.day.theta.round_to_decimal()),
_ => (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
};
var working_long = data.working_long_pos.round_to_decimal();
var working_short = data.working_short_pos.round_to_decimal();
var nd = new PNLNormalizedData() {
// sod
sod_pnl = sod_pnl.value.round_to_decimal(),
sod_long_pos = sod_long,
sod_short_pos = sod_short,
sod_delta = sod_delta,
sod_gamma = sod_gamma,
sod_vega = sod_vega,
sod_theta = sod_theta,
display_sod_long_pos = sod_long,
display_sod_short_pos = sod_short,
// day
day_pnl = day_pnl.value.round_to_decimal(),
day_long_pos = day_long,
day_short_pos = day_short,
day_delta = day_delta,
day_gamma = day_gamma,
day_vega = day_vega,
day_theta = day_theta,
display_day_long_pos = day_long,
display_day_short_pos = day_short,
// working
working_long_pos = working_long,
working_short_pos = working_short,
display_working_long_pos = working_long,
display_working_short_pos = working_short
};
if (_normalized_data != nd) {
_normalized_data = nd;
updated?.Invoke(nd);
}
}
return _normalized_data;
}
}
// security
public string account_name => data.account_name;
public Venue exchange => security?.Exchange ?? Venue.Unset;
public string symbol => security.symbol_or_default();
public DateTime expiration => data.expiration;
public Security.ContractOneofCase security_type => security?.ContractCase ?? Security.ContractOneofCase.None;
public SecurityImmutable? security => data.security;
// cells
public PNLCellVM<PNLRowData, AccountTuple> account_cell { get; }
public PNLCellVM<PNLRowData, Venue> exchange_cell { get; }
public PNLCellVM<PNLRowData, DateTime> expiration_cell { get; }
public PNLCellVM<PNLRowData, string> symbol_cell { get; }
public PNLCellVM<PNLRowData, Security.ContractOneofCase> security_type_cell { get; }
public PNLCellVM<PNLRowData, SecurityImmutable?> security_cell { get; }
public PNLCellVM<PNLRowData, uint> security_key { get; }
public PNLCellVM<PNLRowData, double> cpp { get; }
// sod
public PNLCellVM<PNLRowData, double> sod_pnl { get; }
public PNLCellVM<PNLRowData, double> sod_net_pos { get; }
public PNLCellVM<PNLRowData, double> sod_long_pos { get; }
public PNLCellVM<PNLRowData, double> sod_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> sod_short_pos { get; }
public PNLCellVM<PNLRowData, double> sod_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> sod_delta { get; }
public PNLCellVM<PNLRowData, double> sod_gamma { get; }
public PNLCellVM<PNLRowData, double> sod_vega { get; }
public PNLCellVM<PNLRowData, double> sod_theta { get; }
// day
public PNLCellVM<PNLRowData, double> day_pnl { get; }
public PNLCellVM<PNLRowData, double> day_net_pos { get; }
public PNLCellVM<PNLRowData, double> day_long_pos { get; }
public PNLCellVM<PNLRowData, double> day_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> day_short_pos { get; }
public PNLCellVM<PNLRowData, double> day_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> day_delta { get; }
public PNLCellVM<PNLRowData, double> day_gamma { get; }
public PNLCellVM<PNLRowData, double> day_vega { get; }
public PNLCellVM<PNLRowData, double> day_theta { get; }
public PNLCellVM<PNLRowData, bool> has_day_trades { get; }
// working
public PNLCellVM<PNLRowData, double> working_net_pos { get; }
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_long_pos { get; }
public PNLCellVM<PNLRowData, double> working_long_avg_price { get; }
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_short_pos { get; }
public PNLCellVM<PNLRowData, double> working_short_avg_price { get; }
// total
public PNLCellVM<PNLRowData, double> total_pnl { get; }
public PNLCellVM<PNLRowData, double> total_net_pos { get; }
public PNLCellVM<PNLRowData, double> total_long_pos { get; }
public PNLCellVM<PNLRowData, double> total_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> total_short_pos { get; }
public PNLCellVM<PNLRowData, double> total_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> total_delta { get; }
public PNLCellVM<PNLRowData, double> total_gamma { get; }
public PNLCellVM<PNLRowData, double> total_vega { get; }
public PNLCellVM<PNLRowData, double> total_theta { get; }
// (market)
public PNLCellVM<PNLRowData, double> bid_price { get; }
public PNLCellVM<PNLRowData, double> mid_price { get; }
public PNLCellVM<PNLRowData, double> ask_price { get; }
public PNLCellVM<PNLRowData, double> last_price { get; }
public PNLCellVM<PNLRowData, double> settlement_price { get; }
public PNLCellVM<PNLRowData, double> pnl_price { get; }
// display
public api.domain.TickDisplaySettingsImmutable quantity_display {
get {
if (_quantity_display_backing.HasValue) {
return _quantity_display_backing.Value;
}
if (security.HasValue) {
_quantity_display_backing = _sec_meta_store.get_qty_display(security.Value);
}
return _quantity_display_backing ??= Security.Defaults.quantity_display_settingsi;
}
}
public IList<mbts.ui.spark.CellAnimation> animations { get; private set; }
public bool is_selected {
get => _is_selected;
set {
if (Set(ref _is_selected, value))
is_selected_changed?.Invoke(value);
}
}
IEnumerable<PNLCellVM<PNLRowData, double>> _market_price_cells {
get {
yield return pnl_price;
yield return last_price;
yield return bid_price;
yield return mid_price;
yield return ask_price;
yield return settlement_price;
}
}
// includes sod, day, total, and working avg price columns
IEnumerable<PNLCellVM<PNLRowData, double>> _fill_price_cells {
get {
// sod
yield return sod_long_avg_price;
yield return sod_short_avg_price;
// day
yield return day_long_avg_price;
yield return day_short_avg_price;
// total
yield return total_long_avg_price;
yield return total_short_avg_price;
// working
yield return working_long_avg_price;
yield return working_short_avg_price;
}
}
public void update(PNLRowData data) {
_quantity_display_backing = null;
_refresh_normalized_data = true;
this.data = data;
_cell_map.on_row(data);
_animator.on_row(data);
_cell_map.set_appearances(_animator.cell_appearances);
}
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers, Func<double, string> fmt_fill_price, Func<double, string> fmt_market_price) {
_animator.on_settings(defaults, triggers);
_cell_map.set_appearances(_animator.cell_appearances);
_set_price_formatting_delegates(fmt_fill_price, fmt_market_price);
}
public bool Equals(PNLRowVM other) =>
other != null && key == other.key;
public override int GetHashCode() =>
key.GetHashCode();
public override string ToString() =>
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
void _set_price_formatting_delegates(Func<double, string> fmt_fill_price, Func<double, string> fmt_market_price) {
foreach (var cell in _fill_price_cells) {
cell.format = fmt_fill_price;
}
foreach (var cell in _market_price_cells) {
cell.format = fmt_market_price;
}
}
}
}
using bts.api;
using bts.utils;
using mbts;
using mbts.spark;
using mbts.ui;
using mbts.ui.shell;
using Spark.Core.Collections;
using Spark.Core.Data;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reactive.Linq;
using ColumnKeys = bts.shell.pnl.PNLView.Columns;
using PNLKey = bts.AccountSecurityKey;
namespace bts.shell.pnl {
class PNLRowVM : DisposableViewModelBase, IEquatable<PNLRowVM> {
public static readonly IComparer<PNLRowVM> key_comparer = Comparer<PNLKey>.Default.wrap<PNLKey, PNLRowVM>(r => r.key);
static readonly Func<bool, string> _fmt_has_day_trades = hdt => hdt ? "Yes" : "No";
//
readonly PNLCellIndexBuilder<PNLRowData> _cell_map = new PNLCellIndexBuilder<PNLRowData>();
readonly PNLCellEmbeddedAnimationModel<PNLRowData> _animator;
readonly ISecurityMetadataStore _sec_meta_store;
bool _refresh_normalized_data = true, _is_selected;
PNLNormalizedData _normalized_data;
api.domain.TickDisplaySettingsImmutable? _quantity_display_backing;
public PNLRowVM(
PNLRowData data,
ISecurityMetadataStore sec_meta_store,
ImmutableDictionary<ColumnKeys, PNLCellAppearance> default_appearances,
IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers,
Func<double, string> fmt_fill_price,
Func<double, string> fmt_market_price) {
//
this.data = data;
_sec_meta_store = sec_meta_store;
_animator = new PNLCellEmbeddedAnimationModel<PNLRowData>(default_appearances, triggers);
_animator.on_row(data);
pnl_price = _cell_map.add_price_double(ColumnKeys.PNLPrice, d => d.pnl_price);
account_cell = _cell_map.add(ColumnKeys.Account, d => d.account);
security_type_cell = _cell_map.add(ColumnKeys.SecurityType, d => d.security?.ContractCase ?? Security.ContractOneofCase.None);
exchange_cell = _cell_map.add(ColumnKeys.Exchange, d => d.security?.Exchange ?? Venue.Unset);
expiration_cell = _cell_map.add(
ColumnKeys.Expiration,
d => d.expiration,
e => e == DateTime.MinValue
? "N/A"
: SecurityFormatter.format_expiration(e));
symbol_cell = _cell_map.add(ColumnKeys.Symbol, d => d.symbol);
security_cell = _cell_map.add(
ColumnKeys.Security,
d => d.security,
null,
EqualityComparer<SecurityImmutable?>.Default,
Comparer<SecurityImmutable?>.Default);
security_key = _cell_map.add(ColumnKeys.SecurityKey, d => d.key.security_id);
cpp = _cell_map.add(ColumnKeys.CPP, d => d.effective_cpp);
// sod
sod_pnl = _cell_map.add_pnl_double(ColumnKeys.SODPNL, d => d.sod_pnl);
sod_net_pos = _cell_map.add_net_pos(
ColumnKeys.SODNetPos, d => d.display_sod_net_pos, fmt_qty);
sod_long_pos = _cell_map.add_long_pos(
ColumnKeys.SODLongPos, d => d.display_sod_long_pos, fmt_qty);
sod_long_avg_price = _cell_map.add_long_price(ColumnKeys.SODLongAvgPrice, d => d.sod_long_avg_price);
sod_short_pos = _cell_map.add_short_pos(
ColumnKeys.SODShortPos, d => d.display_sod_short_pos, fmt_qty);
sod_short_avg_price = _cell_map.add_short_price(ColumnKeys.SODShortAvgPrice, d => d.sod_short_avg_price);
sod_delta = _cell_map.add_pnl_double(ColumnKeys.SODDelta, d => d.sod_delta, fmt_greek);
sod_gamma = _cell_map.add_pnl_double(ColumnKeys.SODGamma, d => d.sod_gamma, fmt_greek);
sod_vega = _cell_map.add_pnl_double(ColumnKeys.SODVega, d => d.sod_vega, fmt_greek);
sod_theta = _cell_map.add_pnl_double(ColumnKeys.SODTheta, d => d.sod_theta, fmt_greek);
// day
day_pnl = _cell_map.add_pnl_double(ColumnKeys.DayPNL, d => d.day_pnl);
day_net_pos = _cell_map.add_net_pos(
ColumnKeys.DayNetPos, d => d.display_day_net_pos, fmt_qty);
day_long_pos = _cell_map.add_long_pos(
ColumnKeys.DayLongPos, d => d.display_day_long_pos, fmt_qty);
day_long_avg_price = _cell_map.add_long_price(ColumnKeys.DayLongAvgPrice, d => d.day_long_avg_price);
day_short_pos = _cell_map.add_short_pos(
ColumnKeys.DayShortPos, d => d.display_day_short_pos, fmt_qty);
day_short_avg_price = _cell_map.add_short_price(ColumnKeys.DayShortAvgPrice, d => d.day_short_avg_price);
day_delta = _cell_map.add_pnl_double(ColumnKeys.DayDelta, d => d.day_delta, fmt_greek);
day_gamma = _cell_map.add_pnl_double(ColumnKeys.DayGamma, d => d.day_gamma, fmt_greek);
day_vega = _cell_map.add_pnl_double(ColumnKeys.DayVega, d => d.day_vega, fmt_greek);
day_theta = _cell_map.add_pnl_double(ColumnKeys.DayTheta, d => d.day_theta, fmt_greek);
has_day_trades = _cell_map.add(ColumnKeys.HasDayTrades, d => d.has_day_trades, _fmt_has_day_trades);
// working
working_net_pos = _cell_map.add_net_pos(
ColumnKeys.WorkingNetPos, d => d.display_working_net_pos, fmt_qty);
var qty_display = _sec_meta_store.get_qty_display(security);
working_long_pos = _cell_map.add_working_long(
ColumnKeys.WorkingLongPos,
d => d.working.long_data,
d => d.bid_price,
qty_display);
working_long_avg_price = _cell_map.add_long_price(ColumnKeys.WorkingLongAvgPrice, d => d.working_long_avg_price);
working_short_pos = _cell_map.add_working_short(
ColumnKeys.WorkingShortPos, r => r.working.short_data, r => r.ask_price, qty_display);
working_short_avg_price = _cell_map.add_short_price(ColumnKeys.WorkingShortAvgPrice, d => d.working_short_avg_price);
// total
total_pnl = _cell_map.add_pnl_double(ColumnKeys.TotalPNL, d => d.total_pnl);
total_net_pos = _cell_map.add_net_pos(
ColumnKeys.TotalNetPos, d => d.display_total_net_pos, fmt_qty);
total_long_pos = _cell_map.add_long_pos(
ColumnKeys.TotalLongPos, d => d.display_total_long_pos, fmt_qty);
total_long_avg_price = _cell_map.add_long_price(ColumnKeys.TotalLongAvgPrice, d => d.total_long_avg_price);
total_short_pos = _cell_map.add_short_pos(
ColumnKeys.TotalShortPos, d => d.display_total_short_pos, fmt_qty);
total_short_avg_price = _cell_map.add_short_price(ColumnKeys.TotalShortAvgPrice, d => d.total_short_avg_price);
total_delta = _cell_map.add_pnl_double(
ColumnKeys.TotalDelta, d => d.total_delta, fmt_greek);
total_gamma = _cell_map.add_pnl_double(
ColumnKeys.TotalGamma, d => d.total_gamma, fmt_greek);
total_vega = _cell_map.add_pnl_double(ColumnKeys.TotalVega, d => d.total_vega, fmt_greek);
total_theta = _cell_map.add_pnl_double(
ColumnKeys.TotalTheta, d => d.total_theta, fmt_greek);
// market
bid_price = _cell_map.add_price_double(ColumnKeys.BidPrice, d => d.bid_price);
ask_price = _cell_map.add_price_double(ColumnKeys.AskPrice, d => d.ask_price);
mid_price = _cell_map.add_price_double(ColumnKeys.MidPrice, d => d.mid_price);
last_price = _cell_map.add_price_double(ColumnKeys.LastPrice, d => d.last_price);
settlement_price = _cell_map.add_price_double(ColumnKeys.SettlementPrice, d => d.settlement_price);
_set_price_formatting_delegates(fmt_fill_price, fmt_market_price);
_cell_map.on_row(data);
_cell_map.set_appearances(_animator.cell_appearances);
_dispose = _cell_map.animation_model.OnDispatcher().Subscribe(new_animations => {
animations = new_animations.Select(kv => new mbts.ui.spark.CellAnimation(kv.Key.column, kv.Key.type, kv.Value.color.to_brush())).ToList();
RaisePropertyChanged(nameof(animations));
});
static string fmt_greek(double v) =>
v.is_nan() ? "-" : v.ToString("#,0.00");
string fmt_qty(double v) =>
quantity_display.format(v);
}
// events
public event Action<PNLNormalizedData> updated;
public event Action<bool> is_selected_changed;
// data
public PNLKey key => data.key;
public PNLRowData data { get; private set; }
public PNLNormalizedData normalized_data {
get {
if (_refresh_normalized_data) {
_refresh_normalized_data = false;
// Only count outrights (spread positions should be normalized into outrights)
var sec_type = security?.ContractCase ?? Security.ContractOneofCase.None;
var (
sod_long,
sod_short,
sod_delta,
sod_gamma,
sod_vega,
sod_theta,
day_long,
day_short,
day_delta,
day_gamma,
day_vega,
day_theta) = sec_type switch {
Security.ContractOneofCase.Future or Security.ContractOneofCase.Option => (
data.sod.long_pos.round_to_decimal(),
data.sod.short_pos.round_to_decimal(),
data.sod.delta.round_to_decimal(),
data.sod.gamma.round_to_decimal(),
data.sod.vega.round_to_decimal(),
data.sod.theta.round_to_decimal(),
data.day.long_pos.round_to_decimal(),
data.day.short_pos.round_to_decimal(),
data.day.delta.round_to_decimal(),
data.day.gamma.round_to_decimal(),
data.day.vega.round_to_decimal(),
data.day.theta.round_to_decimal()),
_ => (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
};
var working_long = data.working_long_pos.round_to_decimal();
var working_short = data.working_short_pos.round_to_decimal();
var nd = new PNLNormalizedData() {
// sod
sod_pnl = sod_pnl.value.round_to_decimal(),
sod_long_pos = sod_long,
sod_short_pos = sod_short,
sod_delta = sod_delta,
sod_gamma = sod_gamma,
sod_vega = sod_vega,
sod_theta = sod_theta,
display_sod_long_pos = sod_long,
display_sod_short_pos = sod_short,
// day
day_pnl = day_pnl.value.round_to_decimal(),
day_long_pos = day_long,
day_short_pos = day_short,
day_delta = day_delta,
day_gamma = day_gamma,
day_vega = day_vega,
day_theta = day_theta,
display_day_long_pos = day_long,
display_day_short_pos = day_short,
// working
working_long_pos = working_long,
working_short_pos = working_short,
display_working_long_pos = working_long,
display_working_short_pos = working_short
};
if (_normalized_data != nd) {
_normalized_data = nd;
updated?.Invoke(nd);
}
}
return _normalized_data;
}
}
// security
public string account_name => data.account_name;
public Venue exchange => security?.Exchange ?? Venue.Unset;
public string symbol => security.symbol_or_default();
public DateTime expiration => data.expiration;
public Security.ContractOneofCase security_type => security?.ContractCase ?? Security.ContractOneofCase.None;
public SecurityImmutable? security => data.security;
// cells
public PNLCellVM<PNLRowData, AccountTuple> account_cell { get; }
public PNLCellVM<PNLRowData, Venue> exchange_cell { get; }
public PNLCellVM<PNLRowData, DateTime> expiration_cell { get; }
public PNLCellVM<PNLRowData, string> symbol_cell { get; }
public PNLCellVM<PNLRowData, Security.ContractOneofCase> security_type_cell { get; }
public PNLCellVM<PNLRowData, SecurityImmutable?> security_cell { get; }
public PNLCellVM<PNLRowData, uint> security_key { get; }
public PNLCellVM<PNLRowData, double> cpp { get; }
// sod
public PNLCellVM<PNLRowData, double> sod_pnl { get; }
public PNLCellVM<PNLRowData, double> sod_net_pos { get; }
public PNLCellVM<PNLRowData, double> sod_long_pos { get; }
public PNLCellVM<PNLRowData, double> sod_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> sod_short_pos { get; }
public PNLCellVM<PNLRowData, double> sod_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> sod_delta { get; }
public PNLCellVM<PNLRowData, double> sod_gamma { get; }
public PNLCellVM<PNLRowData, double> sod_vega { get; }
public PNLCellVM<PNLRowData, double> sod_theta { get; }
// day
public PNLCellVM<PNLRowData, double> day_pnl { get; }
public PNLCellVM<PNLRowData, double> day_net_pos { get; }
public PNLCellVM<PNLRowData, double> day_long_pos { get; }
public PNLCellVM<PNLRowData, double> day_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> day_short_pos { get; }
public PNLCellVM<PNLRowData, double> day_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> day_delta { get; }
public PNLCellVM<PNLRowData, double> day_gamma { get; }
public PNLCellVM<PNLRowData, double> day_vega { get; }
public PNLCellVM<PNLRowData, double> day_theta { get; }
public PNLCellVM<PNLRowData, bool> has_day_trades { get; }
// working
public PNLCellVM<PNLRowData, double> working_net_pos { get; }
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_long_pos { get; }
public PNLCellVM<PNLRowData, double> working_long_avg_price { get; }
public PNLCellVM<PNLRowData, WorkingOrderSideData> working_short_pos { get; }
public PNLCellVM<PNLRowData, double> working_short_avg_price { get; }
// total
public PNLCellVM<PNLRowData, double> total_pnl { get; }
public PNLCellVM<PNLRowData, double> total_net_pos { get; }
public PNLCellVM<PNLRowData, double> total_long_pos { get; }
public PNLCellVM<PNLRowData, double> total_long_avg_price { get; }
public PNLCellVM<PNLRowData, double> total_short_pos { get; }
public PNLCellVM<PNLRowData, double> total_short_avg_price { get; }
public PNLCellVM<PNLRowData, double> total_delta { get; }
public PNLCellVM<PNLRowData, double> total_gamma { get; }
public PNLCellVM<PNLRowData, double> total_vega { get; }
public PNLCellVM<PNLRowData, double> total_theta { get; }
// (market)
public PNLCellVM<PNLRowData, double> bid_price { get; }
public PNLCellVM<PNLRowData, double> mid_price { get; }
public PNLCellVM<PNLRowData, double> ask_price { get; }
public PNLCellVM<PNLRowData, double> last_price { get; }
public PNLCellVM<PNLRowData, double> settlement_price { get; }
public PNLCellVM<PNLRowData, double> pnl_price { get; }
// display
public api.domain.TickDisplaySettingsImmutable quantity_display {
get {
if (_quantity_display_backing.HasValue) {
return _quantity_display_backing.Value;
}
if (security.HasValue) {
_quantity_display_backing = _sec_meta_store.get_qty_display(security.Value);
}
return _quantity_display_backing ??= Security.Defaults.quantity_display_settingsi;
}
}
public IList<mbts.ui.spark.CellAnimation> animations { get; private set; }
public bool is_selected {
get => _is_selected;
set {
if (Set(ref _is_selected, value))
is_selected_changed?.Invoke(value);
}
}
IEnumerable<PNLCellVM<PNLRowData, double>> _market_price_cells {
get {
yield return pnl_price;
yield return last_price;
yield return bid_price;
yield return mid_price;
yield return ask_price;
yield return settlement_price;
}
}
// includes sod, day, total, and working avg price columns
IEnumerable<PNLCellVM<PNLRowData, double>> _fill_price_cells {
get {
// sod
yield return sod_long_avg_price;
yield return sod_short_avg_price;
// day
yield return day_long_avg_price;
yield return day_short_avg_price;
// total
yield return total_long_avg_price;
yield return total_short_avg_price;
// working
yield return working_long_avg_price;
yield return working_short_avg_price;
}
}
public void update(PNLRowData data) {
_quantity_display_backing = null;
_refresh_normalized_data = true;
this.data = data;
_cell_map.on_row(data);
_animator.on_row(data);
_cell_map.set_appearances(_animator.cell_appearances);
}
public void on_settings(ImmutableDictionary<ColumnKeys, PNLCellAppearance> defaults, IReadOnlyList<IPNLAnimationTrigger<PNLRowData>> triggers, Func<double, string> fmt_fill_price, Func<double, string> fmt_market_price) {
_animator.on_settings(defaults, triggers);
_cell_map.set_appearances(_animator.cell_appearances);
_set_price_formatting_delegates(fmt_fill_price, fmt_market_price);
}
public bool Equals(PNLRowVM other) =>
other != null && key == other.key;
public override int GetHashCode() =>
key.GetHashCode();
public override string ToString() =>
$"account: {account_name} exchange: {exchange} symbol: {symbol} security: {security} buy_quantity: {day_long_pos} sell_quantity: {day_short_pos} pnl: {total_pnl}";
void _set_price_formatting_delegates(Func<double, string> fmt_fill_price, Func<double, string> fmt_market_price) {
foreach (var cell in _fill_price_cells) {
cell.format = fmt_fill_price;
}
foreach (var cell in _market_price_cells) {
cell.format = fmt_market_price;
}
}
}
}
@@ -65,9 +65,9 @@ namespace BCG {
public static NMSheetTheoBBOCross get_cross_state(double bid_price, double ask_price, double theo, double delta, bool is_call, double driver_tick_size, bool is_proximity_alert_enabled) {
double abs_delta = Math.Abs(delta);
abs_delta = abs_delta == 0 ? Double.Epsilon : abs_delta;
if (!Double.IsNaN(theo) && !Double.IsNaN(bid_price) && theo < bid_price)
if (!Double.IsNaN(theo) && !Double.IsNaN(bid_price) && (bid_price - theo) > _basic_crosss_eps)
return new NMSheetTheoBBOCross(CrossState.bid_cross, 0);
else if (!Double.IsNaN(theo) && !Double.IsNaN(ask_price) && theo > ask_price)
else if (!Double.IsNaN(theo) && !Double.IsNaN(ask_price) && (theo - ask_price) > _basic_crosss_eps)
return new NMSheetTheoBBOCross(CrossState.ask_cross, 0);
else if (is_proximity_alert_enabled) {
/// bool is passed to bypass doing the below
@@ -87,6 +87,7 @@ namespace BCG {
return NMSheetTheoBBOCross.get_nulled_obj();
}
private static double _basic_crosss_eps = 0.01;
private static double _n_max_ticks = 100.0;
private static NMSheetTheoBBOCross _nulled_obj = new NMSheetTheoBBOCross(CrossState.none, 0);
public static NMSheetTheoBBOCross get_nulled_obj() { return _nulled_obj; }
@@ -64,8 +64,7 @@ namespace mbts.analytics {
if (tl.tl_row.size > 0) {
start_long_pos += tl.tl_row.size;
start_long_total_cost += cost;
}
else {
} else {
start_short_pos += Math.Abs(tl.tl_row.size);
start_short_total_cost += cost;
}
@@ -78,8 +77,7 @@ namespace mbts.analytics {
if (tl.tl_row.size > 0) {
day_long_pos += tl.tl_row.size;
day_long_total_cost += cost;
}
else {
} else {
day_short_pos += Math.Abs(tl.tl_row.size);
day_short_total_cost += cost;
}
@@ -165,6 +163,13 @@ namespace mbts.analytics {
is_day_trade = _is_day_trade(tl)
};
pnl_trades.Add(pnl_tl);
} else if (tl.security_type == SecurityType.Spot) {
PNLTradeRow pnl_tl = new PNLTradeRow() {
tl_row = tl,
md = tl_md,
is_day_trade = _is_day_trade(tl)
};
pnl_trades.Add(pnl_tl);
}
}
return pnl_trades;
+17 -6
View File
@@ -32,6 +32,7 @@ namespace mbts.analytics {
return now_asset_value - init_asset_value;
}
public double driver {
get {
if (tl_row.security_type == SecurityType.Option) {
@@ -44,13 +45,18 @@ namespace mbts.analytics {
}
public double theo {
get {
if (tl_row.security_type == SecurityType.Option) {
return tl_row.call_put == PutOrCall.Call ?
opt_theo.fwd_normalized_call : opt_theo.fwd_normalized_put;
} else if (tl_row.security_type == SecurityType.Future) {
return fut_theo.theo;
switch (tl_row.security_type) {
case SecurityType.Option:
return tl_row.call_put == PutOrCall.Call ?
opt_theo.fwd_normalized_call : opt_theo.fwd_normalized_put;
case SecurityType.Future:
//return fut_theo.theo;
case SecurityType.Spot:
var bbo = top_of_book;
return (bbo.Item1 + bbo.Item2) / 2;
default:
return Double.NaN;
}
return Double.NaN;
}
}
public double pnl {
@@ -61,6 +67,8 @@ namespace mbts.analytics {
rv = _future_pnl(tl_row.price, theo);
} else if (tl_row.security_type == SecurityType.Option) {
rv = _option_pnl(tl_row.price, theo);
} else if (tl_row.security_type == SecurityType.Spot) {
rv = tl_row.size * (theo - tl_row.price);
}
return rv;
}
@@ -69,7 +77,10 @@ namespace mbts.analytics {
rv = _future_pnl(tl_row.price, theo);
} else if (tl_row.security_type == SecurityType.Option) {
rv = _option_pnl(tl_row.price, theo);
} else if (tl_row.security_type == SecurityType.Spot) {
rv = tl_row.size * tl_row.price - settle_price;
}
return rv;
}
}
@@ -36,6 +36,10 @@ namespace mbts {
}
public static bool is_unexpired(bts.api.SecurityTuple st, DateTime exp_date) {
if (st.Venue == Venue.Cboe || st.Venue == Venue.Glbx) {
// server is already getting teh righ contracts
return true;
}
TimeSpan exp_time = get_exp_time(st);
DateTime full_exp_utc = exp_date.ToUniversalTime().Date.Add(exp_time);
DateTime now_utc = DateTime.UtcNow;
@@ -43,6 +47,8 @@ namespace mbts {
}
public static bool is_unexpired(bts.api.SecurityImmutable sec) {
if (sec.security_tuple().Venue == Venue.Cboe || sec.security_tuple().Venue == Venue.Glbx)
return true;
var sec_type = sec.security_type();
switch (sec_type) {
case bts.api.SecurityType.Future:
@@ -58,9 +58,10 @@ namespace BCG {
}
// TODO: put it as a setting
bts.Log.assert(theos.Length == drivers.Length);
for (int i = 0; i < theos.Length; i++) {
theos[i] /= drivers[i];
}
// only do this when its deribit crypto option, TODO
// for (int i = 0; i < theos.Length; i++) {
// theos[i] /= drivers[i];
// }
return new SpreadValue(spread_spec, theos, deltas, gammas, vegas, adj_vegas, oevs, thetas, slope_vegas, drivers, qs_midpoint_theos, pos, additional_delta);
}
}
@@ -142,9 +143,21 @@ namespace BCG {
return tp ?? mbts.spark.TheoPoint.getNulledObject();
}
private static mbts.spark.TheoPoint _compute_single_option_leg_at_tied_price(DateTime option_expiration, double strike, bool is_call, OptionTheoInterpolationData sparse_theo_map, double driver_price) {
mbts.spark.TheoPoint tp = sparse_theo_map == null ? mbts.spark.TheoPoint.getNulledObject() : sparse_theo_map.interpolate_theos(option_expiration, strike, driver_price);
return tp ?? mbts.spark.TheoPoint.getNulledObject();
private static mbts.spark.TheoPoint _compute_single_option_leg_at_tied_price(DateTime option_expiration, double strike, bool is_call, OptionTheoInterpolationData sparse_theo_map,
ExpStrikeKeyedData<mbts.spark.TheoPoint> live_symbol_theo_map, double driver_price) {
// mbts.spark.TheoPoint tp = sparse_theo_map == null ? mbts.spark.TheoPoint.getNulledObject() : sparse_theo_map.interpolate_theos(option_expiration, strike, driver_price);
// return tp ?? mbts.spark.TheoPoint.getNulledObject();
// TOD: config here
return _compute_single_option_leg_at_tied_price_from_live(option_expiration, strike, is_call, live_symbol_theo_map, driver_price);
}
private static mbts.spark.TheoPoint _compute_single_option_leg_at_tied_price_from_live(DateTime option_expiration, double strike, bool is_call,
ExpStrikeKeyedData<mbts.spark.TheoPoint> live_symbol_theo_map, double driver_price) {
var tp = live_symbol_theo_map.get_data(option_expiration, strike);
if (tp == null)
return mbts.spark.TheoPoint.getNulledObject();
return TheoPoint.extrapolate(tp, driver_price, is_call);
}
/// this is the only relatively tricky thing here
@@ -174,7 +187,7 @@ namespace BCG {
if (leg_price_spec.primaryFrozen) /// tied
{
effective_driver = leg_price_spec.primary;
return _compute_single_option_leg_at_tied_price(spread_option_leg.contract.expiration, spread_option_leg.contract.strike, spread_option_leg.contract.isCall, symbol_sparse_theo_map, leg_price_spec.primary);
return _compute_single_option_leg_at_tied_price(spread_option_leg.contract.expiration, spread_option_leg.contract.strike, spread_option_leg.contract.isCall, symbol_sparse_theo_map, live_symbol_theo_map, leg_price_spec.primary);
} else {
mbts.spark.TheoPoint tp = _compute_single_option_leg_at_live_price(spread_option_leg.contract.expiration, spread_option_leg.contract.strike, spread_option_leg.contract.isCall, live_symbol_theo_map);
effective_driver = tp.driver;
@@ -188,7 +201,7 @@ namespace BCG {
future_theo_map.TryGetValue(driver_price_info.first, out symbol_future_theo_map);
double driver_price = StatelessFutureTheoCalculator.transform_price(symbol_future_theo_map, leg_price_spec.driverExpiration, leg_price_spec.primary, driver_price_info.second);
effective_driver = leg_price_spec.primary;
return _compute_single_option_leg_at_tied_price(spread_option_leg.contract.expiration, spread_option_leg.contract.strike, spread_option_leg.contract.isCall, symbol_sparse_theo_map, driver_price);
return _compute_single_option_leg_at_tied_price(spread_option_leg.contract.expiration, spread_option_leg.contract.strike, spread_option_leg.contract.isCall, symbol_sparse_theo_map, live_symbol_theo_map, driver_price);
} else {
bts.Log.Ui.error(String.Format("This spread is live but the driver expiration {0} is different from the system driver expiration {1} for {2}",
spread_option_leg.legPriceSpec.driverExpiration, driver_price_info.second, spread_option_leg.contract.product));
@@ -1,4 +1,4 @@
namespace mbts.ui.controls {
namespace mbts.ui.controls {
using bts;
using bts.utils;
@@ -26,7 +26,7 @@
public const double
Account = 62,
CheckMark = 42,
Currency = 54,
Currency = 68,
Exchange = 66,
FillKey = 68,
OrderKey = 78,
@@ -43,7 +43,9 @@
Side = 47,
Qty = 48,
MergedQty = 68,
NotionalValue = 68,
Filled = 56,
FilledNotionalValue = 81,
Symbol = 60,
Time = 112,
TimeOfDay = 84,
@@ -122,6 +124,7 @@
SecurityType = "Security Type",
Side = "B/S",
Qty = "Qty",
NotionalValue = "Notional",
MergedQty = "Qty",
DirectQty = "Qty (D)",
ImpliedQty = "Qty (I)",
@@ -144,6 +147,7 @@
FillKind = "Fill Kind",
FilledPrice = "Fill Price",
FilledQuantity = "Fill Qty",
FilledNotionalValue = "Fill Notional",
FillStatus = "Fill Status",
Name = "Name",
Description = "Description",
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -100,7 +100,7 @@
}
void PerformMEFComposition(string config_source) {
Log.Ui.info($"this is v2.2.1 Generating MEF dependencies for the shell" +
Log.Ui.info($"Generating MEF dependencies for the shell" +
$"({typeof(IInitializable)}s, {typeof(IShellModule)}s, " +
$"{typeof(IShellDaemon)}s, etc.)");
try {
@@ -154,37 +154,48 @@ namespace mbts.ui.spark {
static class MDDisplayHelper {
/// This delegate should be created by calling <see cref="get_formatter(MarketSizeDisplay)"/>
public delegate string SizeFormatter(double merged, double implied, double price);
public delegate string SizeFormatter(double merged, double implied, double price, double notional);
/// The private delegates below use refs bc the <see cref="SizeFormatter"/> returned by <see cref="get_formatter(MarketSizeDisplay)"/>
/// internally uses several <see cref="_SizeFormatter"/>s to add different text components to the resultant string.
/// The refs, therefor, avoid making extra copies of the variables.
/// These are private members, so any code-readability cost is confined to a limited scope.
delegate void _SizeFormatter(ref double merged, ref double implied, ref double price, ref StringBuilder sb);
delegate void _SizeFormatter(ref double merged, ref double implied, ref double price, ref double notional, ref StringBuilder sb);
delegate void _SizeFormatterNPS(ref NativePriceSize nps, ref StringBuilder sb);
public static Func<double, double, string> get_value_formatter(
MarketSizeDisplay size_display, TickDisplaySettingsImmutable display, TickDisplaySettingsImmutable price_display) {
var raw_fmt_size = get_formatter(size_display, display, price_display);
return (q, p) => raw_fmt_size(q, 0, p);
MarketSizeDisplay size_display, TickDisplaySettingsImmutable display, TickDisplaySettingsImmutable notional_value_display) {
var raw_fmt_size = get_formatter(size_display, display, notional_value_display);
return (q, p) => raw_fmt_size(q, 0, p, double.NaN);
}
public static Func<LadderPriceLevel, string> get_formatter_lpl(
MarketSizeDisplay size_display, TickDisplaySettingsImmutable display, TickDisplaySettingsImmutable notional_value_display) {
var raw_fmt_size = get_formatter(size_display, display, notional_value_display);
return nps => raw_fmt_size(
nps.Volume,
0,
nps.Price,
nps.NotionalValue);
}
public static Func<NativePriceSize, string> get_formatter_nps(
MarketSizeDisplay size_display, TickDisplaySettingsImmutable display, TickDisplaySettingsImmutable price_display) {
var raw_fmt_size = get_formatter(size_display, display, price_display);
MarketSizeDisplay size_display, TickDisplaySettingsImmutable display, TickDisplaySettingsImmutable notional_value_display) {
var raw_fmt_size = get_formatter(size_display, display, notional_value_display);
return nps => raw_fmt_size(
nps.implied_size + nps.direct_size,
nps.implied_size,
nps.price);
nps.price,
nps.notional_size);
}
public static SizeFormatter get_formatter(
MarketSizeDisplay size_display, TickDisplaySettingsImmutable display, TickDisplaySettingsImmutable price_display) {
_SizeFormatter implied_absent = _get_formatter(size_display.normal, display, price_display);
MarketSizeDisplay size_display, TickDisplaySettingsImmutable display, TickDisplaySettingsImmutable notional_value_display) {
_SizeFormatter implied_absent = _get_formatter(size_display.normal, display, notional_value_display);
_SizeFormatter implied_present = _get_formatter(
size_display.implied_present ?? size_display.normal, display, price_display);
return (merged, implied, price) => {
size_display.implied_present ?? size_display.normal, display, notional_value_display);
return (merged, implied, price, notional) => {
StringBuilder sb = new StringBuilder();
(implied > 0 ? implied_present : implied_absent)(ref merged, ref implied, ref price, ref sb);
(implied > 0 ? implied_present : implied_absent)(ref merged, ref implied, ref price, ref notional, ref sb);
return sb.ToString();
};
}
@@ -205,13 +216,13 @@ namespace mbts.ui.spark {
static _SizeFormatter _get_formatter(
MarketSizeDisplay.SizeFormattingElement[] fmt_elements,
TickDisplaySettingsImmutable display,
TickDisplaySettingsImmutable price_display) {
TickDisplaySettingsImmutable notional_value_display) {
_SizeFormatter[] formatters = fmt_elements.is_null_or_empty() ?
Array.Empty<_SizeFormatter>() :
CollectionHelper.convert_array(fmt_elements, sfe => _get_formatter(sfe, display, price_display));
return new _SizeFormatter((ref double m, ref double i, ref double p, ref StringBuilder sb) => {
CollectionHelper.convert_array(fmt_elements, sfe => _get_formatter(sfe, display, notional_value_display));
return new _SizeFormatter((ref double m, ref double i, ref double p, ref double n, ref StringBuilder sb) => {
foreach (var f in formatters)
f(ref m, ref i, ref p, ref sb);
f(ref m, ref i, ref p, ref n, ref sb);
});
}
@@ -237,7 +248,7 @@ namespace mbts.ui.spark {
static _SizeFormatter _get_formatter(
MarketSizeDisplay.SizeFormattingElement fmt_element,
TickDisplaySettingsImmutable display,
TickDisplaySettingsImmutable price_display) =>
TickDisplaySettingsImmutable notional_value_display) =>
fmt_element.type switch {
MarketSizeDisplay.SizeFormattingElementType.Const =>
SizeTokenFormatters.char_formatter(fmt_element.data ?? string.Empty),
@@ -248,7 +259,7 @@ namespace mbts.ui.spark {
MarketSizeDisplay.SizeFormattingElementType.Merged =>
SizeTokenFormatters.merged_formatter(display),
MarketSizeDisplay.SizeFormattingElementType.MergedNotional =>
SizeTokenFormatters.merged_notional_formatter(price_display),
SizeTokenFormatters.merged_notional_formatter(notional_value_display),
_ => throw ExceptionBuilder.Argument.unexpected_case(
fmt_element.type, nameof(fmt_element.type)),
};
@@ -256,24 +267,24 @@ namespace mbts.ui.spark {
static class SizeTokenFormatters {
public static _SizeFormatter char_formatter(string data) {
string local = data.ToString();
return (ref double m, ref double i, ref double p, ref StringBuilder sb) =>
return (ref double m, ref double i, ref double p, ref double n, ref StringBuilder sb) =>
sb.Append(local);
}
public static _SizeFormatter merged_formatter(TickDisplaySettingsImmutable display) =>
(ref double m, ref double i, ref double p, ref StringBuilder sb) =>
(ref double m, ref double i, ref double p, ref double n, ref StringBuilder sb) =>
sb.Append(display.format(m));
public static _SizeFormatter direct_formatter(TickDisplaySettingsImmutable display) =>
(ref double m, ref double i, ref double p, ref StringBuilder sb) =>
(ref double m, ref double i, ref double p, ref double n, ref StringBuilder sb) =>
sb.Append(display.format(m - i));
public static _SizeFormatter implied_formatter(TickDisplaySettingsImmutable display) =>
(ref double m, ref double i, ref double p, ref StringBuilder sb) =>
(ref double m, ref double i, ref double p, ref double n, ref StringBuilder sb) =>
sb.Append(display.format(i));
public static _SizeFormatter merged_notional_formatter(TickDisplaySettingsImmutable display) =>
(ref double m, ref double i, ref double p, ref StringBuilder sb) =>
sb.Append(display.format(m * p));
(ref double m, ref double i, ref double p, ref double n, ref StringBuilder sb) =>
sb.Append(display.format(n));
public static _SizeFormatterNPS char_formatter_nps(string data) {
string local = data.ToString();
@@ -373,9 +384,10 @@ namespace mbts.ui.spark {
_display = backing(nameof(display), fmt);
string fmt() {
const double SAMPLE_PRICE = 25.0;
var impl = MDDisplayHelper.get_formatter(
owner.create(), Security.Defaults.quantity_display_settingsi, Security.Defaults.price_display_settingsi);
return impl(merged, implied, 1);
return impl(merged, implied, 1, merged * SAMPLE_PRICE);
}
}
@@ -11,6 +11,7 @@ namespace mbts {
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
public readonly struct SecurityMD : IEquatable<SecurityMD> {
public static readonly Func<SecurityMD, uint> key_fn = smd => smd.security.Id;
@@ -60,4 +61,128 @@ namespace mbts {
public static bool operator !=(SecurityMD left, SecurityMD right) =>
!(left == right);
}
static class SecurityMDExtensions {
const uint DEPTH_COUNT = 10;
[ThreadStatic]
static NativePriceSize[] leg1Bids;
[ThreadStatic]
static NativePriceSize[] leg1Asks;
static SecurityMDExtensions() {
leg1Bids = new NativePriceSize[DEPTH_COUNT];
leg1Asks = new NativePriceSize[DEPTH_COUNT];
}
public static Book Depth(this SecurityMD security, IReadOnlyDictionary<uint, SecurityMD> securities, ref ulong time) {
var bids = new NativePriceSize[DEPTH_COUNT];
var asks = new NativePriceSize[DEPTH_COUNT];
var bidsOut = new (double, double, double)[(int)DEPTH_COUNT];
var asksOut = new (double, double, double)[(int)DEPTH_COUNT];
if (!security.security.HasSynthetic) {
security.md.get_depth(ref time, ref bids, ref asks, DEPTH_COUNT);
for (int i = 0; i < DEPTH_COUNT; i++) {
bidsOut[i] = (bids[i].price, bids[i].size, bids[i].notional_size);
asksOut[i] = (asks[i].price, asks[i].size, asks[i].notional_size);
}
return new(bidsOut, asksOut);
}
Log.assert(security.security.Synthetic.Legs.TryGetFirst(out var leg1));
if (!securities.TryGetValue(leg1.Security.Id, out var sec1)) {
return Book.Empty;
}
ulong sec1Time = 0;
if (!sec1.md.get_depth(ref sec1Time, ref leg1Bids, ref leg1Asks, DEPTH_COUNT)) {
return Book.Empty;
}
security.md.get_depth(ref time, ref bids, ref asks, DEPTH_COUNT);
int leg1BidsLevel = 0;
int leg1AsksLevel = 0;
double leg1BidsConsumed = 0;
double leg1AsksConsumed = 0;
double takeQuantity;
double notional;
for (int i = 0; i < DEPTH_COUNT && !bids[i].is_nan; i++) {
takeQuantity = bids[i].size * leg1.Quantity;
notional = getNotionalValue(ref leg1Bids, ref leg1BidsLevel, ref leg1BidsConsumed, ref takeQuantity);
bidsOut[i] = (bids[i].price, bids[i].size, notional);
takeQuantity = asks[i].size * leg1.Quantity;
notional = getNotionalValue(ref leg1Asks, ref leg1AsksLevel, ref leg1AsksConsumed, ref takeQuantity);
asksOut[i] = (asks[i].price, asks[i].size, notional);
}
return new(bidsOut, asksOut);
}
public static Book Depth(this SecurityMD security, NativePriceSize[] bids, NativePriceSize[] asks, IReadOnlyDictionary<uint, SecurityMD> securities) {
bids ??= new NativePriceSize[NativePriceHelpers.COCONUT_MAX_DEPTH];
asks ??= new NativePriceSize[NativePriceHelpers.COCONUT_MAX_DEPTH];
var bidsOut = new (double, double, double)[bids.Length];
var asksOut = new (double, double, double)[asks.Length];
var depthCount = (uint)Math.Min(bids.Length, asks.Length);
if (!security.security.HasSynthetic) {
for (int i = 0; i < depthCount; i++) {
bidsOut[i] = (bids[i].price, bids[i].size, bids[i].notional_size);
asksOut[i] = (asks[i].price, asks[i].size, asks[i].notional_size);
}
return new(bidsOut, asksOut);
}
Log.assert(security.security.Synthetic.Legs.TryGetFirst(out var leg1));
if (!securities.TryGetValue(leg1.Security.Id, out var sec1)) {
return Book.Empty;
}
ulong sec1Time = 0;
if (!sec1.md.get_depth(ref sec1Time, ref leg1Bids, ref leg1Asks, depthCount)) {
return Book.Empty;
}
int leg1BidsLevel = 0;
int leg1AsksLevel = 0;
double leg1BidsConsumed = 0;
double leg1AsksConsumed = 0;
double takeQuantity;
double notional;
for (int i = 0; i < depthCount && !bids[i].is_nan; i++) {
takeQuantity = bids[i].size;
notional = getNotionalValue(ref leg1Bids, ref leg1BidsLevel, ref leg1BidsConsumed, ref takeQuantity);
bidsOut[i] = (bids[i].price, bids[i].size, notional);
takeQuantity = asks[i].size;
notional = getNotionalValue(ref leg1Asks, ref leg1AsksLevel, ref leg1AsksConsumed, ref takeQuantity);
asksOut[i] = (asks[i].price, asks[i].size, notional);
}
return new(bidsOut, asksOut);
}
static double getNotionalValue(ref NativePriceSize[] depth, ref int level, ref double levelConsumed, ref double takeQuantity) {
if (level >= depth.Length) {
return double.NaN;
}
double notionalValue = 0;
while (takeQuantity > MathUtils.epsilon<double>() && level < depth.Length && !depth[level].is_nan) {
var lvlDepth = (double)depth[level].size - levelConsumed;
if (lvlDepth <= 0) {
level++;
levelConsumed = 0;
continue;
}
var lvlTake = Math.Min(takeQuantity, lvlDepth);
notionalValue += lvlTake * depth[level].price;
levelConsumed += lvlTake;
takeQuantity -= lvlDepth;
}
return notionalValue;
}
public class Book {
public Book(IEnumerable<(double Price, double Volume, double NotionalValue)> bids, IEnumerable<(double Price, double Quantity, double NotionalValue)> asks) {
Bids = bids;
Asks = asks;
}
public static Book Empty =>
new(Array.Empty<(double, double, double)>(), Array.Empty<(double, double, double)>());
public IEnumerable<(double Price, double Volume, double NotionalValue)> Bids { get; }
public IEnumerable<(double Price, double Volume, double NotionalValue)> Asks { get; }
}
}
}
@@ -69,6 +69,20 @@ namespace Spark.Api.Orders {
public uint security_id => order.security_id;
public SecurityImmutable? security => order.security;
// TODO: remove assumption that synthetic quantities are already notional values
public double notional_value =>
(security?.HasSynthetic ?? false)
&& security.Value.Synthetic.PriceCalc == SyntheticContract.Types.PriceCalc.NotionalLeg
? order.order.quantity()
: order.order.notional_value();
// TODO: remove assumption that synthetic quantities are already notional values
public double filled_notional_value =>
(security?.HasSynthetic ?? false)
&& security.Value.Synthetic.PriceCalc == SyntheticContract.Types.PriceCalc.NotionalLeg
? order.order.filled_quantity()
: order.order.filled_notional_value();
public bool Equals(OrderRecordTupleWithRoot other) =>
order.Equals(other.order) &&
EqualityComparer.nullable_equals(_root, other._root);
@@ -48,6 +48,12 @@ namespace Spark.Api.Orders {
public static double quantity(this OrderRecordImmutable order) =>
order.HasState ? order.State.quantity() : 0;
public static double notional_value(this OrderRecordImmutable order) =>
order.quantity() * order.ui_price();
public static double filled_notional_value(this OrderRecordImmutable order) =>
order.avg_price() * order.filled_quantity();
public static double working_quantity(this OrderRecordImmutable order) =>
order.HasState ? order.State.working_quantity() : 0;
@@ -487,5 +493,8 @@ namespace Spark.Api.Orders {
CustomOrderDefinition.CustomOneofCase.Composite => "COMP",
_ => algo_type.ToString(),
};
public static double notional_value(this FillImmutable fill) =>
fill.LastShares * fill.LastPrice;
}
}
+3 -2
View File
@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Threading;
@@ -10,8 +10,9 @@ namespace bts
static int _needs_init_spark_lib = 0;
public static void init(string config_source = null) {
var version = Assembly.GetExecutingAssembly().GetName().Version;
Console.WriteLine(
$"Configuring BTS with {(config_source is null ? "defaults" : $"'{config_source}'")}");
$"Configuring Spark {version} with {(config_source is null ? "defaults" : $"'{config_source}'")}");
var args = Environment.GetCommandLineArgs();
utils.BTSNativeMethods.provide_command_line(args, args.Length);
@@ -65,10 +65,10 @@ namespace bts.utils {
public static bool epsilon_equals<T>(this T x, T y, T epsilon) where T : IFloatingPoint<T> =>
MathUtils.epsilon_equals(x, y, epsilon);
public static bool marginal_equals<T>(T x, T y) where T : IFloatingPoint<T> =>
marginal_equals(x, y, MathUtils.epsilon<T>());
public static bool MarginalEquals<T>(this T x, T y) where T : IFloatingPoint<T> =>
MarginalEquals(x, y, MathUtils.epsilon<T>());
public static bool marginal_equals<T>(T x, T y, T margin) where T : IFloatingPoint<T> =>
public static bool MarginalEquals<T>(this T x, T y, T margin) where T : IFloatingPoint<T> =>
MathUtils.marginal_equals(x, y, margin);
public static bool is_within_range<T>(
@@ -13,6 +13,6 @@ message DatabentoMarketDataConfig {
double replay_sleep_timer_sec = 50;
string key = 60;
string dataset = 70;
bool filter_securities_one_year= 80;
uint32 filter_securities_upto_n_months= 80;
bool load_reference_data = 90;
}
@@ -0,0 +1,15 @@
OkxExeCfg {
value{
account : "ACC A"
ops_id : "op_id_bts"
secret_key : "DAF6CBB09475D2520B88D2070788A976"
api_key : "4492a9ab-5784-454c-acc7-38dc2a503ed0"
passphrase : "Pakistan@123"
ws_url : "wss://wspap.okx.com:8443"
rest_url : "https://www.okx.com"
ws_ping_timeout_seconds : 10
ws_ping_interval_seconds : 25
ws_reconnect_interval_seconds : 5
ws_reconnect_max_count : 5
}
}
@@ -37,6 +37,7 @@ SymbolLists {
list {
key: "CBOE"
symbol:"SPX"
symbol:"SPXW"
}
list {
key: "BINANCE_US"
-2
View File
@@ -39,8 +39,6 @@ LadderKnobs {
security_timeout_secs: 60
use_algo_param_grid: true
max_mbo_orders: 200
lighten_market_cells: true
mimic_tt: true
scrollable_tick_window_size: 256
}
}
+16
View File
@@ -0,0 +1,16 @@
syntax = "proto3";
package bts.config;
message OkxExeCfg {
string account = 1;
string ops_id = 2;
string secret_key = 3;
string api_key = 4;
string passphrase = 5;
string ws_url = 6;
string rest_url = 7;
uint32 ws_ping_timeout_seconds = 8;
uint32 ws_ping_interval_seconds = 9;
uint32 ws_reconnect_interval_seconds = 10;
uint32 ws_reconnect_max_count = 11;
}
+4 -4
View File
@@ -82,7 +82,7 @@ message LadderKnobs {
bool show_scrollbar = 9;
// DJ wants bid/ask cells with markets (i.e., size > 0) to have a
// lighter background than those without markets.
bool lighten_market_cells = 10;
// bool lighten_market_cells = 10;
// For market cell backgrounds, 0 means 'ignore this color setting' (i.e.,
// employ the ladder's default behavior instead).
@@ -97,9 +97,9 @@ message LadderKnobs {
// Pure Red : 0xFFFF0000
// Pure Blue : 0xFF00FF00
// Pure Green : 0xFF0000FF
uint32 bid_market_cell_background = 11;
uint32 ask_market_cell_background = 12;
bool mimic_tt = 13;
// uint32 bid_market_cell_background = 11;
// uint32 ask_market_cell_background = 12;
// bool mimic_tt = 13;
// Defines the number of price rows rendered by the ladder. The smaller this
// window is, the cheaper it is to render the ladder grid (redraw prices,
// sizes, etc.). However, if the window is too small, scrolling may appear
+1
View File
@@ -205,6 +205,7 @@ message SyntheticContract {
enum PriceCalc {
PRICECALC_STANDARD = 0;
PRICECALC_NET_CHANGE = 1;
PRICECALC_NOTIONAL_LEG = 2;
// RATIO = 2;
}
}
+28 -1
View File
@@ -34,7 +34,31 @@ static size_t write_headers(
void *ptr, size_t size, size_t nmemb, IHttpResponse *data) {
return data->write_headers((char *)ptr, size, nmemb);
}
// static int curl_debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, void *userptr) {
// (void)handle;
// (void)type;
// (void)userptr;
// switch (type) {
// case CURLINFO_TEXT:
// LOG(Http, Info, "CURLINFO_TEXT: {}", std::string(data, size));
// break;
// case CURLINFO_HEADER_IN:
// LOG(Http, Info, "Header In: {}", std::string(data, size));
// break;
// case CURLINFO_HEADER_OUT:
// LOG(Http, Info, "Header Out: {}", std::string(data, size));
// break;
// case CURLINFO_DATA_IN:
// LOG(Http, Info, "Data In: {} bytes, in: {}", size,std::string(data, size));
// break;
// case CURLINFO_DATA_OUT:
// LOG(Http, Info, "Data Out: {} bytes, out: {}", size, std::string(data, size));
// break;
// default:
// break;
// }
// return 0;
// }
void HttpClient::get(
string url,
unordered_map<string, string> headers,
@@ -91,6 +115,8 @@ void HttpClient::send(const HttpRequest &req, IHttpResponse *response) {
CURL *curl = session.curl;
CurlHeaders curl_headers{req.headers};
// Prepare req
//to debug curl requests
// curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, curl_debug_callback);
curl_easy_setopt(curl, CURLOPT_FAILONERROR, true);
curl_easy_setopt(curl, CURLOPT_URL, req.url.c_str());
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curl_headers.data());
@@ -108,6 +134,7 @@ void HttpClient::send(const HttpRequest &req, IHttpResponse *response) {
curl_easy_setopt(curl, CURLOPT_HTTPPOST, 1L);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req.body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, req.body.size());
// curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
break;
case io::http::HttpMethod::PUT:
curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "PUT");
+11 -3
View File
@@ -8,6 +8,7 @@
#include "lib/io/websocket/websocket_error_code.h"
#include "lib/io/websocket/websocketpp_exception_formatter.h"
#include "lib/logging/logging.h"
#include "lib/utils/bts_assert.h"
#include "lib/utils/formatters/exception_formatter.h"
#include <atomic>
@@ -201,9 +202,10 @@ struct WebsocketClient::PImpl {
}
}
} catch (const std::exception &ex) {
std::string str = ex.what();
ERROR_FN(
"connect_no_timeout. Failed to connect. {}; err: {}", snapshot(),
ex);
str);
set_state(State::IDLE);
// Don't wait for connect
return;
@@ -236,11 +238,16 @@ struct WebsocketClient::PImpl {
ERROR_FN(
"io_thread. Websocket io threw websocketpp::exception. {}; "
"err: {}",
snapshot(), ex);
snapshot(), std::string(ex.what()));
bts_assert_msgf(false, "io_thread. Websocket io threw websocketpp::exception. {}; "
"err: {}",
snapshot(), std::string(ex.what()));
} catch (const std::exception &ex) {
ERROR_FN(
"io_thread. Websocket io threw std::exception. {}; err: {}",
snapshot(), ex);
snapshot(), std::string(ex.what()));
bts_assert_msgf(false, "io_thread. Websocket io threw std::exception. {}; err: {}",
snapshot(), std::string(ex.what()));
} catch (...) {
bts_assert_msgf(
false,
@@ -300,6 +307,7 @@ struct WebsocketClient::PImpl {
const auto ec = client.get_con_from_hdl(hdl)->get_ec();
if (ec) {
ERROR_FN("on_open. {}; err: {}", snapshot(), ec.message());
bts_assert(false);
}
switch (state.value) {
case State::OPENING:
+157 -118
View File
@@ -68,74 +68,88 @@ struct CCSecSearchResult {
struct IncrementalBookData {
std::map<domain::SecurityID, domain::Book> book_map;
bool update_incremental(const IMarketDataListener::BookOp &op) {
//DONT NEED THIS?
// Validate inputs first
if (op.price <= 0.0 || op.quantity < 0) {
LOG(md, Error, "Invalid input - Price: {}, Quantity: {}", op.price, op.quantity);
return false;
}
bool update_incremental(const IMarketDataListener::BookOp &op) {
//DONT NEED THIS?
// Validate inputs first
if (op.price <= 0.0 || op.quantity < 0) {
LOG(md, Error, "Invalid input - Price: {}, Quantity: {}", op.price,
op.quantity);
return false;
}
// Logging setup
//DO WE NEED TO PUT THESE IN A STRING
std::string side_str = (op.side == domain::Side::Buy) ? "Buy" : "Sell";
std::string op_type_str;
switch (op.op_type) {
case IMarketDataListener::BookOpType::Add: op_type_str = "Add"; break;
case IMarketDataListener::BookOpType::MODIFY: op_type_str = "Modify"; break;
case IMarketDataListener::BookOpType::REMOVE: op_type_str = "Delete"; break;
default: op_type_str = "Unknown"; break;
}
//CAN WE DO THIS WITHOUT ASSIGNING TO STRINGS
std::stringstream op_details;
op_details << "Operation: Price: " << op.price << ", Quantity: " << op.quantity
<< ", Side: " << side_str << ", Type: " << op_type_str << "\n";
// Logging setup
//DO WE NEED TO PUT THESE IN A STRING
std::string side_str = (op.side == domain::Side::Buy) ? "Buy" : "Sell";
std::string op_type_str;
switch (op.op_type) {
case IMarketDataListener::BookOpType::Add:
op_type_str = "Add";
break;
case IMarketDataListener::BookOpType::MODIFY:
op_type_str = "Modify";
break;
case IMarketDataListener::BookOpType::REMOVE:
op_type_str = "Delete";
break;
default:
op_type_str = "Unknown";
break;
}
//CAN WE DO THIS WITHOUT ASSIGNING TO STRINGS
std::stringstream op_details;
op_details << "Operation: Price: " << op.price
<< ", Quantity: " << op.quantity << ", Side: " << side_str
<< ", Type: " << op_type_str << "\n";
LOG(md, Info, "Book Increment: {}", op_details.str());
LOG(md, Info, "Book Increment: {}", op_details.str());
// Get book and validate
auto book = get_book(op.id);
if (book == nullptr) {
LOG(md, Error, "Book not found for id: {}", op.id);
return false;
}
// Get book and validate
auto book = get_book(op.id);
if (book == nullptr) {
LOG(md, Error, "Book not found for id: {}", op.id);
return false;
}
// Get correct side of the book
auto& book_side = (op.side == domain::Side::Buy) ? book->bids() : book->asks();
// Get correct side of the book
auto &book_side =
(op.side == domain::Side::Buy) ? book->bids() : book->asks();
// Log initial state
std::stringstream book_before;
book_before << "Book before update (" << side_str << " side):\n";
for (const auto& level : book_side) {
book_before << "Price: " << level.price << ", Quantity: " << level.total_quantity << "\n";
}
// Log initial state
std::stringstream book_before;
book_before << "Book before update (" << side_str << " side):\n";
for (const auto &level : book_side) {
book_before << "Price: " << level.price
<< ", Quantity: " << level.total_quantity << "\n";
}
/*-----------*/
if(op.side == domain::Side::Buy){
switch(op.op_type){
case IMarketDataListener::BookOpType::Add:
{
/*-----------*/
if (op.side == domain::Side::Buy) {
switch (op.op_type) {
case IMarketDataListener::BookOpType::Add: {
int insert_position = 0;
for (size_t i = 0; i < book_side.size(); i++){//not sure how to access book
if(book_side[i].price != 0 && book_side[i].price > op.price ){
for (size_t i = 0; i < book_side.size();
i++) { //not sure how to access book
if (book_side[i].price != 0 && book_side[i].price > op.price) {
//book_side is the book we have ?
//op.price is what we received ?
insert_position++;
continue;
}else {//need j to be last level
for (size_t j =fmin(book_side.size()-1,5-1); j > i; j--){
book_side[j] = book_side[j-1];
} else { //need j to be last level
for (size_t j = fmin(book_side.size() - 1, 5 - 1); j > i;
j--) {
book_side[j] = book_side[j - 1];
}
break;
}
}
book_side[insert_position].price = op.price;
book_side[insert_position].total_quantity = op.quantity;//dont know how to add quantity
book_side[insert_position].total_quantity =
op.quantity; //dont know how to add quantity
break;
}
case IMarketDataListener::BookOpType::MODIFY:{
for(size_t i = 0; i < fmin(book_side.size(),5); i++){
if(book_side[i].price != 0 && book_side[i].price == op.price){
case IMarketDataListener::BookOpType::MODIFY: {
for (size_t i = 0; i < fmin(book_side.size(), 5); i++) {
if (book_side[i].price != 0 && book_side[i].price == op.price) {
//want to update quantity if price is a match
book_side[i].total_quantity = op.quantity;
break;
@@ -143,103 +157,113 @@ bool update_incremental(const IMarketDataListener::BookOp &op) {
}
break;
}
case IMarketDataListener::BookOpType::REMOVE:{
case IMarketDataListener::BookOpType::REMOVE: {
int found_position = -1;
for (size_t i = 0; i < fmin(book_side.size(),5); i++){
if(book_side[i].price == op.price){//trying to find based on price only
for (size_t i = 0; i < fmin(book_side.size(), 5); i++) {
if (book_side[i].price ==
op.price) { //trying to find based on price only
found_position = i;
break;
}
}
size_t level = found_position;
//would've had 4 here; want to be at last level
for(size_t i = found_position; i <fmin(book_side.size(),5); i++){
book_side[i] = book_side[i+1];
//would've had 4 here; want to be at last level
for (size_t i = found_position; i < fmin(book_side.size(), 5);
i++) {
book_side[i] = book_side[i + 1];
level++;
}
if(found_position != -1){
if (found_position != -1) {
//trying to set last level to 0
book_side[level].total_quantity =domain::Quantity(0);
book_side[level].total_quantity = domain::Quantity(0);
}
break;
}
default:
LOG(md,Error, "In the Default Case for IMarketDataListener::BookOp {}");
}
}else if (op.side == domain::Side::Sell){
switch(op.op_type){
case IMarketDataListener::BookOpType::Add:{
LOG(md, Error,
"In the Default Case for IMarketDataListener::BookOp {}");
}
} else if (op.side == domain::Side::Sell) {
switch (op.op_type) {
case IMarketDataListener::BookOpType::Add: {
int insert_position = 0;
for (int i = 0; i < fmin(book_side.size(),5); i++) {
if (book_side[i].price != 0 && book_side[i].price < op.price) {
insert_position++;
continue;
}
}//need i to be at the last level
for (int i = fmin(book_side.size()-1,5-1); i > insert_position; i--) {
book_side[i].price = book_side[i - 1].price;
book_side[i].total_quantity = book_side[i - 1].total_quantity;
for (int i = 0; i < fmin(book_side.size(), 5); i++) {
if (book_side[i].price != 0 && book_side[i].price < op.price) {
insert_position++;
continue;
}
} //need i to be at the last level
for (int i = fmin(book_side.size() - 1, 5 - 1); i > insert_position;
i--) {
book_side[i].price = book_side[i - 1].price;
book_side[i].total_quantity = book_side[i - 1].total_quantity;
}
book_side[insert_position].price = op.price;
book_side[insert_position].total_quantity = op.quantity;
book_side[insert_position].total_quantity = op.quantity;
break;
}
case IMarketDataListener::BookOpType::MODIFY:{
for (int i = 0; i < fmin(book_side.size(),5); i++) {
if (book_side[i].price != 0 && book_side[i].price == op.price) {
// For modify, we might want to update or keep the same value
book_side[i].price = op.price;
book_side[i].total_quantity = op.quantity;
break;
}
}break;
case IMarketDataListener::BookOpType::MODIFY: {
for (int i = 0; i < fmin(book_side.size(), 5); i++) {
if (book_side[i].price != 0 && book_side[i].price == op.price) {
// For modify, we might want to update or keep the same value
book_side[i].price = op.price;
book_side[i].total_quantity = op.quantity;
break;
}
}
break;
}
case IMarketDataListener::BookOpType::REMOVE: {
int found_position = -1;
for (size_t i = 0; i < fmin(book_side.size(),5); i++){
if(book_side[i].price == op.price){
for (size_t i = 0; i < fmin(book_side.size(), 5); i++) {
if (book_side[i].price == op.price) {
found_position = i;
break;
}
}
size_t level =found_position;
for (int i = found_position; i < fmin(book_side.size(),5); i++){
book_side[i].price = book_side[i+1].price;
book_side[i].total_quantity = book_side[i+1].total_quantity;
size_t level = found_position;
for (int i = found_position; i < fmin(book_side.size(), 5); i++) {
book_side[i].price = book_side[i + 1].price;
book_side[i].total_quantity = book_side[i + 1].total_quantity;
level++;
}
if(found_position != -1){
book_side[fmin(book_side.size()-1,level)].total_quantity = domain::Quantity(0);
if (found_position != -1) {
book_side[fmin(book_side.size() - 1, level)].total_quantity =
domain::Quantity(0);
}
break;
}
default:
LOG(md, Error, "In the Default Case for IMarketDataListener::BookOp {}");
LOG(md, Error,
"In the Default Case for IMarketDataListener::BookOp {}");
break;
}
}
/*---------------------*/
int counter = 0;
for (auto &level : book_side) {
if (counter > 4) {
level.total_quantity = domain::Quantity(0);
}
counter++;
}
/*---------------------*/
int counter =0;
for(auto &level:book_side){
if(counter >4){
level.total_quantity =domain::Quantity(0);
// Log final state
std::stringstream book_after;
book_after << "Book after update (" << side_str << " side):\n";
for (const auto &level : book_side) {
book_after << "Price: " << level.price
<< ", Quantity: " << level.total_quantity << "\n";
}
counter++;
std::string book_before_after_op =
book_before.str() + "\n" + book_after.str() + "\n" + op_details.str();
LOG(md, Diag, "Book Increment desc: {}", book_before_after_op);
bts_assert_msgf(
book->is_sorted(), "book not sorted {}", book_before_after_op);
return true;
}
// Log final state
std::stringstream book_after;
book_after << "Book after update (" << side_str << " side):\n";
for (const auto& level : book_side) {
book_after << "Price: " << level.price << ", Quantity: " << level.total_quantity << "\n";
}
std::string book_before_after_op = book_before.str() + "\n" + book_after.str() + "\n" + op_details.str();
LOG(md, Diag, "Book Increment desc: {}", book_before_after_op);
bts_assert_msgf(book->is_sorted(), "book not sorted {}", book_before_after_op);
return true;
}
domain::Book const *get_const_book(domain::SecurityID id) const {
auto it = book_map.find(id);
if (it == book_map.end()) {
@@ -289,8 +313,7 @@ struct GeneralMarketDataListener::PImpl {
GeneralSecurityIndex &index,
MDProducer &md_producer,
std::shared_ptr<bts::orderman::SimExecutionGateway> sim_ex_gwy)
: venue(venue), index(index),
md_producer(md_producer),
: venue(venue), index(index), md_producer(md_producer),
sim_ex_gwy(std::move(sim_ex_gwy)) {
if (this->sim_ex_gwy != nullptr) {
this->sim_ex_gwy->set_producers(&this->index);
@@ -452,7 +475,7 @@ void GeneralMarketDataListener::on_book(
void GeneralMarketDataListener::on_book_increment(
const IMarketDataListener::BookOp &book_op) {
if(!me->inc_book_data.update_incremental(book_op)){
if (!me->inc_book_data.update_incremental(book_op)) {
return;
}
domain::Book const *book_ptr = me->inc_book_data.get_const_book(book_op.id);
@@ -659,6 +682,18 @@ static bool get_qty_info(
qty_tick_unit = 1;
qty_tick_base = 1000000;
break;
case 10'000'000:
qty_tick_unit = 1;
qty_tick_base = 10'000'000;
break;
case 100'000'000:
qty_tick_unit = 1;
qty_tick_base = 100'000'000;
break;
case 1'000'000'000:
qty_tick_unit = 1;
qty_tick_base = 1'000'000'000;
break;
case 4:
qty_tick_unit = 25;
qty_tick_base = 100;
@@ -766,11 +801,15 @@ void GeneralMarketDataListener::_on_book(
_possibly_match_with_sim_orders(
bts::md::SecurityKey(book.security_id().value()));
LOG(md, Diag,
"GeneralMarketDataListener::_on_book post id: {} bid: {}/{}/{} ask: "
"GeneralMarketDataListener::_on_book sec: {} sec_id: {} handle_id: {} "
"bid: "
"{}/{}/{} ask: "
"{}/{}/{}",
handle->id, me->bids.at(0).price(), me->bids.at(0).direct_size(),
me->bids.at(0).size(), me->asks.at(0).price(),
me->asks.at(0).direct_size(), me->asks.at(0).size());
std::string(handle->handle.security().security_desc()),
book.security_id(), handle->id, me->bids.at(0).price(),
me->bids.at(0).direct_size(), me->bids.at(0).size(),
me->asks.at(0).price(), me->asks.at(0).direct_size(),
me->asks.at(0).size());
handle->handle.update_direct(time.value(), me->bids, me->asks);
}
+85
View File
@@ -509,6 +509,91 @@ void convert_security(
}
}
void to_json(nlohmann::json &j, const FixedSecurity &f) {
j = nlohmann::json{
{"key", f.key},
{"symbol", f.symbol()},
{"type", api::SecurityType_Name(f.type)},
{"venue", api::Venue_Name(f.exchange)},
{"alt_symbol", f.alt_symbol()},
{"cfi_code", f.cfi_code()},
{"security_desc", f.security_desc()},
{"platform_id", f.platform_id()},
{"display_factor", f.display_factor},
{"numeric_id", f.numeric_id},
{"cboe_class_key", f.cboe_class_key},
{"main_frac", (int)f.cme_main_fraction},
{"sub_frac", (int)f.cme_sub_fraction},
{"price_format", (int)f.cme_price_display_format},
{"market_segment", (int)f.cme_market_segment_id},
{"price_break", f.price_break},
{"low_tick", f.low_tick},
{"high_tick", f.high_tick},
{"cpp", f.cash_per_point},
{"ice_market_description", f.ice_market_description()},
{"ice_cleared_alias", f.ice_cleared_alias()},
{"ice_product_name", f.ice_product_name()},
{"ice_hub_alias", f.ice_hub_alias()},
{"ice_strip_name", f.ice_strip_name()},
{"mic_code", f.mic_code()},
};
// Qty Info
{
auto qty_infos_j = nlohmann::json::array();
float min_value, max_value, tick_unit, tick_base, max_price;
bool is_notional;
for (int8_t i = 0; f.get_quantity_info(
i, min_value, max_value, tick_unit, tick_base, max_price,
is_notional);
i++) {
auto qty_info_j = nlohmann::json::object();
qty_info_j["min_value"] = min_value;
qty_info_j["max_value"] = max_value;
qty_info_j["tick_unit"] = tick_unit;
qty_info_j["tick_base"] = tick_base;
qty_info_j["max_price"] = max_price;
qty_info_j["is_notional"] = is_notional;
qty_infos_j.push_back(std::move(qty_info_j));
}
j["qty_infos"] = std::move(qty_infos_j);
}
switch (f.type) {
case api::SECURITYTYPE_SPOT:
break;
case api::SECURITYTYPE_FUTURE: {
auto *fut = static_cast<const FixedFutureSecurity *>(&f);
core::TimeStamp exp;
core::TimeStamp::from_ymd(
fut->expiration_year, fut->expiration_month, fut->expiration_day,
&exp);
j["expiration"] = exp.to_iso_string();
break;
}
case api::SECURITYTYPE_OPTION: {
auto *opt = static_cast<const FixedOptionSecurity *>(&f);
core::TimeStamp exp;
core::TimeStamp::from_ymd(
opt->expiration_year, opt->expiration_month, opt->expiration_day,
&exp);
j["expiration"] = exp.to_iso_string();
j["strike"] = opt->strike;
j["callput"] = api::PutOrCall_Name(opt->call_put);
break;
}
case api::SECURITYTYPE_SPREAD: {
auto *spd = static_cast<const FixedSpreadSecurityXL *>(&f);
std::vector<FixedSpreadLeg> legs{spd->legs, spd->legs + spd->leg_count()};
j["legs"] = legs;
break;
}
default:
break;
}
}
} // namespace md
} // namespace bts
-65
View File
@@ -273,71 +273,6 @@ inline void to_json(nlohmann::json &j, const FixedSpreadLeg &f) {
};
}
inline void to_json(nlohmann::json &j, const FixedSecurity &f) {
j = nlohmann::json{
{"key", f.key},
{"symbol", f.symbol()},
{"type", api::SecurityType_Name(f.type)},
{"venue", api::Venue_Name(f.exchange)},
{"alt_symbol", f.alt_symbol()},
{"cfi_code", f.cfi_code()},
{"security_desc", f.security_desc()},
{"platform_id", f.platform_id()},
{"display_factor", f.display_factor},
{"numeric_id", f.numeric_id},
{"cboe_class_key", f.cboe_class_key},
{"main_frac", (int)f.cme_main_fraction},
{"sub_frac", (int)f.cme_sub_fraction},
{"price_format", (int)f.cme_price_display_format},
{"market_segment", (int)f.cme_market_segment_id},
{"price_break", f.price_break},
{"low_tick", f.low_tick},
{"high_tick", f.high_tick},
{"cpp", f.cash_per_point},
{"ice_market_description", f.ice_market_description()},
{"ice_cleared_alias", f.ice_cleared_alias()},
{"ice_product_name", f.ice_product_name()},
{"ice_hub_alias", f.ice_hub_alias()},
{"ice_strip_name", f.ice_strip_name()},
{"mic_code", f.mic_code()},
};
switch (f.type) {
case api::SECURITYTYPE_SPOT:
break;
case api::SECURITYTYPE_FUTURE: {
auto *fut = static_cast<const FixedFutureSecurity *>(&f);
core::TimeStamp exp;
core::TimeStamp::from_ymd(
fut->expiration_year, fut->expiration_month, fut->expiration_day,
&exp);
j["expiration"] = exp.to_iso_string();
break;
}
case api::SECURITYTYPE_OPTION: {
auto *opt = static_cast<const FixedOptionSecurity *>(&f);
core::TimeStamp exp;
core::TimeStamp::from_ymd(
opt->expiration_year, opt->expiration_month, opt->expiration_day,
&exp);
j["expiration"] = exp.to_iso_string();
j["strike"] = opt->strike;
j["callput"] = api::PutOrCall_Name(opt->call_put);
break;
}
case api::SECURITYTYPE_SPREAD: {
auto *spd = static_cast<const FixedSpreadSecurityXL *>(&f);
std::vector<FixedSpreadLeg> legs{spd->legs, spd->legs + spd->leg_count()};
j["legs"] = legs;
break;
}
default:
break;
}
}
inline std::ostream &operator<<(
std::ostream &out, const FixedSecurity &f) noexcept {
out << io::JsonDumper::dump(f);
+1 -1
View File
@@ -155,7 +155,7 @@ void OrderStore::store_and_publish_if_complete(
auto &rec_info = me->known_orders[k];
auto &rec = rec_info.rec;
if (rec.has_def() && rec.has_state()) {
LOG(OrderMan, Info,
LOG(OrderMan, Diag,
"OrderStore::store_and_publish_if_complete rec: {}",
utils::pb::to_string(rec));
publish(me->order_stream, rec);
@@ -271,13 +271,14 @@ void OrderManager::on_execution_report(
if (it == me->orders.end()) {
LOG(OrderMan, Error,
"Received an execution report for an unknown order with key "
"{}. Order state will be reconstructed from this report but "
"{}. Order state USED TO be reconstructed from this report but "
"some "
"information will be lost, including CustomerOrFirm,CTI "
"code "
"and "
"parent order id",
"parent order id but now these are IGNORED",
k.value());
continue;
auto order =
&me->services_data.order_factory.recreate_from_exe_report(
*report.data);
@@ -50,7 +50,7 @@ std::unique_ptr<IExecutionGateway> ExecutionGatewayFactory::create_gateway(
}
case bts::api::VENUE_OKX:
{
std::unique_ptr<IExecutionGenericAdapter> adapter = std::make_unique<OkxExecutionAdapter>(o);
std::unique_ptr<IExecutionGenericAdapter> adapter = std::make_unique<okxexe::OkxExecutionAdapter>(o);
return std::make_unique<GeneralExecutionGateway>(conn.venue(),std::move(adapter),o);
}
default:
@@ -22,7 +22,7 @@ class CancelReject;
using OnExecutionReport = std::function<void(ExecutionReport const &, const trace::CorID &)>;
// fix execution gateway for a single FIX session
// execution gateway for a single session
class IExecutionGenericAdapter {
public:
IExecutionGenericAdapter() = default;
+481 -42
View File
@@ -1,95 +1,534 @@
#include "OkxExecutionAdapter.h"
#include "OnixS/FIXEngine/FIX/Engine.h"
#include "lib/config/protos/marex_fix_cfg.pb.h"
#include "lib/api/services/OrderService.pb.h"
#include "lib/config/protos/okx_exe_cfg.pb.h"
#include "lib/domain_model/Security.pb.h"
#include "lib/domain_model/common.h"
#include "lib/io/http/http_client.h"
#include "lib/io/http/http_response.h"
#include "lib/io/websocket/websocket_client.h"
#include "lib/io/websocket/websocket_client_config.h"
#include "lib/logging/logging.h"
#include "lib/md/SecurityOracle.h"
#include "lib/orderman/CancelOrderMsg.h"
#include "lib/orderman/CancelReplaceOrderMsg.h"
#include "lib/orderman/CreateOrderMsg.h"
#include "lib/orderman/gateways/OnixFIXListener.h"
#include "lib/orderman/gateways/TrafixOrderStateHandler.h"
#include "lib/orderman/gateways/IExecutionGateway.h"
#include "lib/orderman/orderman_utils/ExecutionReport.h"
#include "lib/orderman/orderman_utils/OkxUtils.h"
#include "lib/orderman/orderman_utils/orderman_common.h"
#include "lib/utils/Uuid.h"
#include "lib/utils/bts_assert.h"
#include "lib/utils/helper_macros.h"
#include <OnixS/FIXEngine/FIX/Exception.h>
#include <OnixS/FIXEngine/FIX/FIX42.h>
#include <OnixS/FIXEngine/FIX/Message.h>
#include <OnixS/FIXEngine/FIX/Numeric.h>
#include <OnixS/FIXEngine/FIX/ProtocolVersion.h>
#include <atomic>
#include <boost/pool/object_pool.hpp>
#include <cassert>
#include <chrono>
#include <cstddef>
#include <ctime>
#include <exception>
#include <fmt/core.h>
#include <map>
#include <memory>
#include <openssl/buffer.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <quickfix/FixFields.h>
#include <ratio>
#include <quickfix/Utility.h>
#include <string>
#include <unistd.h>
#include <unordered_map>
// Example namespace alias for nlohmann::json
using json = nlohmann::json;
namespace bts::orderman {
namespace bts::orderman::okxexe {
OkxExecutionAdapter::OkxExecutionAdapter(md::SecurityOracle){}//listener() {}
OkxExecutionAdapter::OkxExecutionAdapter(md::SecurityOracle o)
: me_(std::make_unique<PImpl>(o)) {
LOG(OrderMan, Info, "OkxExecutionAdapter::constructor ");
} //listener() {}
OkxExecutionAdapter::~OkxExecutionAdapter() {}
inline void handleOkException(
const std::string &context, const std::string &error) {
bts_assert_msgf(false, "{} Exception: {}", context, error);
}
void OkxExecutionAdapter::start() {
int attempt = 0;
while (true) {
try {
LOG(OrderMan, Warn, "OkxExecutionAdapter::start Attempting login (Attempt: {})", attempt + 1);
LOG(OrderMan, Warn, "OkxExecutionAdapter::start Login successful");
break; // Exit loop on success
} catch (const std::exception &ex) {
const std::string err = ex.what();
LOG(OrderMan, Error, "OkxExecutionAdapter::start Login failed: {}. Retrying in {} seconds.", err, 10);
enum class OkxExecutionState { OFF, STARTED, LOGGED_IN, SUBSCIRBED };
struct OkxExecutionAdapter::PImpl {
const std::string api_key = config::get<config::OkxExeCfg>().api_key();
const std::string secret_key = config::get<config::OkxExeCfg>().secret_key();
const std::string passphrase = config::get<config::OkxExeCfg>().passphrase();
const std::string url =
config::get<config::OkxExeCfg>().ws_url() + "/ws/v5/private";
const core::TimeSpan connect_timeout = core::TimeSpan::from_seconds(0);
const core::TimeSpan pong_timeout = core::TimeSpan::from_seconds(
config::get<config::OkxExeCfg>().ws_ping_interval_seconds());
websocket::WebsocketClient ws_client;
boost::object_pool<ExecutionReport> exe_report_pool;
OnExeReport cb;
md::SecurityOracle o;
std::atomic<OkxExecutionState> state = OkxExecutionState::OFF;
// if (maxRetries > 0 && ++attempt >= maxRetries) {
// LOG(OrderMan, Error, "OkxExecutionAdapter::start Exceeded max retries ({}). Aborting.", maxRetries);
// handleOkException("OkxExecutionAdapter::start", err);
// break; // Exit if retries are exhausted
// }
// std::this_thread::sleep_for(std::chrono::seconds(std::max(60,retryInterval)));
PImpl(md::SecurityOracle o)
: ws_client(
websocket::WebsocketClient({url, connect_timeout, pong_timeout})),
cb(dummy), o(o) {
ws_client.message_handler().bind<&PImpl::on_message>(*this);
ws_client.close_handler().bind<&PImpl::on_close>(*this);
};
static void dummy(const ExecutionReport &, const trace::CorID &) {
LOG(OrderMan, Error, "OnixFixListener::Dummy fired");
}
void setCallback(const OnExeReport &callback) {
LOG(OrderMan, Info,
"OkxExecutionAdapter::setCallback callback registered");
cb = callback;
}
std::optional<std::thread> ws_run_thread;
std::optional<std::thread> ws_ping_thread;
void get_exe_report(
const nlohmann::basic_json<> &order, ExecutionReport &report) {
report.exchange = api::VENUE_OKX;
std::string account_name = config::get<config::OkxExeCfg>().account();
std::string ops_id = config::get<config::OkxExeCfg>().ops_id();
std::array<char, 12> account_array = {};
std::copy_n(
account_name.begin(),
std::min(account_name.size(), account_array.size()),
account_array.begin());
report.account_name = AccountName(account_array);
to_array(ops_id, &report.operator_id.value());
report.order_key =
OrderKey(std::stoi(order.at("clOrdId").get<std::string>()));
auto insttype =
OkxUtils::getInstType(order.at("instType").get<std::string>());
auto venue = api::VENUE_OKX;
auto inst_id = order.at("instId").get<std::string>();
// report.msg_seq_num = /* Generate or map message sequence */;
bts_assert(o.get_securities(venue, insttype, inst_id).size() == 1);
report.security = o.get_securities(venue, insttype, inst_id)[0];
report.side = (order.at("side").get<std::string>() == "buy")
? api::Side::SIDE_BUY
: api::Side::SIDE_SELL;
report.order_type =
OkxUtils::getOrdType(order.at("ordType").get<std::string>());
OkxUtils::populateExecTypeAndOrderStatus(
report, order.at("state").get<std::string>());
report.exchange_order_id = ExchangeOrderID(
OkxUtils::stringToFixedArray(order.at("ordId").get<std::string>()));
report.price = Price(std::stod(order.at("px").get<std::string>()));
report.quantity =
Quantity::create_new(std::stod(order.at("sz").get<std::string>()));
report.filled_quantity = Quantity::create_new(
std::stod(order.at("accFillSz").get<std::string>()));
report.last_price =
Price(std::stod(order.at("lastPx").get<std::string>()));
report.avg_price = Price(std::stod(order.at("avgPx").get<std::string>()));
report.last_shares = Quantity::create_new(
std::stod(order.at("fillSz").get<std::string>()));
//there are algos, like spreader
// that are super sensitive to messages and cancel themselves. To avoid
// this we only pass on the error messages when the order has terminated.
// The assumption being that if the order has not terminated it is OK to
// ignore the message.
// Alternatively we can change the spreader to not do that. Right not this
// seems to be the better choice
if (!(order.at("code").get<std::string>() == "0")) {
report.msg = order.at("msg").get<std::string>();
}
//report can be new order, cancel order, order filled, replace order, and partial fill or errror
//new order, cancelled and order filled dont need special handling, error gets handled above
//for other three we will look for fields that are only specific to the respectives types
// if (!(order.at("cancelSource").get<std::string>() == "")) {
// }
if (!(order.at("amendSource").get<std::string>() == "")) {
if((order.at("amendResult").get<std::string>() == "0"))
report.exec_type = api::orderman::ExecType::EXEC_TYPE_REPLACED;
else{
LOG(OrderMan, Warn, "order:{} replace rejected", report.order_key);
report.msg = order.at("msg").get<std::string>();
}
}
}
} else if (!(order.at("fillSz").get<std::string>() == "0")) {
utils::Uuid guid;
std::string fill_id = guid.to_text_string();
to_array(fill_id, &report.exchange_fill_id.value());
}
// report.transact_time = ExchangeTimeStampVar(std::stoll(order.at("uTime").get<std::string>()));
ExchangeTimeStamp64 now = ExchangeTimeStamp64(datetime::unix_nano_now());
report.transact_time = now;
std::string er = report.to_string();
LOG(OrderMan, Info, "Created ExecutionReport: {}", er);
}
void on_close(const std::error_code &ec) {
bts_assert_msgf(
false, "Websocket stream client disconnected. {}", ec.message());
}
void on_message(const std::string &msg_str) {
LOG(OrderMan, Info, "okxlistener::on_message. {}", msg_str);
nlohmann::json msg;
try {
msg = nlohmann::json::parse(msg_str);
} catch (const std::exception &ex) {
std::string s = ex.what();
LOG(OrderMan, Error, "Failed to parse json. {}\n{}", s, msg_str);
return;
}
const auto event_type_it = msg.find("event");
if (event_type_it != msg.end()) {
const auto event_type = event_type_it->get<std::string>();
if (event_type == "login") {
bts_assert(state == OkxExecutionState::STARTED);
const auto code_it = msg.find("code");
const auto code = code_it->get<std::string>();
if (code == "0") {
LOG(OrderMan, Info, "Succefully logged in {}", msg_str);
state = OkxExecutionState::LOGGED_IN;
}
return;
}
if (event_type == "subscribe") {
bts_assert(state == OkxExecutionState::LOGGED_IN);
state = OkxExecutionState::SUBSCIRBED;
return;
}
if (event_type == "channel-conn-count") { //ignore for now
return;
}
}
// Handle order updates
const auto channel_it = msg.find("arg");
if (channel_it == msg.end() ||
channel_it->at("channel").get<std::string>() != "orders") {
LOG(OrderMan, Error, "Unexpected response id:\n{}", msg_str);
return;
}
const auto &data = msg.at("data");
for (const nlohmann::basic_json<> &order : data) {
ExecutionReport *er_p = exe_report_pool.construct();
ExecutionReport &report = *er_p;
get_exe_report(order, report);
cb(report, {});
}
}
void start() {
size_t attempt = 0;
state = OkxExecutionState::STARTED;
while (state != OkxExecutionState::OFF) {
try {
LOG(OrderMan, Warn,
"OkxExecutionAdapter::start Attempting login (Attempt: {})",
attempt + 1);
ws_client.connect({}, true);
sleep(2);
if (ws_ping_thread.has_value()) {
if (ws_ping_thread->joinable()) {
ws_ping_thread
->join(); // Ensure the previous thread exits cleanly
}
ws_ping_thread
.reset(); // Reset the optional before creating a new thread
}
ws_ping_thread.emplace([&]() {
LOG(OrderMan, Warn,
"OkxExecutionAdapter::ws_ping_thread thread starting. url: "
"{}",
url);
auto last_ping_time = core::TimeStamp::now();
auto ping_interval = core::TimeSpan::from_seconds(
config::get<config::OkxExeCfg>().ws_ping_interval_seconds());
std::this_thread::sleep_for(
std::chrono::milliseconds(ping_interval.millis()));
while (ws_client.is_connected()) {
const auto now = bts::core::TimeStamp::now();
if ((now - last_ping_time) > ping_interval) {
LOG(OrderMan, Warn,
"OkxExecutionAdapter::ws_ping_thread Pinging "
"websocket. url: {}",
url);
ws_client.ping();
last_ping_time = now;
}
std::this_thread::sleep_for(
std::chrono::seconds(ping_interval.seconds() / 10));
}
LOG(OrderMan, Warn,
"OkxExecutionAdapter::ws_ping_thread thread exiting. url: "
"{}",
url);
});
auto [timestamp, sign] = OkxUtils::getLoginSignTime(secret_key);
static constexpr const char *AUTH_REQ_TEMPLATE =
R"({{"op": "login", "args": [{{ "apiKey": "{}", "passphrase": "{}", "timestamp": "{}", "sign": "{}" }}] }})";
const std::string auth_req = fmt::format(
AUTH_REQ_TEMPLATE, api_key, passphrase, timestamp, sign);
LOG(OrderMan, Warn,
"OkxExecutionAdapter::start Attempting login (req: {})",
auth_req);
const auto auth_res = ws_client.send_text(auth_req);
LOG(OrderMan, Warn,
"OkxExecutionAdapter::start Attempting login {}, {}, {}",
std::to_string(auth_res.value()),
std::string(auth_res.category().name()), auth_res.message());
if (auth_res.value()) {
LOG(md, Info, "okx authentication failed. {}",
auth_res.message());
attempt++;
throw std::runtime_error(
"okx authentication failed: " + auth_res.message());
}
while (state == OkxExecutionState::STARTED) {
LOG(OrderMan, Warn,
"OkxExecutionAdapter::start waiting to login");
std::this_thread::sleep_for(1s);
}
LOG(OrderMan, Warn,
"OkxExecutionAdapter::start Login successful {}",
auth_res.message());
std::string subscribe_req = R"({
"op": "subscribe",
"args": [{"channel": "orders", "instType": "ANY"}]
})";
LOG(OrderMan, Warn,
"OkxExecutionAdapter::start AGttempting sub (req: {})",
subscribe_req);
const auto auth_res2 = ws_client.send_text(subscribe_req);
LOG(OrderMan, Warn, "OkxExecutionAdapter::subscribing to orders{}",
auth_res2.message());
while (state != OkxExecutionState::SUBSCIRBED) {
LOG(OrderMan, Warn,
"OkxExecutionAdapter::start waiting to subscribe");
std::this_thread::sleep_for(1s);
}
LOG(OrderMan, Warn,
"OkxExecutionAdapter::subscribing to orders successful{}",
auth_res2.message());
break; // Exit loop on success
} catch (const std::exception &ex) {
const std::string err = ex.what();
LOG(OrderMan, Error,
"OkxExecutionAdapter::start Login failed: {}. Retrying in {} "
"seconds.",
err,
config::get<config::OkxExeCfg>()
.ws_reconnect_interval_seconds());
size_t maxRetries =
config::get<config::OkxExeCfg>().ws_reconnect_max_count();
if (maxRetries > 0 && ++attempt >= maxRetries) {
LOG(OrderMan, Error,
"OkxExecutionAdapter::start Exceeded max retries ({}). "
"Aborting.",
maxRetries);
OkxUtils::handleOkException("OkxExecutionAdapter::start", err);
break; // Exit if retries are exhausted
}
std::this_thread::sleep_for(
std::chrono::seconds(config::get<config::OkxExeCfg>()
.ws_reconnect_interval_seconds()));
}
}
}
};
void OkxExecutionAdapter::start() { me_->start(); }
void OkxExecutionAdapter::modify_order(
const CancelReplaceOrderMsg &msg) { //confirm will have everything
try {
LOG(OrderMan, Warn, "OkxExecutionAdapter::modify_order modifying order{}",
msg);
http::HttpResponse res;
// static constexpr const char *body_template = R"({"instId": "BTC-USDT", "tdMode": "cash", "side": "buy", "ordType": "limit", "px": "1000", "sz": "0.01"})";
static constexpr const char *body_template =
R"({{"instId": "{}", "clordId": "{}", "newPx": "{}", "newSz": "{}"}})";
const std::string REQUEST_PATH = "/api/v5/trade/amend-order";
const std::string body = fmt::format(
body_template, msg.create_fields.security->okx().security_desc(),
msg.create_fields.key, msg.create_fields.price,
msg.create_fields.quantity.full_value());
auto headers = OkxUtils::createHeaders(
me_->api_key, me_->passphrase, me_->secret_key, "POST", REQUEST_PATH,
body);
// Generate and log the curl command
std::string curlCommand = OkxUtils::generateCurlCommand(
config::get<config::OkxExeCfg>().rest_url() + REQUEST_PATH, headers,
body);
LOG(OrderMan, Warn, "Curl Command: \n{}", curlCommand);
http::HttpClient::post(
config::get<config::OkxExeCfg>().rest_url() + REQUEST_PATH, body,
headers, &res);
const auto &[suc, top_msg] = OkxUtils::orderAccepted(res.body);
// ExecutionReport *er_p = me_->exe_report_pool.construct();
// ExecutionReport &er = *er_p;
// OkxUtils::populateCreateOrderFields(er, msg.create_fields);
// er.price = msg.create_fields.price;
// er.quantity = msg.create_fields.quantity;
// er.order_key = msg.create_fields.key;
// const auto &[id, ts, ex_msg] = OkxUtils::extractOrderInfo(res.body);
if (suc) {
LOG(md, Info, "Http get succeeded {}", res.body);
// er.exec_type = api::orderman::ExecType::EXEC_TYPE_PENDING_REPLACE;
// er.order_status = api::orderman::NATIVE_ORDER_STATUS_PENDING_REPLACE;
} else {
LOG(md, Info, "Http get failed {},,,{}", res.errors, res.body);
// er.exec_type = api::orderman::ExecType::EXEC_TYPE_REJECTED;
// er.order_status = api::orderman::NATIVE_ORDER_STATUS_REJECTED;
// er.msg = ex_msg + " " + top_msg;
}
// me_->cb(er, {});
} catch (const std::exception &ex) {
const std::string err = ex.what();
handleOkException("Modify order", err);
const std::string err = ex.what();
OkxUtils::handleOkException("Amend order", err);
}
}
void OkxExecutionAdapter::launch_new_order(const CreateOrderMsg &msg) {
LOG(OrderMan, Warn, "launch order {}", msg);
try {
LOG(OrderMan, Warn, "OkxExecutionAdapter::launch_new_order creating order");
LOG(OrderMan, Warn,
"OkxExecutionAdapter::launch_new_order creating order");
static constexpr const char *body_template =
R"({{"instId": "{}", "tdMode": "cash", "side": "{}", "ordType": "{}", "px": "{}", "sz": "{}", "clordId": "{}"}})";
const std::string REQUEST_PATH = "/api/v5/trade/order";
const std::string side = msg.side == api::Side::SIDE_BUY ? "buy" : "sell";
const std::string quantity = std::to_string(msg.quantity.full_value());
const std::string price = std::to_string(msg.price.value());
std::string suffix = "ORDER_TYPE_";
auto ord_full_name = NativeOrderType_Name(msg.order_type);
auto pos = ord_full_name.find(suffix);
const std::string ord_type =
FIX::string_toLower(ord_full_name.substr(pos + suffix.size()));
const std::string body = fmt::format(
body_template, msg.security->okx().security_desc(), side, ord_type,
price, quantity, msg.key);
http::HttpResponse res;
auto headers = OkxUtils::createHeaders(
me_->api_key, me_->passphrase, me_->secret_key, "POST", REQUEST_PATH,
body);
// Generate and log the curl command
std::string curlCommand = OkxUtils::generateCurlCommand(
config::get<config::OkxExeCfg>().rest_url() + REQUEST_PATH, headers,
body);
LOG(OrderMan, Warn, "Curl Command: \n{}", curlCommand);
http::HttpClient::post(
config::get<config::OkxExeCfg>().rest_url() + REQUEST_PATH, body,
headers, &res);
ExecutionReport *er_p = me_->exe_report_pool.construct();
ExecutionReport &er = *er_p;
const auto &[suc, top_msg] = OkxUtils::orderAccepted(res.body);
OkxUtils::populateCreateOrderFields(er, msg);
er.price = msg.price;
er.quantity = msg.quantity;
er.filled_quantity = 0;
er.last_shares = 0;
er.avg_price = Price(0);
ExchangeTimeStamp64 now = ExchangeTimeStamp64(datetime::unix_nano_now());
er.transact_time = now;
er.order_key = msg.key;
const auto &[id, ts, ex_msg] = OkxUtils::extractOrderInfo(res.body);
er.exchange_order_id = ExchangeOrderID{OkxUtils::stringToFixedArray(id)};
if (suc) {
LOG(md, Info, "Http get succeeded {}", res.body);
er.exec_type = api::orderman::ExecType::EXEC_TYPE_PENDING_NEW;
er.order_status = api::orderman::NATIVE_ORDER_STATUS_PENDING_NEW;
} else {
LOG(md, Info, "Http get failed {},,,{}", res.errors, res.body);
er.exec_type = api::orderman::ExecType::EXEC_TYPE_REJECTED;
er.order_status = api::orderman::NATIVE_ORDER_STATUS_REJECTED;
er.msg = ex_msg + " " + top_msg;
}
me_->cb(er, {});
}
// Shut down all reactors if they exist.
catch (const std::exception &ex) {
const std::string err = ex.what();
handleOkException("Launch order", err);
const std::string err = ex.what();
OkxUtils::handleOkException("Launch order", err);
}
}
void OkxExecutionAdapter::register_callback([[maybe_unused]]const OnExecutionReport &cb) {
LOG(OrderMan, Warn, "OkxExecutionAdapter::register_callback callback registered");
// listener.setCallback(cb);
void OkxExecutionAdapter::register_callback(
[[maybe_unused]] const OnExecutionReport &cb) {
LOG(OrderMan, Warn,
"OkxExecutionAdapter::register_callback callback registered");
me_->setCallback(cb);
}
void OkxExecutionAdapter::cancel_order(const CancelOrderMsg &msg) {
LOG(OrderMan, Warn, "cancel order {}", msg);
try {
LOG(OrderMan, Warn, "order cancel sent");
http::HttpResponse res;
// static constexpr const char *body_template = R"({"instId": "BTC-USDT", "tdMode": "cash", "side": "buy", "ordType": "limit", "px": "1000", "sz": "0.01"})";
static constexpr const char *body_template =
R"({{"instId": "{}", "clordId": "{}"}})";
const std::string REQUEST_PATH = "/api/v5/trade/cancel-order";
const std::string body = fmt::format(
body_template, msg.security->okx().security_desc(), msg.key);
auto headers = OkxUtils::createHeaders(
me_->api_key, me_->passphrase, me_->secret_key, "POST", REQUEST_PATH,
body);
// Generate and log the curl command
std::string curlCommand = OkxUtils::generateCurlCommand(
config::get<config::OkxExeCfg>().rest_url() + REQUEST_PATH, headers,
body);
LOG(OrderMan, Warn, "Curl Command: \n{}", curlCommand);
http::HttpClient::post(
config::get<config::OkxExeCfg>().rest_url() + REQUEST_PATH, body,
headers, &res);
const auto &[suc, top_msg] = OkxUtils::orderAccepted(res.body);
// ExecutionReport *er_p = me_->exe_report_pool.construct();
// ExecutionReport &er = *er_p;
// er.security = msg.security;
// er.side = msg.side;
// er.order_key = msg.key;
// to_array(msg.routing_record->account(), &er.account_name.value());
// to_array(msg.routing_record->operator_id(), &er.operator_id.value());
// er.exchange = msg.security->exchange();
// er.order_key = msg.key;
// const auto &[id, ts, ex_msg] = OkxUtils::extractOrderInfo(res.body);
if (suc) {
LOG(md, Info, "Http get succeeded {}", res.body);
// er.exec_type = api::orderman::ExecType::EXEC_TYPE_PENDING_CANCEL;
// er.order_status = api::orderman::NATIVE_ORDER_STATUS_PENDING_CANCEL;
} else {
LOG(md, Info, "Http get failed {},,,{}", res.errors, res.body);
// er.exec_type = api::orderman::ExecType::EXEC_TYPE_REJECTED;
// er.order_status = api::orderman::NATIVE_ORDER_STATUS_REJECTED;
// er.msg = ex_msg + " " + top_msg;
}
// me_->cb(er, {});
}
// Shut down all reactors if they exist.
catch (const std::exception &ex) {
const std::string err = ex.what();
handleOkException("Cacnel order", err);
const std::string err = ex.what();
OkxUtils::handleOkException("Cacnel order", err);
}
}
} // namespace bts::orderman
} // namespace bts::orderman::okxexe
@@ -2,7 +2,7 @@
#include "IExecutionGenericAdapter.h"
#include "lib/md/SecurityOracle.h"
namespace bts::orderman {
namespace bts::orderman::okxexe {
class OkxExecutionAdapter : public IExecutionGenericAdapter {
public:
@@ -14,6 +14,8 @@ class OkxExecutionAdapter : public IExecutionGenericAdapter {
void modify_order(const CancelReplaceOrderMsg &) override;
void start() override;
private:
struct PImpl;
std::unique_ptr<PImpl> me_;
};
} // namespace bts::orderman
+13 -3
View File
@@ -271,7 +271,10 @@ void OnixFIXAdapter::modify_order(
populate_common_order_fields(order, msg.create_fields);
order.validate();
LOG(OrderMan, Warn, "order Validated");
order.set(OnixFIXAdapter::FIX_TAG_SMART_ROUTING, "SMART");
order.set(OnixFIXAdapter::FIX_TAG_SMART_ROUTING, "SMART").set(
OnixS::FIX::FIX42::Tags::SendingTime,
OnixS::FIX::Timestamp::utc(),
OnixS::FIX::TimestampFormat::YYYYMMDDHHMMSSMsec);
std::string order_string = order.toString('|');
state_handler->modifyOrder(msg, new_id);
session->send(&order);
@@ -310,7 +313,11 @@ void OnixFIXAdapter::launch_new_order(const CreateOrderMsg &msg) {
std::string order_json = order.toJson();
order.validate();
LOG(OrderMan, Warn, "order Validated");
order.set(OnixFIXAdapter::FIX_TAG_SMART_ROUTING, "SMART");
order.set(OnixFIXAdapter::FIX_TAG_SMART_ROUTING, "SMART").set(
OnixS::FIX::FIX42::Tags::SendingTime,
OnixS::FIX::Timestamp::utc(),
OnixS::FIX::TimestampFormat::YYYYMMDDHHMMSSMsec);
state_handler->launchOrder(msg, new_id);
// listener.fire_dummy_report(msg);
session->send(&order);
@@ -362,7 +369,10 @@ void OnixFIXAdapter::cancel_order(const CancelOrderMsg &msg) {
LOG(OrderMan, Warn, "order_cancel Created: {}", order_str);
// order_cancel.validate(); fails becasuse of HandlInst
LOG(OrderMan, Warn, "order Validated");
order_cancel.set(OnixFIXAdapter::FIX_TAG_SMART_ROUTING, "SMART");
order_cancel.set(OnixFIXAdapter::FIX_TAG_SMART_ROUTING, "SMART").set(
OnixS::FIX::FIX42::Tags::SendingTime,
OnixS::FIX::Timestamp::utc(),
OnixS::FIX::TimestampFormat::YYYYMMDDHHMMSSMsec);
state_handler->cancelOrder(msg, new_id);
session->send(&order_cancel);
LOG(OrderMan, Warn, "order cancel sent");
+268
View File
@@ -0,0 +1,268 @@
#include "lib/api/services/OrderService.pb.h"
#include "lib/domain_model/Security.pb.h"
#include "lib/logging/logging.h"
#include "lib/md/SecurityOracle.h"
#include "lib/orderman/CreateOrderMsg.h"
#include "lib/orderman/gateways/IExecutionGateway.h"
#include "lib/orderman/orderman_utils/ExecutionReport.h"
#include "lib/utils/bts_assert.h"
#include <OnixS/FIXEngine/FIX/Exception.h>
#include <OnixS/FIXEngine/FIX/FIX42.h>
#include <OnixS/FIXEngine/FIX/Message.h>
#include <OnixS/FIXEngine/FIX/Numeric.h>
#include <OnixS/FIXEngine/FIX/ProtocolVersion.h>
#include <boost/pool/object_pool.hpp>
#include <cassert>
#include <cstddef>
#include <ctime>
#include <exception>
#include <fmt/core.h>
#include <map>
#include <nlohmann/json_fwd.hpp>
#include <openssl/buffer.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <quickfix/FixFields.h>
#include <quickfix/Utility.h>
#include <regex>
#include <sstream>
#include <string>
#include <tuple>
#include <unistd.h>
#include <unordered_map>
namespace bts::orderman::okxexe {
using json = nlohmann::json;
class OkxUtils {
public:
// Function template to get a value from a JSON object by key
template <typename T>
static inline T getJsonValue(
const json &jsonObject,
const std::string &key,
const T &defaultValue = T()) {
if (jsonObject.contains(key) && jsonObject[key].is_primitive()) {
return jsonObject.value(key, defaultValue);
}
return defaultValue;
}
// Function to parse JSON safely
static inline std::optional<json> parseJsonSafely(
const std::string &jsonString) {
try {
return json::parse(jsonString);
} catch (const std::exception &e) {
std::string s = e.what();
LOG(OrderMan, bts::Error, "Failed to parse JSON: {}", s);
return std::nullopt;
}
}
struct OrderResponse {
bool accepted;
std::string message;
};
static inline OrderResponse orderAccepted(const std::string &jsonString) {
auto body = parseJsonSafely(jsonString);
if (!body)
return {false, "NO Body"};
auto &jsonResponse = *body;
return {
jsonResponse.value("code", "1") == "0",
jsonResponse.value("msg", "NO code or message")};
}
// Function to extract order information from JSON
static inline std::tuple<std::string, std::string, std::string> extractOrderInfo(
const std::string &jsonString) {
auto body = parseJsonSafely(jsonString);
if (!body.has_value()) {
return {"", "", "json parsing error no body"};
}
auto jsonResponse = body.value();
if (jsonResponse.contains("data") && jsonResponse["data"].is_array()) {
const auto &dataArray = jsonResponse["data"];
if (!dataArray.empty() && dataArray[0].is_object()) {
const auto &firstItem = dataArray[0];
// Extract values using the templated function
std::string ordId =
OkxUtils::getJsonValue(firstItem, "ordId", std::string());
std::string timestamp =
OkxUtils::getJsonValue(firstItem, "ts", std::string());
std::string sMsg =
OkxUtils::getJsonValue(firstItem, "sMsg", std::string());
return {ordId, timestamp, sMsg};
}
}
return {"", "", "json parsing error"};
}
// Function to encode a byte array to Base64
static std::string base64Encode(const unsigned char *buffer, size_t length) {
BIO *bmem = BIO_new(BIO_s_mem());
BIO *b64 = BIO_new(BIO_f_base64());
BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); // No newlines
BIO_push(b64, bmem);
BIO_write(b64, buffer, static_cast<int>(length));
BIO_flush(b64);
BUF_MEM *bptr;
BIO_get_mem_ptr(b64, &bptr);
std::string result(bptr->data, bptr->length);
BIO_free_all(b64);
return result;
}
static inline std::vector<unsigned char> generateSignature(
const std::string &prehash, const std::string &secretKey) {
unsigned int len = EVP_MAX_MD_SIZE;
std::vector<unsigned char> result(len);
HMAC(
EVP_sha256(), secretKey.c_str(), static_cast<int>(secretKey.length()),
reinterpret_cast<const unsigned char *>(prehash.c_str()),
prehash.length(), result.data(), &len);
result.resize(len);
return result;
}
static inline std::string getSign(
std::string message, const std::string &secret_key = "your_secret_key") {
auto signature = generateSignature(message, secret_key);
std::string sign = base64Encode(signature.data(), signature.size());
return sign;
}
static std::tuple<long, std::string> getLoginSignTime(
const std::string &secret_key = "your_secret_key",
const std::string &method = "GET",
const std::string &path = "/users/self/verify") {
auto seconds = bts::core::TimeStamp::now().seconds();
std::string message = std::to_string(seconds) + method + path;
return {seconds, getSign(message, secret_key)};
}
static std::string getUTCTimestamp(bts::core::TimeStamp t) {
auto rawTimestamp =
t.to_iso_string() +
"Z"; //gives 20241213T175033.840123Z should be 2024-12-13T17:50:33.840Z
// Use regex to parse and reformat
std::regex re(
R"((\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})\.(\d{3})\d*Z)");
std::string formattedTimestamp =
std::regex_replace(rawTimestamp, re, "$1-$2-$3T$4:$5:$6.$7Z");
return formattedTimestamp;
}
static std::array<char, 17> stringToFixedArray(const std::string &s) {
std::array<char, 17> idAsArray{};
std::copy_n(
s.begin(), std::min(s.size(), idAsArray.size() - 1),
idAsArray.begin());
return idAsArray;
}
static std::unordered_map<std::string, std::string> createHeaders(
const std::string &apiKey,
const std::string &passphrase,
const std::string &secretKey,
const std::string &method,
const std::string &requestPath,
const std::string &body = "") {
auto ts = bts::core::TimeStamp::now();
std::string timestamp = getUTCTimestamp(ts);
std::string prehash = timestamp + method + requestPath + body;
auto signature = getSign(prehash, secretKey);
LOG(OrderMan, bts::Info, "prehash '{}' , key '{}'", prehash, signature);
// Prepare headers
std::unordered_map<std::string, std::string> headers = {
{"accept", "application/json"},
{"Content-Type", "application/json"},
{"x-simulated-trading", "1"},
{"OK-ACCESS-KEY", apiKey},
{"OK-ACCESS-SIGN", signature},
{"OK-ACCESS-TIMESTAMP", timestamp},
{"OK-ACCESS-PASSPHRASE", passphrase}};
return headers;
}
static std::string generateCurlCommand(
const std::string &url,
const std::unordered_map<std::string, std::string> &headers,
const std::string &body) {
std::ostringstream oss;
// Start with the curl command
std::string curlCommand = "curl -X POST " + url + " \\\n";
// Add headers to the curl command
for (const auto &header : headers) {
curlCommand +=
" -H \"" + header.first + ": " + header.second + "\" \\\n";
}
// Add bodyl
curlCommand += " -d '" + body + "'";
return curlCommand;
}
static inline api::SecurityType getInstType(const std::string &type) {
static const std::unordered_map<std::string, api::SecurityType> type_map =
{{"SPOT", api::SECURITYTYPE_SPOT},
{"OPTION", api::SECURITYTYPE_OPTION},
{"FUTURES", api::SECURITYTYPE_FUTURE},
{"SWAP", api::SECURITYTYPE_FUTURE}};
auto it = type_map.find(type);
return it != type_map.end() ? it->second : api::SECURITYTYPE_UNSET;
}
static inline api::orderman::NativeOrderType getOrdType(const std::string &type) {
static const std::unordered_map<std::string, api::orderman::NativeOrderType> type_map =
{{"limit", api::orderman::NATIVE_ORDER_TYPE_LIMIT},
{"market", api::orderman::NATIVE_ORDER_TYPE_MARKET}};
auto it = type_map.find(type);
return it != type_map.end() ? it->second : api::orderman::NativeOrderType::NATIVE_ORDER_TYPE_UNDEFINED;
}
static void populateCreateOrderFields(
ExecutionReport &er, const CreateOrderMsg &create_order_msg) {
er.security = create_order_msg.security;
er.order_type = create_order_msg.order_type;
er.time_in_force = create_order_msg.time_in_force;
er.manual_order_indicator = create_order_msg.manual_order_indicator;
er.side = create_order_msg.side;
er.order_key = create_order_msg.key;
to_array(
create_order_msg.routing_record->account(), &er.account_name.value());
to_array(
create_order_msg.routing_record->operator_id(),
&er.operator_id.value());
er.exchange = create_order_msg.security->exchange();
}
static void populateExecTypeAndOrderStatus(
ExecutionReport &er, const std::string &state) {
if (state == "canceled") {
er.exec_type = api::orderman::ExecType::EXEC_TYPE_CANCELED;
er.order_status = api::orderman::NATIVE_ORDER_STATUS_CANCELED;
} else if (state == "live") {
er.exec_type = api::orderman::ExecType::EXEC_TYPE_NEW;
er.order_status = api::orderman::NATIVE_ORDER_STATUS_NEW;
} else if (state == "partially_filled") {
er.exec_type = api::orderman::ExecType::EXEC_TYPE_PARTIAL_FILL;
er.order_status = api::orderman::NATIVE_ORDER_STATUS_PARTIALLY_FILLED;
} else if (state == "filled") {
er.exec_type = api::orderman::ExecType::EXEC_TYPE_FILL;
er.order_status = api::orderman::NATIVE_ORDER_STATUS_FILLED;
} else {
er.exec_type = api::orderman::ExecType::EXEC_TYPE_UNDEFINED;
er.order_status = api::orderman::NATIVE_ORDER_STATUS_UNDEFINED;
}
}
static inline void handleOkException(
const std::string &context, const std::string &error) {
bts_assert_msgf(false, "{} Exception: {}", context, error);
}
};
} // namespace bts::orderman::okxexe
@@ -12,7 +12,6 @@
#include "lib/orderman/orderman_utils/orderman_utils.h"
#include "lib/orderman/orders/algo/IAlgoOrder.h"
#include "lib/orderman/orders/algo/IAlgoServices.h"
#include "lib/orderman/orders/algo/algo_orders/QuotingAlgo.h"
#include "lib/utils/PBUtils.h"
#include "lib/utils/bts_assert.h"
#include <bits/stdint-intn.h>
@@ -20,7 +19,6 @@
#include <boost/circular_buffer.hpp>
#include <boost/tokenizer.hpp>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <fcntl.h>
@@ -29,9 +27,10 @@
#include <optional>
#include <stdint.h>
#include <string>
namespace bts {
namespace orderman {
using namespace api::orderman;
using namespace google::protobuf;
using namespace boost;
@@ -41,29 +40,23 @@ bool from_string(const std::string &str, T *out) {
}
//All information required from user
struct QuotingAlgoParams {
SecurityKey security_key_id;
std::string security_name_str;
size_t n_ticks;
bool quote_around_market;
bool quote_around_theo;
int n_ticks;
ChildTemplate *child_template = nullptr;
api::Side side;
QuotingAlgoParams(
SecurityKey security_key_id,
std::string security_name_str,
size_t n_ticks,
bool quote_around_market,
bool quote_around_theo,
int n_ticks,
ChildTemplate* child,
api::Side side
)
: security_key_id(security_key_id), security_name_str(security_name_str),
n_ticks(n_ticks), quote_around_market(quote_around_market),
quote_around_theo(quote_around_theo), child_template(child),side(side) {}
n_ticks(n_ticks), child_template(child),side(side) {}
std::string print() {
std::string sep_obj = ", ";
std::string sep_key_val = ":";
@@ -73,42 +66,31 @@ struct QuotingAlgoParams {
s += sep_obj;
}
s += desc + sep_key_val + val;
};
};
add_to_s("security_key_id", std::to_string(security_key_id.value()));
add_to_s("quote_around_market", std::to_string(quote_around_market));
add_to_s("quote_around_theo", std::to_string(quote_around_theo));
add_to_s("n_ticks", std::to_string(n_ticks));
return s;
}
};
// enum class QuotingAlgoParams::State { WORKING, DONE };
// All state information needed for this algo
struct QuotingAlgo::PImpl {
OrderKey self_key = null_order_key();
OrderDefinition self_def;
google::protobuf::uint32 self_security_key;
IBeforeLaunchAlgoServices *servg;
bts::orderman::OrderKey child_order_key=null_order_key();
Price child_order_price=Price(0);
OrderKey child_order_key = null_order_key();
Price child_order_price = Price(0);
google::protobuf::TextFormat::Printer printer;
State state = State::UPDATING_PARAMS;
double tick_size;
IAlgoServices *algo = nullptr;
analytics::ITheoCalculator const *theo_calc = nullptr;
SecurityKey driver_sec_key;
bool subbed_to_driver = false;
std::optional<Book> driver_book = std::nullopt;
std::optional<Book> sec_book = std::nullopt;
std::optional<Book> sec_book;
std::unique_ptr<QuotingAlgoParams> algo_params;
std::optional<double> current_mid;
PImpl() { printer.SetSingleLineMode(true); }
};
};
QuotingAlgo::QuotingAlgo() : me(new PImpl()) {}
QuotingAlgo::~QuotingAlgo() {}
@@ -143,15 +125,13 @@ bool _convert_params(
const DynamicOrderDefinition &def = cod.dynamic();
const auto &params = def.order_params();
auto quote_around_theo = get_if<bool>(params, "quote_around_theo");
auto quote_around_market = get_if<bool>(params, "quote_around_market");
auto n_ticks = get_if<uint32_t>(params, "n_ticks");
auto n_ticks = get_if<int32_t>(params, "n_ticks");
auto custom_ord = get_if<CustomOrderDefinition>(params,"child_order");
if ( !n_ticks.has_value() || !quote_around_theo.has_value() || !quote_around_market.has_value() || quote_around_theo.value() == quote_around_market.value()){
if ( !n_ticks.has_value() ){
LOG(OrderMan, Info,
"QuotingAlgo::convert_params failed n_ticks:{}, quote_around_theo:{}, quote_around_market:{}",
n_ticks.has_value(), quote_around_theo.has_value(), quote_around_market.has_value());
"QuotingAlgo::convert_params failed n_ticks:{}",
n_ticks.has_value());
return false;
}
auto key = SecurityKey(serv.security().id());
@@ -162,8 +142,6 @@ bool _convert_params(
key,
serv.security().glbx().security_desc(),
n_ticks.value(),
quote_around_market.value(),
quote_around_theo.value(),
child_template,
side
);
@@ -192,7 +170,6 @@ OrderOpResult QuotingAlgo::do_before_launch(IBeforeLaunchAlgoServices &serv) {
_convert_params(serv, def.custom_def(), me->algo_params)) {
me->tick_size = serv.security().contract_spec().low_tick();
me->self_def = serv.definition();
me->self_security_key = serv.security().id();
return {};
@@ -215,14 +192,13 @@ OrderOpResult QuotingAlgo::do_before_launch(IBeforeLaunchAlgoServices &serv) {
// Called in the latency sensitive path, once, when the algo is first launched.
void QuotingAlgo::do_launch(IAlgoServices &algo_services) {
// Cache our algo services
me->algo = &algo_services;
me->algo = &algo_services;
// Subscribe to market data
me->algo->subscribe_book(me->algo_params->security_key_id);
if(me->algo_params->quote_around_theo){
me->algo->subscribe_theos();
}
me->state = State::WORKING;
}
void QuotingAlgo::do_launch_attached( // not needed right now
@@ -234,57 +210,20 @@ void QuotingAlgo::do_launch_attached( // not needed right now
_cxl_and_end(true, "QuotingAlgo can not be attached to working orders");
}
void QuotingAlgo::on_theos(const analytics::ITheoCalculator *theo_calculator){
if(theo_calculator==NULL){
return;
}
me->theo_calc = theo_calculator;
if (me->subbed_to_driver) {
const md::PriceSize &driver_bid = me->driver_book->bid().at(0);
const md::PriceSize &driver_ask = me->driver_book->ask().at(0);
double driver = (driver_bid.price() + driver_ask.price())/2;
bts::api::orderman::PrecomputedTheoPoint result;
if(!theo_calculator->get_theo(me->algo_params->security_key_id.value(),driver, result)){
LOG(OrderMan, Error, "QuotingAlgo::on_theos CANT GET THEOS");
return;
}
me->current_mid = result.theo();
quote(me->current_mid.value());
return;
}
auto driver_sec = me->theo_calc->get_driver_sec(me->algo_params->security_key_id.value());
if (driver_sec.has_value()) {
LOG(OrderMan, Info,
"QuotingAlgo::on_theos order_key: {} subscribing to driver sec_key: {}",
me->algo->order_key(), driver_sec.value());
me->subbed_to_driver = true;
me->driver_sec_key = SecurityKey(driver_sec.value());
me->algo->subscribe_book(SecurityKey(me->driver_sec_key));
}
}
// Called when the book for the specified security has changed
void QuotingAlgo::on_book(SecurityKey sec_key, Book const &book) {
if (sec_key == me->driver_sec_key) {
me->driver_book = book;
return;
}
if (sec_key != me->algo_params->security_key_id)
return;
me->sec_book = book;
if(!me->algo_params->quote_around_market)
return;
const md::PriceSize &bid = me->sec_book->bid().at(0);
const md::PriceSize &ask = me->sec_book->ask().at(0);
double mid_price=(bid.price() + ask.price())/2;
quote(mid_price);
if ( me->algo_params->side==api::Side::SIDE_BUY)
quote(bid.price());
else
quote(ask.price());
}
// Round the price to the nearest tick
@@ -294,14 +233,16 @@ double normalizePrice(double price, double tickSize) {
}
void QuotingAlgo::quote(double price){
double normalized= normalizePrice(price,me->tick_size);
if(me->state!=State::WORKING){
return;
}
double offset = me->algo_params->n_ticks*me->tick_size;
double price_with_offset = normalized + offset*(me->algo_params->side==api::Side::SIDE_BUY?-1:1);
double price_with_offset = price + offset*(me->algo_params->side==api::Side::SIDE_BUY?-1:1);
_fire_orders(price_with_offset);
}
void QuotingAlgo::_fire_orders(double price){
if(price == me->child_order_price.value()){
if(price == me->child_order_price.value() || me->state != State::WORKING){
return;//avoid unnecessary updates
}
me->child_order_price = Price(price);
@@ -324,12 +265,30 @@ void QuotingAlgo::on_ticker(SecurityKey, TradeVector const &) {}
void QuotingAlgo::report_state() {}
void QuotingAlgo::do_update(
api::orderman::Update const &upd) { // not needed right now
LOG(OrderMan, Warn,
"QuotingAlgo::do_update order: {} update not allowed, please "
"cxl and re-enter {} upd key: {}",
me->algo->order_key().value(), upd.order_key());
_cxl_and_end(true, " update not allowed");
api::orderman::Update const &upd) {
me->state = State::UPDATING_PARAMS;
LOG(OrderMan, Diag, "QuotingAlgo order {} receiving update: {}",
_algo().order_key().value(), utils::pb::to_string(upd));
if (!upd.has_custom() || !upd.custom().has_dynamic()) {
_algo().terminate_error("Invalid update");
return;
}
const auto &params = upd.custom().dynamic().order_params();
auto n_ticks = get_if<int32_t>(params, "n_ticks");
if ( !n_ticks.has_value() ){
LOG(OrderMan, Info,
"QuotingAlgo::convert_params failed n_ticks:{}",
n_ticks.has_value());
_algo().terminate_error("Invalid update no value for n ticks");
return;
}
me->algo_params->n_ticks = n_ticks.value();
me->state = State::WORKING;
if(me->sec_book.has_value())
on_book(SecurityKey(me->self_security_key),me->sec_book.value());
}
void QuotingAlgo::on_child_change(OrderKey key, const ChildState &state) {
@@ -340,14 +299,14 @@ void QuotingAlgo::on_child_change(OrderKey key, const ChildState &state) {
}
}
}
// updates states every 500 ms, state visible if algo tree checked on front
// end
void QuotingAlgo::on_status(SecurityKey key, co2::TradingStatus s) {
LOG(OrderMan, Info, "QuotingAlgo::on_status key: {} status: {}", key.value(),
static_cast<int>(s));
}
// updates states every 500 ms, state visible if algo tree checked on front
// end
void QuotingAlgo::on_timer() {}
void QuotingAlgo::_cxl_and_end(bool is_error, const std::string &err_msg) {
@@ -358,4 +317,4 @@ void QuotingAlgo::_cxl_and_end(bool is_error, const std::string &err_msg) {
}
} // namespace orderman
} // namespace bts
} // namespace bts
@@ -18,7 +18,6 @@ class QuotingAlgo : public IAlgoOrder {
void on_status(SecurityKey, co2::TradingStatus) override;
void on_timer() override;
void on_book(SecurityKey, Book const &) override;
void on_theos(const analytics::ITheoCalculator *theo_calculator) override;
void on_ticker(SecurityKey, TradeVector const &) override;
void report_state();
void run_algo();
@@ -26,7 +25,7 @@ class QuotingAlgo : public IAlgoOrder {
private:
struct PImpl;
enum class State;
enum class State { WORKING, DONE, UPDATING_PARAMS };
std::unique_ptr<PImpl> me;
private:
@@ -236,9 +236,7 @@ extern "C" void init_limit_cover(bts::SparkServices &srv) {
extern "C" void init_quoting_algo_plugin(bts::SparkServices &srv) {
DynamicOrderDescriptor quoting_algo;
quoting_algo.set_dynamic_order_type("QuotingAlgo");
*quoting_algo.add_param_desc() = param_uint32("n_ticks");
*quoting_algo.add_param_desc() = param_bool("quote_around_theo");
*quoting_algo.add_param_desc() = param_bool("quote_around_market");
*quoting_algo.add_param_desc() = param_int32("n_ticks");
*quoting_algo.add_param_desc() = param_order("child_order");
srv.register_dynamic_algo_order(
quoting_algo, []() { return std::make_unique<orderman::QuotingAlgo>(); });
@@ -373,7 +371,7 @@ extern "C" void init_spark_plugin(bts::SparkServices &srv) {
// init_sniper_plugin(srv);
init_task_eye_plugin(srv);
init_vol_algo_plugin(srv);
// init_quoting_algo_plugin(srv);
init_quoting_algo_plugin(srv);
}
} // namespace dorman
@@ -3,6 +3,7 @@
#include "lib/config/protos/spreader.pb.h"
#include "lib/logging/logging.h"
#include "lib/orderman/events/SettlementUpdateEvent.h"
#include "lib/orderman/orderman_utils/orderman_common.h"
#include "lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h"
#include "lib/orderman/orders/spreader_orders/spreader/NLegsHedgeRecompData.h"
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderBookkeepingData.h"
@@ -152,6 +153,12 @@ void SpreaderOrder::_on_time(const bts::mercury::TimerEvent &t_event) {
}
}
void SpreaderOrder::_on_book_update(const BookUpdateEvent &book_event) {
// TODO: rmeove
LOG(OrderMan, Diag,
"SpreaderOrder::_on_book_update skey: {} bid: {}/{} a: {}/{}",
book_event.key(), book_event.book.bid().at(0).price(),
book_event.book.bid().at(0).size(), book_event.book.ask().at(0).price(),
book_event.book.ask().at(0).size());
if (!_active)
return;
auto scoped = _order_services->log_scope(
@@ -429,13 +436,18 @@ void SpreaderOrder::_compute_hedge_price_quantity(
_bookkeeping_data().get_leg_data(filled_leg_index);
SpreaderLegData &hedged_leg_data =
_bookkeeping_data().get_leg_data(hedge_leg_index);
if (_bookkeeping_data().n_legs() == 2)
if (_bookkeeping_data().n_legs() == 2) {
SpreaderUtils::compute_hedge_price_size_2_legs(
_bookkeeping_data().effective_price(),
filled_leg_data.leg_property_data(), fill_price, fill_quantity,
hedged_leg_data.leg_property_data(), computed_hedge_price,
hedge_quantity);
else {
LOG(OrderMan, Info,
"SpreaderOrder::_compute_hedge_price_quantity effective_price: {} "
"fill_price: {} fill_qty: hedge_price: {} hedge_qty: {}",
_bookkeeping_data().effective_price(), fill_price, fill_quantity,
computed_hedge_price, hedge_quantity);
} else {
NLegsHedgeRecompData &recomp_data =
_bookkeeping_data().hedge_recomp_data();
if (recomp_data.activated && recomp_data.last_call_succ()) {
@@ -480,15 +492,18 @@ void SpreaderOrder::_comp_and_report_state() {
reported_state.set_status(status);
reported_state.set_price(bookkeeping_data.input_price().value());
reported_state.set_quantity(bookkeeping_data.quantity().value());
reported_state.set_filled_quantity(filled_qty.value());
bts::orderman::set_quantity(
reported_state, StateQuantityType::FilledQuantity,
filled_qty.full_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
LOG(OrderMan, Info,
"SpreaderOrder::_report_state key:{} status:{} "
"price:{} qty:{} filled:{} avg_price:{}",
"SpreaderOrder::_report_state key: {} status: {} "
"price: {} qty: {} filled: {} avg_price: {}",
key(), status, reported_state.price(), reported_state.quantity(),
reported_state.filled_quantity(), reported_state.avg_price());
auto rerported_state_adj = reported_state;
@@ -180,7 +180,7 @@ class SpreaderOrder : public Order, public ISpreaderManager {
void _on_book_update(const BookUpdateEvent &);
void _on_settlement_update(const SettlementUpdateEvent &);
void _on_child_order_update(const OrderStateUpdateEvent &);
// order handling
// order handling for quoter order update events
inline void _on_quoter_order(
const OrderStateUpdateEvent &ord_ev,
SpreaderLegData &leg_data,
@@ -54,6 +54,9 @@ class LegPropertyData {
bool is_net_change() const {
return _price_calc == bts::api::SyntheticContract::PRICECALC_NET_CHANGE;
}
bool is_notional() const {
return _price_calc == bts::api::SyntheticContract::PRICECALC_NOTIONAL_LEG;
}
bts::spark::LegSpreaderParams::MarketDataType md_type() const {
return _leg_params.market_data_type();
}
@@ -67,9 +70,7 @@ class LegPropertyData {
uint32_t n_queue_holder_orders() const {
return _leg_params.quote_params().n_queue_holder_orders();
}
const bts::api::ContractLeg& leg () const{
return _leg_contract;
}
const bts::api::ContractLeg &leg() const { return _leg_contract; }
};
} // namespace orderman
@@ -1,7 +1,10 @@
#pragma once
#include "lib/domain_model/common.h"
#include "lib/logging/logging.h"
#include "lib/orderman/orderman_utils/orderman_common.h"
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderChildOrderState.h"
#include "lib/utils/bts_assert.h"
namespace bts {
namespace orderman {
@@ -65,7 +68,25 @@ class QuoteLegPriceQuantityState {
rv = _in_target_leg_quantity.value() - accounted_for;
return Quantity(rv);
}
Quantity remaining_to_fill_raw() const {
Quantity rem = remaining_to_fill();
SpreaderFractionalQtyHelper const *qty_helper =
_order.spreader_qty_helper();
bts_assert(qty_helper != nullptr);
Quantity raw_qty = qty_helper->out_transform_leg_order_qty(
_order.sec_key(), _order.total_filled_price(), rem);
return raw_qty;
}
// TODO here, if the rmeainder is > min qty size, do the same
bool target_reached() const { return remaining_to_fill().value() == 0; }
bool target_reached_raw() const {
Quantity rem_raw = remaining_to_fill_raw();
LOG(OrderMan, Info,
"QuoteLegPriceQuantityState::target_reached_raw() rem_raw: {}",
rem_raw.full_value());
return rem_raw.is_zero();
}
// mutators ////////////////////////////////////////////
////////////////////////////////////////////////////////
// the 2 mutators, called by SingleLegQuoter (and the various QuoterStateWorkers)
@@ -290,6 +290,11 @@ void SingleLegQuoter::_transform_to_WAITING_FOR_CANCEL_ACK() {
}
void SingleLegQuoter::_transform_to_END() {
bts_assert(_worker->state() != QuoterState::END);
std::string curr_state = _worker == nullptr ? "NULL" : _worker->state_text();
LOG(OrderMan, Info,
"SingleLegQuoter::_transform_to_END "
"spread_key:{} leg_idx:{}",
_spreader.spreader_order_key(), _this_leg_data.leg_index());
_switch_state(&_end_state_worker);
}
void SingleLegQuoter::_switch_state(IQuoterStateWorker *new_state) {
@@ -1,4 +1,6 @@
#include "SpreaderBookkeepingData.h"
#include "lib/domain_model/common.h"
#include "lib/logging/logging.h"
#include "lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h"
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderFractionalQtyHelper.h"
#include "lib/os/os.h"
@@ -25,7 +27,9 @@ SpreaderBookkeepingData::SpreaderBookkeepingData(
.spreader()
.execution_strategy()
.hedge_pricing()),
_fqty_helper(spreader_order_key, legs) {
_fqty_helper(
spreader_order_key, order_def.custom_def().spreader(), legs) {
//_order_def.custom_def().spreader()
LOG(OrderMan, Info,
"SpreaderBookkeepingData.cons started "
"key: {} n_legs: {}",
@@ -97,8 +101,12 @@ void SpreaderBookkeepingData::total_fills(
avg_price += leg_fill_price.value() *
leg_data.leg_property_data().price_multiplier();
}
LOG(OrderMan, Info,
"SpreaderBookkeepingData::total_fills avg_price: {} fills: {}",
avg_price, fills);
avg_price = fills == 0 ? 0.0 : avg_price; // should it be nan?
out_quantity = Quantity(fills);
out_quantity = Quantity::create_new(fills);
out_price = Price(avg_price);
}
} // namespace orderman
@@ -3,10 +3,13 @@
#include "SpreaderUtils.h"
#include "external_interfacing/IAlternateOrderGateway.h"
#include "lib/api/services/OrderService.pb.h"
#include "lib/domain_model/SecurityKey.h"
#include "lib/domain_model/common.h"
#include "lib/logging/logging.h"
#include "lib/orderman/events/OrderStateUpdateEvent.h"
#include "lib/orderman/orderman_utils/orderman_common.h"
#include "lib/orderman/orders/spreader_orders/spreader/ISpreaderManager.h"
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderFractionalQtyHelper.h"
namespace bts {
namespace orderman {
@@ -80,6 +83,7 @@ class SpreaderChildOrderState {
else
return _test_only_order_key;
}
SecurityKey sec_key() const { return _sec_key; }
api::orderman::OrderStatus status() const { return _status; }
// if the last fill was a status change
// this will be zero
@@ -112,11 +116,20 @@ class SpreaderChildOrderState {
Quantity total_fills() const {
return Quantity(_base_quantity.value() + _filled_quantity.value());
}
Price total_filled_price() const {
Quantity filled_qty;
Price rv;
total_fills(filled_qty, rv);
return rv;
}
void total_fills(Quantity &out_filled, Price &out_price) const {
SpreaderUtils::compute_average_price(
_base_price, _base_quantity, _filled_price, _filled_quantity,
out_price, out_filled);
}
SpreaderFractionalQtyHelper const *spreader_qty_helper() {
return _spreader_manager->spreader_qty_helper();
}
// this is the fills from the current active order
Quantity current_fills() const { return _filled_quantity; }
Price out_price() const { return _out_price; }
@@ -140,7 +153,13 @@ class SpreaderChildOrderState {
// last filled price
void on_order_update(const OrderStateUpdateEvent &e) {
bts_assert(!_is_live || e.key == order_key());
bts_assert(e.filled_quantity >= _filled_quantity.value());
//bts_assert(e.filled_quantity >= _filled_quantity.value());
if (!(e.filled_quantity >= _filled_quantity.value())) {
LOG(OrderMan, Error,
"assert condition e.ok: {} e.qty: {} e.filled_qty: {} "
"_filled_quantity: {}",
e.key, e.quantity, e.filled_quantity, _filled_quantity);
}
_status = e.status;
_last_filled_quantity =
Quantity(e.filled_quantity - _filled_quantity.value());
@@ -161,7 +180,7 @@ class SpreaderChildOrderState {
_n_place_orders++;
auto const *qty_helper = _spreader_manager->spreader_qty_helper();
auto adj_qty =
qty_helper->out_transform_leg_order_qty(_sec_key, quantity);
qty_helper->out_transform_leg_order_qty(_sec_key, price, quantity);
if (_is_live) {
if (_spreader_manager != nullptr)
_spreader_manager->place_order(
@@ -187,7 +206,7 @@ class SpreaderChildOrderState {
_n_modify_orders++;
auto const *qty_helper = _spreader_manager->spreader_qty_helper();
auto adj_qty =
qty_helper->out_transform_leg_order_qty(_sec_key, quantity);
qty_helper->out_transform_leg_order_qty(_sec_key, price, quantity);
if (_is_live) {
if (_spreader_manager != nullptr) {
_spreader_manager->update_order(
@@ -8,10 +8,14 @@
#include "lib/md/PriceSize.h"
#include "lib/orderman/events/BookUpdateEvent.h"
#include "lib/orderman/events/OrderStateUpdateEvent.h"
//#include "lib/orderman/orders/spreader_orders/SpreaderOrder.h"
#include "lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h"
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderUtils.h"
#include "lib/os/os.h"
#include "lib/utils/PBUtils.h"
#include "lib/utils/bts_assert.h"
#include <bits/stdint-uintn.h>
#include <cmath>
#include <utility>
namespace bts::orderman {
@@ -33,20 +37,58 @@ bool has_crypto_leg(const std::vector<LegPropertyData> &legs) {
return false;
}
struct QtyTickInfo {
public:
const double tick_base;
const double tick_unit;
const double tick_sz;
const double tick_mult;
QtyTickInfo(double tick_base_, double tick_unit_)
: tick_base(tick_base_), tick_unit(tick_unit_),
tick_sz(tick_unit_ / tick_base_), tick_mult(tick_base_ / tick_unit_) {}
};
const QtyTickInfo &get_qty_tick_info(
const std::map<bts::SecurityKey, QtyTickInfo> &qty_info_map,
SecurityKey skey,
const QtyTickInfo &def) {
auto it = qty_info_map.find(skey);
if (it != qty_info_map.end()) {
return def;
}
return it->second;
}
struct SpreaderFractionalQtyHelper ::PImpl {
Book op_book = {};
OrderStateUpdateEvent op_ord = {};
const bool passthrough = false;
const double in_qty_mult;
PImpl(const std::vector<LegPropertyData> &lpd_vec)
: passthrough(has_crypto_leg(lpd_vec) ? false : true),
in_qty_mult(
has_crypto_leg(lpd_vec)
? static_cast<double>(
config::get<bts::config::Spreader>().qty_multiplier())
: 1.0) {
LOG(OrderMan, Info, "SpreaderFractionalQtyHelper::cons, in_qty_mult: {}",
in_qty_mult);
//const double in_qty_mult;
const bool is_notional;
const QtyTickInfo def_qty_tick_info = QtyTickInfo(10000.0, 1.0);
std::map<bts::SecurityKey, QtyTickInfo> qty_info_map = {};
PImpl(
const api::orderman::SpreaderOrderDefinition &spreader_order_def,
const std::vector<LegPropertyData> &lpd_vec)
: passthrough(!has_crypto_leg(lpd_vec)),
is_notional(
spreader_order_def.security().synthetic().price_calc() ==
api::SyntheticContract_PriceCalc_PRICECALC_NOTIONAL_LEG) {
auto i = 0;
for (const auto &l : lpd_vec) {
const auto &qiv = l.security().contract_spec().quantity_infos();
QtyTickInfo qi =
qiv.empty()
? QtyTickInfo(100.0, 1)
: QtyTickInfo(qiv.at(0).tick_base(), qiv.at(0).tick_unit());
bts::SecurityKey skey(l.security_key());
qty_info_map.insert({skey, qi});
LOG(OrderMan, Info,
"SpreaderFractionalQtyHelper::cons leg: {} sec: {}", i,
utils::pb::to_string(l.security()));
i++;
}
}
};
@@ -55,8 +97,12 @@ std::vector<LegPropertyData> SpreaderFractionalQtyHelper::get_transformed_legs(
std::vector<LegPropertyData> rv = {};
for (const auto &l : legs) {
bts::api::ContractLeg leg_adj = l.leg_contract();
leg_adj.set_quantity(l.size_multiplier() * me->in_qty_mult);
double qty_tick_size = 1.0 / me->in_qty_mult;
const auto &q_tinfo = get_qty_tick_info(
me->qty_info_map, bts::SecurityKey(l.leg_security_key()),
me->def_qty_tick_info);
double qm = q_tinfo.tick_mult;
leg_adj.set_quantity(l.size_multiplier() * qm);
double qty_tick_size = q_tinfo.tick_sz;
LegPropertyData lpd_adj(
l.leg_index(), leg_adj, l.leg_params(), l.trading_side(),
bts::api::SyntheticContract::PriceCalc::
@@ -68,15 +114,35 @@ std::vector<LegPropertyData> SpreaderFractionalQtyHelper::get_transformed_legs(
}
SpreaderFractionalQtyHelper::SpreaderFractionalQtyHelper(
OrderKey /*spreader_ord_key*/, const std::vector<LegPropertyData> &lpd_vec)
: me(new PImpl(lpd_vec)) {}
OrderKey spreader_ord_key,
const api::orderman::SpreaderOrderDefinition &spreader_order_def,
const std::vector<LegPropertyData> &lpd_vec)
: me(new PImpl(spreader_order_def, lpd_vec)) {
LOG(OrderMan, Info,
"SpreaderFractionalQtyHelper::cons spreader_ord_key: {} passthrough: {} "
"is_notional: {}",
spreader_ord_key, me->passthrough, me->is_notional);
}
const Book &SpreaderFractionalQtyHelper::in_transform_book(
const Book &bk) const {
if (me->passthrough)
if (me->passthrough) {
LOG(OrderMan, Diag,
"SpreaderFractionalQtyHelper::in_transform_book passthrough");
return bk;
me->op_book = bk;
double qm = me->in_qty_mult;
}
if (me->is_notional) {
SpreaderUtils::compute_notional_book(bk, me->op_book);
LOG(OrderMan, Diag,
"SpreaderFractionalQtyHelper::in_transform_book sec: {} bk: {} "
"notional: {}",
bk.security_key().value(), bk, me->op_book);
} else {
me->op_book = bk;
}
const auto &q_tinfo = get_qty_tick_info(
me->qty_info_map, bk.security_key(), me->def_qty_tick_info);
double qm = q_tinfo.tick_mult;
for (auto &ps : me->op_book.bid()) {
ps = bts::md::PriceSize(ps.int_price(), ps.size() * qm);
}
@@ -84,6 +150,9 @@ const Book &SpreaderFractionalQtyHelper::in_transform_book(
ps = bts::md::PriceSize(ps.int_price(), ps.size() * qm);
}
// mult by leg_mult
LOG(OrderMan, Diag,
"SpreaderFractionalQtyHelper::in_transform_book bk: {} output: {}", bk,
me->op_book);
return me->op_book;
}
@@ -92,17 +161,31 @@ SpreaderFractionalQtyHelper::in_transform_order_event(
SecurityKey sec_key, const OrderStateUpdateEvent &ev) const {
auto log_ev = [](auto sec_key, const auto &ev, const auto &rv_ev) {
LOG(OrderMan, Info,
"SpreaderFractionalQtyHelper::in_transform_order_event sec_key: {} "
"ev.qty: {} ev.filled_qty: {} rv.qty: {} rv.filled_qty: {}",
sec_key.value(), ev.quantity, ev.filled_quantity, rv_ev.quantity,
rv_ev.filled_quantity);
"SpreaderFractionalQtyHelper::in_transform_order_event sec_key: "
"{} "
"ev.qty: {} ev.filled_qty: ev.avg_price: {} rv.qty: {} "
"rv.filled_qty: {}",
sec_key.value(), ev.quantity, ev.filled_quantity, ev.avg_price,
rv_ev.quantity, rv_ev.filled_quantity);
};
if (me->passthrough) {
log_ev(sec_key, ev, ev);
return ev;
}
// if notional, then order qtys needs to be converted to notional, since spreader sees books
// in notionals and fire orders in notionals
me->op_ord = ev;
double qmult = me->in_qty_mult;
if (me->is_notional) {
me->op_ord.quantity =
SpreaderUtils::to_notional(Price(ev.price), ev.quantity);
me->op_ord.filled_quantity =
SpreaderUtils::to_notional(Price(ev.avg_price), ev.filled_quantity);
me->op_ord.filled_at_attach_time_quantity = os::nan();
}
// factor mult
const auto &q_tinfo =
get_qty_tick_info(me->qty_info_map, sec_key, me->def_qty_tick_info);
double qmult = q_tinfo.tick_mult;
me->op_ord.quantity *= qmult;
me->op_ord.filled_quantity *= qmult;
me->op_ord.filled_at_attach_time_quantity *= qmult;
@@ -119,23 +202,43 @@ Quantity SpreaderFractionalQtyHelper::in_transform_spreader_qty(
}
Quantity SpreaderFractionalQtyHelper::out_transform_leg_order_qty(
SecurityKey, Quantity qty) const {
SecurityKey skey, Price price, Quantity qty) const {
auto qty_init = qty;
auto tr = [&]() {
if (me->passthrough)
return qty;
double qm = me->in_qty_mult;
return Quantity::create_new(qty.full_value() / qm);
if (me->is_notional) {
qty = SpreaderUtils::from_notional(price, qty);
LOG(OrderMan, Info,
"SpreaderFractionalQtyHelper::out_transform_leg_order_qty "
"notional -> physical {} -> {}",
qty_init, qty);
}
const auto &q_tinfo =
get_qty_tick_info(me->qty_info_map, skey, me->def_qty_tick_info);
double tick_sz = q_tinfo.tick_sz;
double qm = q_tinfo.tick_mult;
double qty_out = qty.full_value() / qm;
// rouning it to tick size
qty_out = std::floor(qty_out / tick_sz) * tick_sz;
return Quantity::create_new(qty_out);
};
auto rv_qty = tr();
LOG(OrderMan, Info,
"SpreaderFractionalQtyHelper::out_transform_leg_order_qty qty: {} "
"SpreaderFractionalQtyHelper::out_transform_leg_order_qty price: {} "
"qty: {} "
"rv_qty: {}",
qty.full_value(), rv_qty.full_value());
price, qty_init.full_value(), rv_qty.full_value());
return rv_qty;
}
void SpreaderFractionalQtyHelper::out_transform_order_state(
bts::api::orderman::OrderState &) const {
bts::api::orderman::OrderState &s) const {
auto s_txt = utils::pb::to_string(s);
LOG(OrderMan, Info,
"SpreaderFractionalQtyHelper::out_transform_order_state state: "
"{} filled_qty: {} avg_price: {}",
s_txt, s.filled_quantity(), s.avg_price());
// todo:
// this needs to divide the filled qty of the spreader
// if (me->passthrough)
@@ -3,6 +3,9 @@
//#include "SpreaderLegData.h"
//#include "lib/domain_model/common.h"
//#include "lib/orderman/orderman_utils/orderman_common.h"
#include "lib/api/services/OrderService.pb.h"
#include "lib/domain_model/SecurityKey.h"
//#include "lib/orderman/orders/spreader_orders/SpreaderOrder.h"
#include "lib/orderman/orders/spreader_orders/spreader/LegPropertyData.h"
namespace bts::api::orderman {
@@ -18,7 +21,9 @@ class OrderStateUpdateEvent;
class SpreaderFractionalQtyHelper {
public:
SpreaderFractionalQtyHelper(
OrderKey spreader_ord_key, const std::vector<LegPropertyData> &legs);
OrderKey spreader_ord_key,
const api::orderman::SpreaderOrderDefinition &spreader_order_def,
const std::vector<LegPropertyData> &legs);
// the leg qtys need to be translated
std::vector<LegPropertyData> get_transformed_legs(
const std::vector<LegPropertyData> &legs) const;
@@ -28,9 +33,13 @@ class SpreaderFractionalQtyHelper {
const OrderStateUpdateEvent &in_transform_order_event(
SecurityKey sec_key, const OrderStateUpdateEvent &) const;
Quantity in_transform_spreader_qty(Quantity qty) const;
Quantity out_transform_leg_order_qty(SecurityKey key, Quantity qty) const;
// TODO add price
Quantity out_transform_leg_order_qty(
SecurityKey key, Price price, Quantity qty) const;
// the state reported ot he rest of the system also has to be converted
void out_transform_order_state(bts::api::orderman::OrderState &) const;
// struct QtyTickInfo;
// const QtyTickInfo &_get_tickinfo(SecurityKey skey);
private:
struct PImpl;
@@ -4,8 +4,10 @@
#include "lib/domain_model/Security.h"
#include "lib/domain_model/Security.pb.h"
#include "lib/domain_model/common.h"
#include "lib/jetfix/codec/FieldDefinitions.h"
#include "lib/md/IntPrice.h"
#include "lib/md/PriceSize.h"
#include "lib/md/Shm/MDStructure.h"
#include "lib/orderman/OrderBuilder.h"
#include "lib/orderman/orderman_utils/Book.h"
#include "lib/orderman/orderman_utils/fix_order_codec.h"
@@ -16,6 +18,7 @@
#include "lib/utils/bts_assert.h"
#include "lib/utils/bts_chrono.h"
#include <cstddef>
#include <cstdint>
#include <random>
#include <utility>
@@ -256,6 +259,7 @@ class SpreaderUtils {
book_side[i] = empty_ps;
}
}
// not in the critical path, creates copy
static BookSide remove_0qtys(const BookSide &bside) {
BookSide rv = {};
@@ -1138,6 +1142,55 @@ class SpreaderUtils {
std::tuple<Price, Quantity> order,
std::vector<std::tuple<Price, Quantity>> &out_fills);
///////////////////////////////////////////////////
// notional ///////////////////////////////////////
//////////////////////////////////////////////////
// for crypto
static md::Size to_notional(Price price, md::Size physical_size) {
// qn = pr * qp
double rv = to_notional(price, static_cast<double>(physical_size));
return static_cast<md::Size>(rv);
}
static double to_notional(Price price, double physical_size) {
// qn = pr * qp
double notional_size = price.value() * physical_size;
return notional_size;
}
static md::Size from_notional(Price price, md::Size notional_sz) {
md::Size physical_size = notional_sz / price.value();
return physical_size;
}
static Quantity from_notional(Price price, Quantity notional_qty) {
double physical_qty = notional_qty.full_value() / price.value();
return Quantity::create_new(physical_qty);
}
void static compute_notional_book(
const Book &source_book, Book &target_book) {
target_book = source_book;
// TODO, 5
compute_notional_book(source_book.bid(), 0, 5, target_book.bid());
compute_notional_book(source_book.ask(), 0, 5, target_book.ask());
}
void static compute_notional_book(
const BookSide &source_book_side,
size_t start_index,
size_t n_levels,
BookSide &target_book_side) {
empty_out_book(target_book_side, start_index, n_levels);
for (size_t i = start_index; i < n_levels; i++) {
const md::PriceSize &ps_source = source_book_side.at(i);
md::Size notional_sz =
to_notional(Price(ps_source.price()), ps_source.size());
md::PriceSize &ps_target = target_book_side.at(i);
ps_target = md::PriceSize(ps_source.int_price(), notional_sz);
}
}
////////////////////////////////////////////////////
// Rounding functions //////////////////////////////
////////////////////////////////////////////////////
@@ -1152,6 +1205,10 @@ class SpreaderUtils {
else
return qty;
}
inline static uint32_t round_to_nearest(uint32_t n, uint32_t nearest) {
return (n / nearest) * nearest;
}
static uint32_t _round_quantity(double qty, bool is_up) {
double round_err = 0.000000001;
double round_qty = std::round(qty);
@@ -1,4 +1,6 @@
#include "lib/orderman/orders/spreader_orders/spreader/quoter/quoter_states/QuoterEndStateWorker.h"
#include "lib/domain_model/common.h"
#include "lib/logging/logging.h"
#include "lib/orderman/orders/spreader_orders/spreader/ISpreaderManager.h"
#include "lib/orderman/orders/spreader_orders/spreader/SingleLegQuoter.h"
#include "lib/orderman/orders/spreader_orders/spreader/SpreaderChildOrderState.h"
@@ -9,10 +11,16 @@ namespace orderman {
QuoterState QuoterEndStateWorker::state() const { return QuoterState::END; }
void QuoterEndStateWorker::on_transition() {
Quantity rem = _quoter->_price_quantity_state.remaining_to_fill();
Quantity rem_raw = _quoter->_price_quantity_state.remaining_to_fill_raw();
LOG(OrderMan, Info,
"QuoterEndStateWorker::on_transition rem: {} rem_raw: {}",
rem.full_value(), rem_raw.full_value());
if (_quoter->_n_failed_iocs >= _quoter->_max_failed_iocs)
_quoter->_set_irrecoverable_error();
bool is_final = _quoter->_is_permenantly_cancelled ||
_quoter->_price_quantity_state.target_reached() ||
_quoter->_price_quantity_state.target_reached_raw() ||
_quoter->irrecoverable_error();
if (!is_final) {
// getting new order
+2 -8
View File
@@ -1,21 +1,15 @@
#ifndef NO_CPP0X
#include "Dispatcher.h"
#include "../os/os.h"
#include "defer.h"
#include "lib/logging/logging.h"
#include "lib/utils/bts_assert.h"
#include "lib/utils/bts_chrono.h"
#include "lib/utils/thread_manager.h"
#include "lib/utils/type_name.h"
#include <boost/asio.hpp>
#include <memory>
#include <stdexcept>
#include <boost/thread.hpp>
#include <chrono>
#include <condition_variable>
#include <memory>
#include <mutex>
#include <thread>
+1 -1
View File
@@ -82,7 +82,7 @@ static OrderServiceServer create_order_service_server(
static void execute() {
using namespace bts;
LOG(Gen, Warn,
"------------------------------------ Sparkd launched "
"------------------------------------ Sparkd v2.2.5 launched "
"--------------------------------");
auto qb_cfg = config::get<config::QuickbusCfg>().node();
@@ -3,7 +3,6 @@
#include "lib/api/md/BinanceMarketDataConfig.pb.h"
#include "lib/collections/Contains.h"
#include "lib/collections/Remove.h"
#include "lib/collections/UnorderedSetFactory.h"
#include "lib/core/FMT.h"
#include "lib/domain/Security.h"
#include "lib/domain/SecurityCriteria.h"
@@ -35,6 +34,7 @@
#include <range/v3/view.hpp>
#include <range/v3/view/transform.hpp>
#include <string>
#include <thread>
#include <unordered_map>
namespace bts::md::binance {
@@ -293,6 +293,20 @@ struct BinanceSubscription::PImpl {
{mk_stats_req(sec_descs, hnd_id),
&mk_stats_handler(sec_descs, hnd_id)});
}
/**
* @brief wrapper method to batch securities so sub limits are not reached
*/
template <class SecDescs>
void batch_subscribe_market_data(SecDescs securities, size_t n = 16) {
auto it = securities.begin();
while (it != securities.end()) {
auto batch_end = std::next(it, std::min(n, static_cast<size_t>(std::distance(it, securities.end()))));
SecDescs batch(it, batch_end);
subscribe_market_data(batch);
it = batch_end;
std::this_thread::sleep_for(500ms);
}
}
/**
* @brief Subscribes to depth, ticker, and price data for a list of
@@ -360,6 +374,8 @@ struct BinanceSubscription::PImpl {
}
listener->on_securities(sec_ptrs);
// batch_subscribe_market_data(sec_descs,4);
bts_assert(sec_descs.size() < 256);
subscribe_market_data(sec_descs);
}
};
@@ -30,7 +30,7 @@ void CboeMarketDatahandler::run(
base_path + config.databento_settings().bbo_file_name(),
config.databento_settings().key(),
config.databento_settings().dataset(),
config.databento_settings().filter_securities_one_year(),
config.databento_settings().filter_securities_upto_n_months(),
config.databento_settings().load_reference_data());
}
@@ -4,14 +4,19 @@
#include "databento/exceptions.hpp"
#include "databento/live_blocking.hpp"
#include "databento/log.hpp"
#include "databento/record.hpp"
#include "databento/timeseries.hpp"
#include "lib/api/domain/Venue.pb.h"
#include "lib/config/config.h"
#include "lib/config/protos/system_config.pb.h"
#include "lib/core/TimeStamp.h"
#include "lib/domain/AggressorSide.h"
#include "lib/domain/Book.h"
#include "lib/domain/Quantity.h"
#include "lib/domain/Security.h"
#include "lib/domain/SecurityID.h"
#include "lib/domain/Side.h"
#include "lib/domain/TradeRecord.h"
#include "lib/domain_model/Security.pb.h"
#include "lib/logging/logging.h"
#include "lib/utils/TimeStamp.h"
@@ -66,11 +71,10 @@ void DatabentoDatahandler::run(
const std::string &books_path,
const std::string &key,
const std::string &dataset,
const bool filter_securities,
const uint filter_securities_upto_n_months,
const bool load_reference_data,
const std::chrono::duration<long double> sleep_duration) {
(void)filter_securities;
auto sleep_duration_updated = sleep_duration;
if (!use_file) {
@@ -119,6 +123,10 @@ void DatabentoDatahandler::run(
std::string exp_string = databento::ToIso8601(exp_nanos);
const auto expiration_time =
bts::core::TimeStamp::from_iso_string(exp_string);
auto duration = filter_securities_upto_n_months*30*24*60*60;
if( expiration_time.seconds() >= bts::core::TimeStamp::now().seconds() + duration){
return databento::KeepGoing::Continue;
}
switch (sec_type) {
case SEC_TYPE(FUTURE):
@@ -186,6 +194,48 @@ void DatabentoDatahandler::run(
utils::TimeStamp{datetime::ptime_to_timestamp(recv_time.to_ptime())});
return databento::KeepGoing::Continue;
};
auto process_trades = [&](const databento::Record &record) {
const auto &msg = record.Get<databento::TradeMsg>();
// The security type to is needed to get the id. Since the sec type isn't known,
// check for an id using both types (OPTION and FUTURE). This is safe to do since
// options and futures never have common names (instrument ids).
const auto name = std::to_string(msg.hd.instrument_id);
auto sec_id_opt =
sec_id_store->get_id(venue, SEC_TYPE(OPTION), name, false);
if (!sec_id_opt.has_value()) {
sec_id_opt =
sec_id_store->get_id(venue, SEC_TYPE(FUTURE), name, false);
if (!sec_id_opt.has_value()) {
return databento::KeepGoing::Continue;
}
}
sleep_for(sleep_duration_updated);
bts::domain::SecurityID sec_id =
bts::domain::SecurityID(sec_id_opt.value());
domain::TradeRecord trade;
trade.trade_price = bts::domain::Price(msg.price, -9);
trade.trade_quantity = bts::domain::Quantity(msg.size);
trade.sec_id = sec_id;
switch(msg.side){
case databento::Side::Ask:
trade.agg_side = domain::AggressorSide::Sell;
break;
case databento::Side::Bid:
trade.agg_side = domain::AggressorSide::Buy;
break;
case databento::Side::None:
trade.agg_side = domain::AggressorSide::None;
break;
}
trade.time_stamp = bts::core::TimeStamp::from_iso_string(
databento::ToIso8601(msg.ts_recv));
std::vector<const domain::TradeRecord *> trade_ptrs;
trade_ptrs.push_back(&trade);
listener->on_trades(trade_ptrs);
return databento::KeepGoing::Continue;
};
if (use_file) {
auto client = databento::HistoricalBuilder{}.SetKey(key).Build();
databento::DbnFileStore dbn_file_store(securities_path);
@@ -217,6 +267,7 @@ void DatabentoDatahandler::run(
client.Subscribe(symbols, databento::Schema::Definition, databento::SType::Parent,databento::UnixNanos{});
else client.Subscribe(symbols, databento::Schema::Definition, databento::SType::Parent);
client.Subscribe(symbols, databento::Schema::Mbp1, databento::SType::Parent);
client.Subscribe(symbols, databento::Schema::Trades, databento::SType::Parent);
client.Start();
while (true) {
try {
@@ -231,6 +282,9 @@ void DatabentoDatahandler::run(
if (record->Holds<databento::InstrumentDefMsg>()) {
process_security(*record);
}
if (record->Holds<databento::TradeMsg>()) {
process_trades(*record);
}
} catch (const databento::TcpError& error) {
std::string err = error.what();
LOG(md, Error, "Caught TCP error: {} \nAttempting reconnect\n",err);
@@ -14,7 +14,7 @@ class DatabentoDatahandler {
const std::string& books_path,
const std::string &dataset="",
const std::string& key = "db-Q4eCbTQfrAHaG6sAcuFUYB9YqkF6j",
const bool filter_securities=false,
const uint filter_securities_upto_n_months=48,
const bool load_reference_data = true,
const std::chrono::duration<long double> sleep_duration = 0.04s);
};
@@ -31,7 +31,7 @@ void GlbxMarketDatahandler::run(
base_path + config.databento_settings().bbo_file_name(),
config.databento_settings().key(),
config.databento_settings().dataset(),
config.databento_settings().filter_securities_one_year());
config.databento_settings().filter_securities_upto_n_months());
}
} // namespace bts::api::md::glbx
@@ -41,8 +41,8 @@ static void instrument_to_sec(const OkxInstrument &oi, Security &sec) {
scomm.exchange_sec_id = oi.instId;
scomm.desc = oi.instId;
scomm.min_price_increment = oi.tickSz;
scomm.min_quantity = oi.lotSz;
scomm.min_quantity_increment = oi.minSz;
scomm.min_quantity = oi.minSz;
scomm.min_quantity_increment = oi.lotSz;
scomm.currency = oi.quoteCcy;
switch (oi.instType.value) {
@@ -51,9 +51,13 @@ static void instrument_to_sec(const OkxInstrument &oi, Security &sec) {
sec.security.emplace<SpotSecurity>();
break;
case bts::md::okx::OkxInstrumentType::SWAP:{
sec.security_common.symbol = oi.settleCcy;
auto &fut = sec.security.emplace<FutureSecurity>();
core::TimeStamp::from_ymd(3000, 1, 1,&fut.expiration);
if(oi.instId.find("USDT")!=std::string::npos){
sec.security_common.symbol = oi.instId.substr(0,oi.instId.find_first_of("-"));
break;
}
sec.security_common.symbol = oi.settleCcy;
break;
}
case OkxInstrumentType::FUTURES: {
+43
View File
@@ -0,0 +1,43 @@
BINANCE_USDS_FUTURES_PROD_URL="https://fapi.binance.com"
BINANCE_USDS_FUTURES_DEV_URL="https://testnet.binancefuture.com"
BINANCE_USDS_FUTURES_EXCHANGE_INFO_PATH="/fapi/v1/exchangeInfo"
BINANCE_USDS_FUTURES_DEPTH_PATH="/fapi/v1/depth"
BINANCE_USDS_FUTURES_DEPTH_PARAMS='symbol=${SYMBOL}&limit=${LIMIT:-5}'
BINANCE_USDS_FUTURES_RECENT_TRADES_PATH="/fapi/v1/trades"
BINANCE_USDS_FUTURES_RECENT_TRADES_PARAMS='symbol=${SYMBOL}&limit=${LIMIT:-500}'
BINANCE_USDS_FUTURES_HISTORICAL_TRADES_PATH="/fapi/v1/historicalTrades"
BINANCE_USDS_FUTURES_HISTORICAL_TRADES_PARAMS='symbol=${SYMBOL}&limit=${LIMIT:-500}'
BINANCE_USDS_FUTURES_MARK_PRICE_PATH="/fapi/v1/premiumIndex"
BINANCE_USDS_FUTURES_MARK_PRICE_PARAMS='symbol=${SYMBOL}'
BINANCE_USDS_FUTURES_PRICE_TICKER_PATH="/fapi/v1/ticker/price"
BINANCE_USDS_FUTURES_PRICE_TICKER_PARAMS='symbol=${SYMBOL}'
BINANCE_USDS_FUTURES_BBO_PATH="/fapi/v1/ticker/bookTicker"
BINANCE_USDS_FUTURES_BBO_PARAMS='symbol=${SYMBOL}'
BINANCE_USDS_FUTURES_OPEN_INTEREST_PATH="/dapi/v1/openInterest"
BINANCE_USDS_FUTURES_OPEN_INTEREST_PARAMS='symbol=${SYMBOL}'
BINANCE_USDS_FUTURES_LONG_SHORT_RATIO_PATH="/futures/data/globalLongShortAccountRatio"
BINANCE_USDS_FUTURES_LONG_SHORT_RATIO_PATH='symbol=${SYMBOL}&period=${PERIOD:-30}&limit=${LIMIT:-10}'
BINANCE_COIN_FUTURES_PROD_URL="https://dapi.binance.com"
BINANCE_COIN_FUTURES_DEV_URL="https://testnet.binancefuture.com"
BINANCE_COIN_FUTURES_EXCHANGE_INFO_PATH="/dapi/v1/exchangeInfo"
BINANCE_COIN_FUTURES_DEPTH_PATH="/dapi/v1/depth"
BINANCE_COIN_FUTURES_RECENT_TRADES_PATH="/dapi/v1/trades"
BINANCE_COIN_FUTURES_HISTORICAL_TRADES_PATH="/dapi/v1/historicalTrades"
BINANCE_COIN_FUTURES_MARK_PRICE_PATH="/dapi/v1/premiumIndex"
BINANCE_COIN_FUTURES_PRICE_TICKER_PATH="/dapi/v1/ticker/price"
BINANCE_COIN_FUTURES_BBO_PATH="/dapi/v1/ticker/bookTicker"
BINANCE_COIN_FUTURES_OPEN_INTEREST_PATH="/dapi/v1/openInterest"
BINANCE_COIN_FUTURES_LONG_SHORT_RATIO_PATH="/futures/data/globalLongShortAccountRatio"
BINANCE_PROD_URL="https://api.binance.com"
BINANCE_DEV_URL="https://testnet.binance.vision"
BINANCE_PATH="/api/v3/depth"
+141
View File
@@ -0,0 +1,141 @@
#!/usr/bin/env bash
help_text="\
Get Binance market data.
Usage: get_binance_md.sh [OPTIONS]
Options:
--usds-future Indicates the symbol is a USDS Future
--coin-future Indicates the symbol is a Coin Future
--spot Indicates the symbol is a Spot
--us Indicates the symbol is a Binance US Spot
--prod Use production url
--dev Use development url
--dry Print the command to console without actually running it
--data DEPTH | RECENT_TRADES | HISTORICAL_TRADES | MARK_PRICE | PRICE_TICKER
--<key>=<value> Query parameter (e.g., --symbol=btcusdt, --limit=5)
"
function print_help_and_exit() {
echo "$help_text"
exit "$1"
}
declare this_dir="$(readlink -f $(dirname ${BASH_SOURCE[0]}))"
source "$this_dir/binance.env"
env_name="binance"
venue="BINANCE"
declare -u mode
declare -u data
declare -A query_params
declare -i dry
dry=0
while [[ $# -gt 0 ]]; do
case "$1" in
--usds-future)
venue="BINANCE_USDS_FUTURES"
shift 1
;;
--coin-future)
venue="BINANCE_COIN_FUTURES"
shift 1
;;
--spot)
venue="BINANCE"
shift 1
;;
--us)
venue="BINANCE_US"
shift 1
;;
--prod)
mode="PROD"
shift 1
;;
--dev)
mode="DEV"
shift 1
;;
--data)
if [[ -n "$data" ]]; then
echo "Unexpected argument '$1'"
print_help_and_exit 1
elif [[ -z "$2" ]]; then
echo "Data (--data) must be non-empty"
print_help_and_exit 1
fi
data="$2"
shift 2
;;
--*=*)
key="${1%%=*}"
key="${key#--}"
value="${1#*=}"
if [[ -z "$key" ]]; then
echo "Invalid --key=value arg (key is empty). $1"
print_help_and_exit 1
fi
query_params["$key"]="$value"
shift 1
;;
--dry)
dry=1
shift 1
;;
-h)
;&
--help)
print_help_and_exit 0
;;
*)
if [[ -z "$1" ]]; then
echo "Invalid positional argument '$1'"
print_help_and_exit 1
elif [[ -z "$symbol" ]]; then
symbol="$1"
else
echo "Unexpected argument '$1'"
print_help_and_exit 1
fi
shift 1
;;
esac
done
if [ -z "$mode" ]; then
mode="DEV"
fi
if [ -z "$data" ]; then
data="DEPTH"
fi
base_url_name="${venue}_${mode}_URL"
base_url_name="${venue}_${mode}_URL"
path_name="${venue}_${data}_PATH"
params_name="${venue}_${data}_PARAMS"
if [ ! -f "$this_dir/$env_name.env" ]; then
echo "Env file '$this_dir/$env_name.env' does not exist"
print_help_and_exit 1
fi
for key in "${!query_params[@]}"; do
val=${query_params[$key]}
declare -u keyu
keyu="$key"
eval "$keyu=$val"
done
source "$this_dir/$env_name.env"
params_template=${!params_name}
eval "params=\"$params_template\""
url="${!base_url_name}${!path_name}?${params}"
if [ $dry -ne 0 ]; then
echo curl --get $url
else
curl --get $url
fi