Compare commits

...
Author SHA1 Message Date
John Colvin ff65acba05 fix: Gracefully handle missing column settings in pnl win
Pnl window settings are stored with the workspace on the server (like all shell
window settings), and may originate from an older version of Spark with a
different set of columns. We shouldn't crash if this happens.
2025-08-07 14:33:55 -04:00
John Colvin dc08945719 fix: Check proto HasPrice/QuantityDisplay before accessing props 2025-08-07 14:02:10 -04:00
John Colvin a739ca9a75 fix build: Disable warning in cs Timestamp struct
It is initialized from data stored in shared memory (not a normal struct). The
cs build emits a warning because it detects a field that "is never written to
and will always have a default value of 0."

This code hasn't changed for years. It suddenly becoming a warning-as-error is
likely related to a recent Visual Studio update.
2025-08-07 13:39:21 -04:00
John Colvin ed7b187876 feat: Add ExportToCsv button to fills win 2025-08-07 13:01:06 -04:00
John Colvin 53e1e92a6a fix: Keep existing items in SettingsPersistenceModel filtered sub
Bugs (fixed):
  * SettingsPersistenceModel drops items after initial update in filtered
    subscriptions.

    Cause: The SettingsPersistenceModel subscribe fn only publishes the "added"
    items in each update, and omits pre-existing items. From the observer's
    perspective, pre-existing items appear deleted.
2025-08-05 16:23:02 -04:00
John Colvin eee7af494a fix (fills ui): Preserve sort order during incremental update
Binary search is used to add, remove, and change rows in response to
incremental updates. Needless to say, if the rows become unsorted,
everything breaks.

Bugs (fixed):
  * The fills window crashes sometimes, possibly related to client
    reconnecting to server.

    Cause: This was a 2 part bug.
      1) Changes to existing rows are applied BEFORE the new sort index
         is determined via binary search. If the change affects a
         property in the current sorting criteria, the rows could
         become unsorted and thus invalidate the binary search.

      2) When an existing row is changed so that its new sort index is
         greater than its current one, the new index is not decremented
         by one (it must be since the row is removed before it is
         re-inserted).
2025-08-05 13:47:06 -04:00
Taha Afzal 91a7c35edf Registering Pingpong algo 2025-08-05 15:59:43 +00:00
Taha Afzal 3eaef1a1b6 PingpongAlgo scaffolding 2025-08-05 15:22:53 +00:00
arham 4a70b4910d config: test algos 2025-07-28 13:07:15 +00:00
arham 7697f73ada Fix: change order of plugin loading 2025-07-28 13:03:45 +00:00
arham 029875e0a0 Add: SASALGO 2025-07-28 13:03:26 +00:00
23 changed files with 1462 additions and 16 deletions

No files matched your search

