Compare commits

..
34 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
35 changed files with 2125 additions and 1079 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"
]
}
}
}
}
+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);
@@ -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;
}
}
}
}
+1 -1
View File
@@ -50,7 +50,7 @@ namespace mbts.analytics {
return tl_row.call_put == PutOrCall.Call ?
opt_theo.fwd_normalized_call : opt_theo.fwd_normalized_put;
case SecurityType.Future:
return fut_theo.theo;
//return fut_theo.theo;
case SecurityType.Spot:
var bbo = top_of_book;
return (bbo.Item1 + bbo.Item2) / 2;
@@ -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:
@@ -26,7 +26,7 @@ namespace mbts.ui.controls {
public const double
Account = 62,
CheckMark = 42,
Currency = 54,
Currency = 68,
Exchange = 66,
FillKey = 68,
OrderKey = 78,
File diff suppressed because it is too large. Load diff
@@ -72,12 +72,14 @@ namespace Spark.Api.Orders {
// 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();
+3 -3
View File
@@ -1,5 +1,5 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Threading;
@@ -10,9 +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 v2.2.4 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);
@@ -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
}
}
+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;
}
+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:
+12
View File
@@ -682,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;
+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
+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
@@ -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"
@@ -491,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;
@@ -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
@@ -8,6 +8,7 @@
#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"
@@ -36,15 +37,6 @@ bool has_crypto_leg(const std::vector<LegPropertyData> &legs) {
return false;
}
bool has_notional_leg(const std::vector<LegPropertyData> &legs) {
return has_crypto_leg(legs);
// TODO
// for (const auto &l : legs) {
// return l.is_notional();
// }
// return false;
}
struct QtyTickInfo {
public:
const double tick_base;
@@ -76,17 +68,13 @@ struct SpreaderFractionalQtyHelper ::PImpl {
const QtyTickInfo def_qty_tick_info = QtyTickInfo(10000.0, 1.0);
std::map<bts::SecurityKey, QtyTickInfo> qty_info_map = {};
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),
is_notional(has_notional_leg(lpd_vec)) {
// LOG(OrderMan, Info,
// "SpreaderFractionalQtyHelper::cons, in_qty_mult: {} is_notional: {}",
// in_qty_mult, is_notional);
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();
@@ -126,19 +114,26 @@ 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) {
LOG(OrderMan, Info,
LOG(OrderMan, Diag,
"SpreaderFractionalQtyHelper::in_transform_book passthrough");
return bk;
}
if (me->is_notional) {
SpreaderUtils::compute_notional_book(bk, me->op_book);
LOG(OrderMan, Info,
LOG(OrderMan, Diag,
"SpreaderFractionalQtyHelper::in_transform_book sec: {} bk: {} "
"notional: {}",
bk.security_key().value(), bk, me->op_book);
@@ -155,7 +150,7 @@ const Book &SpreaderFractionalQtyHelper::in_transform_book(
ps = bts::md::PriceSize(ps.int_price(), ps.size() * qm);
}
// mult by leg_mult
LOG(OrderMan, Info,
LOG(OrderMan, Diag,
"SpreaderFractionalQtyHelper::in_transform_book bk: {} output: {}", bk,
me->op_book);
return me->op_book;
@@ -177,14 +172,16 @@ SpreaderFractionalQtyHelper::in_transform_order_event(
log_ev(sec_key, ev, ev);
return ev;
}
me->op_ord = 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.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();
me->op_ord = ev;
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);
@@ -236,7 +233,12 @@ Quantity SpreaderFractionalQtyHelper::out_transform_leg_order_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,7 +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 {
@@ -19,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;
+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 v2.2.3-1 launched "
"------------------------------------ Sparkd v2.2.5 launched "
"--------------------------------");
auto qb_cfg = config::get<config::QuickbusCfg>().node();
@@ -374,7 +374,9 @@ struct BinanceSubscription::PImpl {
}
listener->on_securities(sec_ptrs);
batch_subscribe_market_data(sec_descs,4);
// batch_subscribe_market_data(sec_descs,4);
bts_assert(sec_descs.size() < 256);
subscribe_market_data(sec_descs);
}
};
@@ -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