@@ -0,0 +1,6 @@
SPARKD__CONFIG_DIR=src/app_sparkd/configurations/bybit_algo_test
SPARKD__RUNTIME_CONF_DIR=/home/bts/8000/sparkdata/conf
SPARKD__PYTHON_REQUIREMENTS=src/app_sparkd/configurations/alphaworx_zen_hk_live-preview/requirements.txt
SPARKD__SHM_SIZE=8gb
SPARKD__MEMORY=8gb
SPARKD__IMAGE=sparkd_symbols
@@ -0,0 +1,18 @@
{
"currencies": [
"USDT"
],
"futures": [
"MOVE",
"DUCK",
"ETH",
"BTC",
"SOL",
"KAITO"
],
"use_testnet": false,
"logging": {
"console_level": "WARNING",
"file_level": "INFO"
}
}
@@ -0,0 +1,10 @@
certifi==2025.4.26
charset-normalizer==3.4.2
dotenv==0.9.9
idna==3.10
pybit==5.11.0
pycryptodome==3.23.0
python-dotenv==1.1.0
requests==2.32.4
urllib3==2.4.0
websocket-client==1.8.0
@@ -0,0 +1,58 @@
QuickbusCfg {
value {
node {
reconnect_period_secs: 1
hb_period_msecs: 15000
max_missed_hbs: 5
parent_host: ""
parent_port: 0
my_port: 0
}
client {
reconnect_period_secs: 1
hb_period_msecs: 15000
max_missed_hbs: 5
parent_host: @SystemConfig.config_ip
parent_port: @Ports.qbus_port
my_port: 0
}
}
}
JetFIX {
value {
connection {
name: "bybit_1"
venue: VENUE_BYBIT
max_order_msgs_per_window: 300
window_duration_msecs: 1000
}
}
}
MarketDataConfig {
value {
python_md_providers {
venue: VENUE_BYBIT
module_path: "python_interop_plugins"
provider_type: "bybit_md_provider.BybitMdProvider"
}
}
}
Logging {
value {
initial_queue_size: 120001
max_queue_growth: 4
file {
default_level: "Info"
}
}
}
SoundNotifications {
value {
enabled: true
}
}
@@ -0,0 +1,32 @@
SymbolLists {
value {
list {
key: "BybitSpots"
symbol: "MOVE"
symbol: "DUCK"
symbol: "ETH"
symbol: "BTC"
symbol: "SOL"
symbol: "KAITO"
}
list {
key: "BybitFutures"
symbol: "MOVE"
symbol: "DUCK"
symbol: "ETH"
symbol: "BTC"
symbol: "SOL"
symbol: "KAITO"
}
}
}
Symbols {
value {
venues {
name: "VENUE_BYBIT"
spot: @SymbolLists.list[BybitSpots].symbol
future: @SymbolLists.list[BybitFutures].symbol
}
}
}
+2 -2
View File
@@ -141,8 +141,8 @@ namespace bts {
(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;
var priceDisplay = meta.HasValue && meta.Value.HasPriceDisplay ? meta.Value.PriceDisplay : Security.Defaults.price_display_settingsi;
var qtyDisplay = meta.HasValue && meta.Value.HasQuantityDisplay ? meta.Value.QuantityDisplay : Security.Defaults.quantity_display_settingsi;
if (sec.HasContractSpec) {
var pd = priceDisplay.CloneToMutable();
pd.TickFactors = TickUtils.get_tick(sec, 0).to_api_tick_factors();
@@ -103,9 +103,9 @@ namespace mbts.persist {
var re = new Regex(pattern);
return _persist
.subscribe_ns(_user_ns(ns))
.Filter(r => re.IsMatch(r.Key))
.Select(update =>
update.added
.Where(item => re.IsMatch(item.Key))
update.all
.Select(item => new PersistResult<T>() {
user = _user,
ns = ns,
@@ -36,6 +36,7 @@ namespace mbts.spark {
class FillsColumn<TColFilter> : DIYGrid.RenderTextColumn<FillsRowVM, ColKey>, IFillsColumn, IFilterableDIYGridColumn<FillTuple, ColKey, FillsFilterStates> {
readonly Func<FillsFilterStates, TColFilter> _get_col_filter;
readonly Action<FillsFilterStates, TColFilter> _set_col_filter;
readonly Func<FillsRowVM, string> getText;
public FillsColumn(
ColKey key,
@@ -57,16 +58,20 @@ namespace mbts.spark {
_get_col_filter = get_col_filter;
_set_col_filter = set_col_filter;
this.filter_model = filter_model;
getText = get_text;
}
public new IDataGridColumnPersistentFilter<FillTuple, TColFilter> filter_model { get; }
IDataGridColumnFilter<FillTuple> IFilterableDIYGridColumn<FillTuple, ColKey>.filter_model => filter_model;
IDataGridColumnFilter <FillTuple> IFilterableDIYGridColumn<FillTuple, ColKey>.filter_model => filter_model;
IDataGridColumnFilter IFilterableDIYGridColumn.filter_model => filter_model;
ColKey IDIYGridColumn<ColKey>.id => key;
DIYGrid.ColumnBase IFillsColumn.column_view => this;
public string GetText(FillsRowVM row) =>
getText(row);
public FillsFilterStates assign_filter_settings(FillsFilterStates filter_settings) {
_set_col_filter(filter_settings, filter_model.get_state());
return filter_settings;
@@ -17,9 +17,11 @@ namespace mbts.spark {
class FillsOrderTypeColumn<TFilter> : DIYGrid.OrderTypeColumn<ColKey>, IFillsColumn {
readonly Func<FillsFilterStates, TFilter> _get_col_filter;
readonly Action<FillsFilterStates, TFilter> _set_col_filter;
readonly Func<FillsRowVM, string> getText;
public FillsOrderTypeColumn(
ColKey key,
Func<FillsRowVM, string> getText,
BindingBase badge_fill_binding,
Binding display_name_binding,
IDataGridColumnPersistentFilter<FillTuple, TFilter> filter_model,
@@ -27,6 +29,7 @@ namespace mbts.spark {
Action<FillsFilterStates, TFilter> set_col_filter,
string header = "Order Type")
: base(key, badge_fill_binding, display_name_binding, filter_model, header) {
this.getText = getText;
this.filter_model = filter_model;
_get_col_filter = get_col_filter;
_set_col_filter = set_col_filter;
@@ -40,6 +43,9 @@ namespace mbts.spark {
IDataGridColumnFilter IFilterableDIYGridColumn.filter_model => filter_model;
ColKey IDIYGridColumn<ColKey>.id => key;
public string GetText(FillsRowVM r) =>
getText(r);
public FillsFilterStates assign_filter_settings(FillsFilterStates filter_settings) {
_set_col_filter(filter_settings, filter_model.get_state());
return filter_settings;
@@ -24,6 +24,9 @@ namespace mbts.spark {
FillsView.Columns IDIYGridColumn<FillsView.Columns>.id => key;
public string GetText(FillsRowVM row) =>
row.side.ToString();
public FillsFilterStates assign_filter_settings(FillsFilterStates filter_settings) {
filter_settings.side = filter_model.get_state();
return filter_settings;
@@ -30,6 +30,7 @@ namespace mbts.spark {
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Windows.Data;
using System.Windows.Media;
@@ -138,6 +139,7 @@ namespace mbts.spark {
_columns.Add(
new FillsOrderTypeColumn<OrderType[]>(
Columns.OrderType,
r => r.order_type.ToString(),
new Binding(nameof(FillsRowVM.badge_fill)),
new Binding(nameof(FillsRowVM.display_name)),
order_type_filter,
@@ -161,6 +163,7 @@ namespace mbts.spark {
_columns.Add(
new FillsOrderTypeColumn<AlgoID[]>(
Columns.RootAlgoType,
r => r.root_algo_order_type.ToString(),
new Binding(nameof(FillsRowVM.root_algo_badge_color)) {
Converter = new ui.converters.ColorToBrushConverter()
},
@@ -474,6 +477,33 @@ namespace mbts.spark {
public void on_sorting(object sender, DIYGridSortingEventArgs e) =>
_rows_model.on_sorting(e);
public IEnumerable<string> CsvRows(bool includeHeaderRow) {
var cols = _columns.AsCollection().ToArray();
if (cols.Length == 0) {
yield break;
}
var sb = new StringBuilder();
if (includeHeaderRow) {
for (int i = 0; i < cols.Length; i++) {
if (i > 0) {
sb.Append(',');
}
sb.Append(cols[i].id.ToString());
}
yield return sb.ToString();
}
foreach (var r in rows) {
sb.Clear();
for (int i = 0; i < cols.Length; i++) {
if (i > 0) {
sb.Append(',');
}
sb.Append(cols[i].GetText(r));
}
yield return sb.ToString();
}
}
static IObservable<TableUpdate<FillTuple, string>> _get_observable(
OrdersModel model, FillsSettings state) {
var orders_hash = state.orders?.ToHashSet();
@@ -17,6 +17,7 @@ using Spark.Core.Disposables;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Windows;
@@ -91,6 +92,7 @@ namespace mbts.spark {
has_selected_rows_src,
_ => _,
_ => _vm.selected_rows.Select(r => r.security_key).ToImmutableHashSet()),
new SparkCommandCommand("Export to CSV...", Icons.Disk, FileDialogCommand.ExportToCsv(ExportToCsv)),
// wrap the grid settings view factory in a dialogCommand here since the vm is destroyed/recreated when the security criteria changes
SparkCommands.window_settings_command(new DialogCommand(() => {
var vm = new ShellSettingsDialogVM<GridSettings, SimpleGridSettingsVM>(
@@ -128,6 +130,14 @@ namespace mbts.spark {
// show_ladder_cmd,
// SparkCommands.KeyGestures.ShowLadder);
// }
void ExportToCsv(string filePath) {
using var fs = File.OpenWrite(filePath);
using var sw = new StreamWriter(fs);
foreach (var row in _vm.CsvRows(true)) {
sw.WriteLine(row);
}
}
}
protected override void OnParentViewCreated(SparkWindowView parent_view) =>
@@ -8,6 +8,7 @@ namespace mbts.spark {
interface IFillsColumn : IFilterableDIYGridColumn<FillTuple, FillsView.Columns, FillsFilterStates>, ISortableDIYGridColumn<FillsView.Columns> {
DIYGrid.ColumnBase column_view { get; }
string GetText(FillsRowVM row);
}
static class FillsColumnExtensions {
@@ -28,7 +28,9 @@ namespace bts.shell.pnl {
foreach (var t in _triggers) {
foreach (var kv in t.trigger(data)) {
if (!bd.TryGetValue(kv.Key, out var app)) {
app = _defaults[kv.Key];
if (!_defaults.TryGetValue(kv.Key, out app)) {
continue;
}
}
app = kv.Value.merge(app);
bd[kv.Key] = app;
@@ -15,6 +15,19 @@ namespace mbts.ui {
public static IDialogCommand open_file(Action<string> execute, Func<bool> can_execute, string initial_dir, params (string name, string[] extensions)[] extensions) =>
new AnonymousFileDialogCommand<object>(_ => _create_open_file_dialog(null, extensions), (_, file) => execute(file), _ => can_execute());
public static IDialogCommand ExportToCsv(Action<string> execute, Func<bool> canExecute = null, string initialDir = null, string initialName = null) {
return new AnonymousFileDialogCommand<object>(DialogFactory, Execute, CanExecute);
SaveFileDialog DialogFactory(object _) =>
_create_save_file_dialog(initialDir, initialName, new[] { ("Excel", new[] { "csv" }) });
void Execute(object _, string file) =>
execute(file);
bool CanExecute(object _) =>
canExecute?.Invoke() ?? true;
}
public static IDialogCommand export_proto<TProto>(
Func<TProto> get_proto,
Func<bool> can_execute = null,
@@ -66,7 +79,7 @@ namespace mbts.ui {
}
}, can_execute == null ? null : new Func<object, bool>(_ => can_execute()));
static TDialog _create_file_dialog<TDialog>(string initial_dir = null, IReadOnlyList<(string name, string[] extensions)> filter_items = null) where TDialog : FileDialog, new() {
public static TDialog CreateFileDialog<TDialog>(string initial_dir = null, IReadOnlyList<(string name, string[] extensions)> filter_items = null) where TDialog : FileDialog, new() {
var rv = new TDialog();
if (!string.IsNullOrEmpty(initial_dir))
rv.InitialDirectory = initial_dir;
@@ -78,7 +91,7 @@ namespace mbts.ui {
}
static SaveFileDialog _create_save_file_dialog(string initial_dir = null, string initial_name = null, IReadOnlyList<(string name, string[] extensions)> filter_items = null) {
var rv = _create_file_dialog<SaveFileDialog>(initial_dir, filter_items);
var rv = CreateFileDialog<SaveFileDialog>(initial_dir, filter_items);
rv.OverwritePrompt = true;
rv.AddExtension = true;
if (!string.IsNullOrEmpty(initial_name))
@@ -87,7 +100,7 @@ namespace mbts.ui {
}
static OpenFileDialog _create_open_file_dialog(string initial_dir = null, IReadOnlyList<(string name, string[] extensions)> filter_items = null) =>
_create_file_dialog<OpenFileDialog>(initial_dir, filter_items);
CreateFileDialog<OpenFileDialog>(initial_dir, filter_items);
static string _build_filter(IEnumerable<(string name, string[] extensions)> items) =>
string.Join("|", items.Select(i => {
@@ -240,16 +240,18 @@ namespace mbts.ui.utility {
wrapped_models.InsertSorted(table.state.Values, comp);
return;
} else {
var view_item = update(target[pos], ch.New);
int new_pos = CollectionHelper.binary_search(wrapped_models, ch.New, comp);
var view_item_state = update(target[pos], ch.New);
if (new_pos < 0)
new_pos = ~new_pos;
var view_item = update(target[pos], ch.New);
if (Math.Abs(new_pos - pos) > 1) {
// If the abs diff between old & new positions is 1 (or 0),
// the item does not need to be moved because its position wrt the other elements is unchanged.
var state = on_moving == null ? default : on_moving(view_item);
target.RemoveAt(pos);
if (new_pos > pos) {
new_pos--;
}
target.Insert(new_pos, view_item);
on_moved?.Invoke(view_item, state);
}
@@ -2,7 +2,9 @@ namespace mbts.co2 {
using System.Threading;
struct TimeStamp {
#pragma warning disable CS0649
long _value;
#pragma warning restore CS0649
public static ulong invalid_value => ulong.MaxValue;
public ulong time => (ulong)Interlocked.Read(ref _value);
@@ -0,0 +1,102 @@
#include "PingpongAlgo.h"
#include "spark/domain_model/Security.h"
#include "spark/orderman/order_factory/IOrderFactory.h"
#include "spark/orderman/orderman_utils/Book.h"
#include "spark/orderman/orderman_utils/orderman_utils.h"
#include "spark/orderman/orders/algo/IAlgoServices.h"
#include "spark/orderman/orders/spreader_orders/spreader/SpreaderUtils.h"
#include "spark/os/os.h"
#include "spark/utils/PBUtils.h"
#include "spark/utils/datetime.h"
#include <stdint.h>
namespace bts {
namespace orderman {
using namespace api::orderman;
enum class PingpongAlgo::Status { WAITING_FOR_SIGNAL, WORKING, DONE };
struct PingpongAlgo::PImpl {
Status state = Status::WAITING_FOR_SIGNAL;
IAlgoServices *algo_services = nullptr;
SecurityKey sec_key = SecurityKey(0);
//PingpongAlgo::PingpongAlgoParams algo_params;
};
PingpongAlgo::PingpongAlgo() : me(new PImpl()) {}
PingpongAlgo::~PingpongAlgo() {}
// initialization /////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
OrderOpResult PingpongAlgo::do_before_launch(
IBeforeLaunchAlgoServices & /*serv*/) {
return {OrderError::ORDER_ERROR_OK, std::string("OK")};
}
void PingpongAlgo::do_launch(IAlgoServices &algo_services) {
me->algo_services = &algo_services;
_algo().subscribe_status(me->sec_key);
//_algo().subscribe_timer();
}
void PingpongAlgo::do_launch_attached(
IAlgoServices &algo_services, OrderKey, ChildState) {
me->algo_services = &algo_services;
_cxl_and_end(true, "TAS Algo can not be attached to working orders");
}
// updates ////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void PingpongAlgo::do_update(const api::orderman::Update &) {
LOG(OrderMan, Warn,
"PingpongAlgo::do_update order:{} state:{} update not allowed ",
_algo().order_key().value(), static_cast<int>(me->state));
}
// callbacks //////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////
void PingpongAlgo::on_book(SecurityKey, const Book &) {
// find the mean and std dev. for std dev, we'll most likely have to keep a sliding window of the
// bids and asks, perhaps a std::vector<std::pair<double, double>> and we recompute it everytime.
// we put buy and sell open pos orders at μ + ol.σ and μ + os.σ respectively
// (ol will be < 0, for example when ol = -2, our buy order will go at μ - 2σ).
// when the open long pos order gets filled, we send the close long position order at μ + cl.σ.
// when the buy open pos order gets filled, we send the close long position order at μ + cl.σ.
// similiarly for short.
}
void PingpongAlgo::on_child_change(OrderKey, ChildState const &) {}
void PingpongAlgo::on_status(SecurityKey, co2::TradingStatus) {}
void PingpongAlgo::on_timer() {}
IAlgoServices &PingpongAlgo::_algo() const { return *me->algo_services; }
///////////////////////////////////////////////////////////////////////////////
ChildTemplate *PingpongAlgo::_create_order_template(
IBeforeLaunchAlgoServices &, SecurityKey, api::Side) {
return nullptr;
}
void PingpongAlgo::_switch_state(PingpongAlgo::Status new_state) {
switch (new_state) {
case (PingpongAlgo::Status::WAITING_FOR_SIGNAL):
bts_assert(false);
break;
case (PingpongAlgo::Status::WORKING):
bts_assert(me->state == PingpongAlgo::Status::WAITING_FOR_SIGNAL);
break;
case (PingpongAlgo::Status::DONE):
bts_assert(me->state != PingpongAlgo::Status::DONE);
break;
}
LOG(OrderMan, Info, "PingpongAlgo::_switch_state key:{} {}->{} ",
_algo().order_key(), static_cast<int>(me->state),
static_cast<int>(new_state));
me->state = new_state;
}
void PingpongAlgo::_cxl_and_end(bool is_error, const std::string &err_msg) {
if (is_error)
_algo().terminate_error(err_msg);
else
_algo().complete();
_switch_state(PingpongAlgo::Status::DONE);
}
} // namespace orderman
} // namespace bts
@@ -0,0 +1,38 @@
#pragma once
#include "spark/orderman/orders/algo/IAlgoOrder.h"
namespace bts::orderman {
class ChildTemplate;
class PingpongAlgo : public IAlgoOrder {
public:
PingpongAlgo();
~PingpongAlgo();
public:
OrderOpResult do_before_launch(IBeforeLaunchAlgoServices &) override;
void do_launch(IAlgoServices &) override;
void do_launch_attached(IAlgoServices &, OrderKey, ChildState) override;
void do_update(api::orderman::Update const &) override;
void on_book(SecurityKey, Book const &) override;
void on_child_change(OrderKey, ChildState const &) override;
void on_status(SecurityKey, co2::TradingStatus) override;
void on_timer() override;
private:
enum class Status;
struct PImpl;
PImpl *me;
//std::unique_ptr<PImpl> me;
private:
IAlgoServices &_algo() const;
static ChildTemplate *_create_order_template(
IBeforeLaunchAlgoServices &, SecurityKey, api::Side);
// acts on md, cognizant of the state
void _switch_state(Status new_status);
void _cxl_and_end(bool is_error, const std::string &err_msg);
};
} // namespace bts::orderman
@@ -1,4 +1,3 @@
#include "DormanAlgo.h"
#include "OnSignalAlgo.h"
#include "PyramidAlgo.h"
@@ -11,6 +10,7 @@
#include "spark/config/protos/toggles.pb.h"
#include "spark/orderman/orderman_utils/orderman_utils.h"
#include "spark/orderman/orders/algo/algo_orders/LimitCoverAlgo.h"
#include "spark/orderman/orders/algo/algo_orders/SprayAndSkewAlgo.h"
#include "spark/orderman/orders/algo/algo_orders/TaskingEyeAlgo.h"
#include "spark/orderman/orders/algo/algo_orders/VolAlgo.h"
#include "spark/orderman/orders/algo/algo_orders/limit_cover/LimitCoverAdapter.h"
@@ -238,11 +238,11 @@ extern "C" void init_quoting_algo_plugin(bts::SparkServices &srv) {
quoting_algo.set_dynamic_order_type("QuotingAlgo");
*quoting_algo.add_param_desc() = param_int32("n_ticks");
*quoting_algo.add_param_desc() = param_order("child_order");
srv.register_dynamic_algo_order(
quoting_algo, []() { return std::make_unique<orderman::QuotingAlgo>(); });
srv.register_dynamic_algo_order(quoting_algo, []() {
return std::make_unique<orderman::QuotingAlgo>();
});
}
extern "C" void init_on_signal_plugin(bts::SparkServices &srv) {
DynamicOrderDescriptor on_signal;
on_signal.set_dynamic_order_type("OnSignal");
@@ -307,6 +307,25 @@ extern "C" void init_execution_tester_plugin(bts::SparkServices &srv) {
return std::make_unique<orderman::ExecutionTesterAlgo>();
});
}
extern "C" void init_spray_and_skew_plugin(bts::SparkServices &srv) {
DynamicOrderDescriptor spray_and_skew;
spray_and_skew.set_dynamic_order_type("Spray and Skew");
// contracts
*spray_and_skew.add_param_desc() = param_double("Half Spread");
*spray_and_skew.add_param_desc() = param_double("Skew");
*spray_and_skew.add_param_desc() = param_double("C1");
*spray_and_skew.add_param_desc() = param_uint32("Looking Depth Levels");
*spray_and_skew.add_param_desc() = param_uint32("Window");
*spray_and_skew.add_param_desc() = param_double("Interval Sec");
*spray_and_skew.add_param_desc() = param_uint32("Grid Num");
*spray_and_skew.add_param_desc() = param_double("Grid Interval");
*spray_and_skew.add_param_desc() = param_double("Order Size");
*spray_and_skew.add_param_desc() = param_double("Max Position Dollar");
srv.register_dynamic_algo_order(spray_and_skew, []() {
return std::make_unique<orderman::SprayAndSkewAlgo>();
});
}
extern "C" void init_task_eye_plugin(bts::SparkServices &srv) {
DynamicOrderDescriptor tasking_eye_algo;
@@ -350,6 +369,22 @@ extern "C" void init_vol_algo_plugin(bts::SparkServices &srv) {
vol_algo, []() { return std::make_unique<orderman::VolAlgo>(); });
}
extern "C" void init_pingpong_plugin(bts::SparkServices &srv) {
DynamicOrderDescriptor pingpong_algo;
pingpong_algo.set_dynamic_order_type("Pingpong");
// μ + ol.σ
*pingpong_algo.add_param_desc() = param_double("Open Long Pos");
//μ + cl.σ
*pingpong_algo.add_param_desc() = param_double("Close Long Pos");
// μ + os.σ
*pingpong_algo.add_param_desc() = param_double("Open Short Pos");
// μ + cs.σ
*pingpong_algo.add_param_desc() = param_double("Close Short Pos");
*pingpong_algo.add_param_desc() = param_uint32("Sliding Window Seconds");
srv.register_dynamic_algo_order(
pingpong_algo, []() { return std::make_unique<orderman::VolAlgo>(); });
}
extern "C" void init_spark_plugin(bts::SparkServices &srv) {
auto &dyn_algo_config = config::get<config::toggles::DynamicAlgos>();
if (dyn_algo_config.enable_dorman())
@@ -369,9 +404,11 @@ extern "C" void init_spark_plugin(bts::SparkServices &srv) {
if (dyn_algo_config.enable_execution_tester())
init_execution_tester_plugin(srv);
// init_sniper_plugin(srv);
init_spray_and_skew_plugin(srv);
init_task_eye_plugin(srv);
init_vol_algo_plugin(srv);
init_quoting_algo_plugin(srv);
init_pingpong_plugin(srv);
}
} // namespace dorman
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,57 @@
#pragma once
#include "spark/domain_model/SecurityKey.h"
#include "spark/domain_model/common.h"
#include "spark/orderman/orders/algo/IAlgoOrder.h"
#include <openssl/ossl_typ.h>
namespace bts::orderman {
/*
Spray tiny limit orders on both sides of the book every 500 ms (here via obi_mm), inside a very tight half-spread (≈1 bp).
Lean (skew) the quotes away from your inventory—if buys fill and you're long, pull bids a tick lower and lift offers a tick higher—so the position mean-reverts instead of snowballing.
Because fills come to you passively, each trade earns the maker rebate (-0.005 %), and the micro-edge from skewing usually caps adverse selection; over thousands of fills the rebates > tiny pnl bleed, so the net is positive even if price action is noise.
The minimal loop looks like:
while hbt.elapse(0.5e9) == 0: # every 0.5 s
mid = (best_bid + best_ask)/2
fair = mid + c1 * alpha # alpha = std-score of OBI std score should be normalized between 1 and -1, c1 would be something lik 1 or 10
rp = fair - skew * (pos/qty) # inventory-adjusted reservation price, skew 1 and 10, dividing by quanity to make sure in ticks
bid = max(np.floor(rp - half_spread), best_bid) #half_spreads 50 or 100 ticks number of ticks
ask = min(np.ceil (rp + half_spread), best_ask)
make_grid(bid, ask, qty) # post / cancel as in obi_mm
max_position_dollar # set as parameter, max position you can hold in terms of dollars and if this is met hold everything on that side keep algo running, algo max position in terms of contracts
cancel and update:
on update
0.005%
That's all: churn volume, stay flat, clip rebates. Tweak half_spread, skew, and order_qty until the "rebate pnl" outweighs fees and slippage.*/
struct custom_state;
class SprayAndSkewAlgo : public IAlgoOrder {
public:
SprayAndSkewAlgo();
~SprayAndSkewAlgo();
OrderOpResult do_before_launch(IBeforeLaunchAlgoServices &) override;
void on_book(SecurityKey, Book const &) override;
void do_launch(IAlgoServices &) override;
void do_launch_attached(IAlgoServices &, OrderKey, ChildState) override;
void do_update(api::orderman::Update const &) override;
void on_child_change(OrderKey, ChildState const &) override;
void on_status(SecurityKey, co2::TradingStatus) override;
void on_timer() override;
void on_position(PositionUpdate const &) override;
double get_rp(
double stddev, double imbalance, double mean, double mid);
private:
struct PImpl;
std::unique_ptr<PImpl> me;
private:
void _terminate_algo(bool is_error, const std::string &err_msg);
void update_custom_state(const custom_state &cs);
void timed_handler();
};
} // namespace bts::orderman
+1 -1
View File
@@ -22,6 +22,7 @@ void init_plugins(SparkServices &services) {
boost::filesystem::path(), ""))>
keep_alive;
init_spark_plugin(services);
auto plugin_path = os::sparkd_wd() + "/plugins/";
if (!boost::filesystem::is_directory(plugin_path)) {
LOG(Plugin, Info,
@@ -31,7 +32,6 @@ void init_plugins(SparkServices &services) {
}
// initialize the plugins in libspark.so
init_spark_plugin(services);
boost::filesystem::directory_iterator end;
for (auto iter = boost::filesystem::directory_iterator(plugin_path);