refactor: Consolidate all py interop into single lib

It was just too hard trying to make it work with separate libs. The
main issue was getting the python md and execution modules to
recognize PySecurityList and other types defined in the core module.
This commit is contained in:
John Colvin committed 2025-06-16 11:08:57 -04:00
1 parent 7c9330650d
commit bdd5e08b60
87 files changed
+1093 -2926

No files matched your search

@@ -0,0 +1,313 @@
import json
import time
from typing import Optional
import uuid
from pybit.unified_trading import WebSocket, WebSocketTrading
from py_execution_utils import load_gateway_config
from spark_core import (
as_bool,
configure_logger,
format_price,
format_quantity,
parse_float,
StructuredLog as SLog,
)
import logging
from python_interop_module import (
ExecutionGateway,
ExecutionGatewayBridge,
PyCreateOrderRequest,
PyCancelReplaceOrderRequest,
PyCancelOrderRequest,
PyExecutionReport,
PySecurity,
PySecurityList,
)
import threading
class OrderData:
def __init__(self, bts_id: int, exch_id: str, price: float, quantity: float):
self.bts_id = bts_id
self.exch_id = exch_id
self.price = price
self.quantity = quantity
self.filled_quantity: float = 0
self.last_filled_quantity: float = 0
self.avg_filled_price: float = 0
self.last_filled_price: float = 0
self.exec_type = "PENDING_NEW"
def on_update(
self,
price: float,
quantity: float,
filled_quantity: float,
avg_filled_price: float,
status: str,
):
self.price = price
self.filled_quantity = 0
self.quantity = quantity
lfq = filled_quantity - self.filled_quantity
if lfq > 0:
if self.filled_quantity == 0:
self.last_filled_quantity = filled_quantity
self.last_filled_price = avg_filled_price
else:
price_diff = avg_filled_price - self.avg_filled_price
lfq_weight = filled_quantity / (self.filled_quantity + filled_quantity)
lfp = self.avg_filled_price + (price_diff / lfq_weight)
self.last_filled_price = lfp
self.last_filled_quantity = lfq
self.filled_quantity = filled_quantity
self.avg_filled_price = avg_filled_price
match status:
case "Filled":
self.exec_type = "FILL"
case "Canceled":
self.exec_type = "CANCELED"
case "Rejected":
self.exec_type = "REJECTED"
case _:
self.exec_type = "NEW" if filled_quantity == 0 else "PARTIAL_FILL"
class BybitExecutionGateway(ExecutionGateway):
def __init__(self, name: str):
super(BybitExecutionGateway, self).__init__()
self.name = name
self.logger = configure_logger(logging.getLogger(name), "OrderMan")
self.logger.info("[bybit_gw_ctr.json]%s" % SLog(name=name))
cfg = load_gateway_config(name)
self.account: str = cfg["account"]
self.api_key: str = cfg["api_key"]
self.secret_key: str = cfg["secret_key"]
self.use_testnet: bool = cfg.get("use_testnet", True)
self.use_trace_logging: bool = cfg.get("use_trace_logging", False)
self.stop_event = threading.Event()
self.stream_cli: Optional[WebSocket] = None
self.trade_cli: Optional[WebSocketTrading] = None
# bts id -> exch id
self.orders: dict[int, OrderData] = {}
self.securities: dict[str, PySecurity] = {}
def _get_order_data(self, exch_id: str):
for _, v in self.orders.items():
if v.exch_id == exch_id:
return v
return None
def _on_order_ack(self, typ, msg: object):
self.logger.info(
'[bybit_gw_ord_ack.json]{ "typ": "%s", "msg": %s}' % (typ, msg)
)
def _on_abort(
self,
key: int,
req_type: str,
rsn: str,
):
self.logger.error(
'[bybit_gw_abort.json]{ "key": %d, "typ": "%s", "rsn": "%s", "ts": %d }'
% (key, req_type, rsn, time.time_ns())
)
def start_impl(self, bridge: ExecutionGatewayBridge):
def on_order_update(msg: dict):
def on_error(err: str):
self.logger.error(
'[bybit_gw_ord_upd_err.json]{ "err": "%s", "ts": %d }'
% (err, time.time_ns())
)
self.logger.info("[bybit_gw_ord_upd.json]%s" % msg)
try:
upds: list[dict] = msg["data"]
for d in upds:
er = PyExecutionReport()
exch_id: str = d["orderLinkId"]
ord_data: Optional[OrderData] = self._get_order_data(exch_id)
if not ord_data:
on_error("Did not resolve exch order id %s" % exch_id)
return
total_filled_qty = parse_float(d["cumExecQty"])
avg_filled_price = parse_float(d["avgPrice"])
status: str = d["orderStatus"]
# brits can't spell
if status == "Cancelled":
status = "Canceled"
exec_type = ord_data.exec_type
match ord_data.exec_type:
case "PARTIAL_FILL":
exec_type = "PARTIALLY_FILLED"
case _:
pass
px = parse_float(d["price"])
qty = parse_float(d["qty"])
ord_data.on_update(
px, qty, total_filled_qty, avg_filled_price, status
)
transact_time = int(d["updatedTime"]) * 1_000_000
er.set_data(
key=ord_data.bts_id,
exchange_order_id=d["orderLinkId"],
venue="BYBIT",
security_type="FUTURE",
exchange_security_id=d["symbol"],
account=self.account,
order_type=d["orderType"],
is_buy=d["side"] == "Buy",
price=px,
quantity=qty,
time_in_force=d["timeInForce"],
exec_type=exec_type,
order_status=status,
last_filled_price=ord_data.last_filled_price,
last_filled_quantity=ord_data.last_filled_quantity,
filled_quantity=total_filled_qty,
avg_filled_price=avg_filled_price,
# There is no transact_id in Bybit, so we use updatedTime
transact_id=str(transact_time),
transact_time=transact_time,
)
self.logger.info("[bybit_gw_exe_rep_out.json]%s" % er)
bridge.on_execution_report(er)
except Exception as e:
self.logger.error(
'[bybit_gw_ord_upd_err.json]{ "err": %s }' % e, exc_info=True
)
def on_exe_report(msg):
self.logger.info("[bybit_gw_exe_rep.json]%s" % msg)
# Stream client (receives order updates)
params = {
"api_key": self.api_key,
"api_secret": self.secret_key,
"testnet": self.use_testnet,
"channel_type": "private",
"trace_logging": False,
}
self.logger.info("[bybit_gw_stream_sub.json]%s" % json.dumps(params))
self.stream_cli = WebSocket(**params)
self.stream_cli.order_stream(on_order_update)
self.stream_cli.execution_stream(on_exe_report)
# Trade client (sends orders)
params = {
"api_key": self.api_key,
"api_secret": self.secret_key,
"testnet": True,
}
self.logger.info("[bybit_gw_start_trd_cli.json]%s" % json.dumps(params))
self.trade_cli = WebSocketTrading(**params)
params = {
"category": "linear",
}
self.bridge.on_is_ready(True)
def start(self, bridge: ExecutionGatewayBridge):
def start_wrapper():
try:
self.start_impl(bridge)
except Exception as e:
self.logger.error(
'[bybit_gw_start_err.json]{ "err": "%s" }' % str(e), exc_info=True
)
self.logger.info("[bybit_gw_start.json]%s" % json.dumps({"ts": time.time_ns()}))
self.bridge = bridge
threading.Thread(
target=start_wrapper,
name=f"{self.name}_main",
daemon=True,
).start()
def stop(self):
self.logger.info("[bybit_gw_stop.json]%s" % json.dumps({"ts": time.time_ns()}))
self.stop_event.set()
def create_order(self, req: PyCreateOrderRequest):
self.logger.info("[bybit_gw_crt_ord.json]%s" % str(req))
if not self.stream_cli or not self.trade_cli or not self.bridge:
self._on_abort(
req.key(),
"crt",
"WebSocket not initialized (stream_cli: %s, trade_cli: %s, bridge: %s)"
% as_bool(self.stream_cli, self.trade_cli, self.bridge),
)
return
exch_ord_id = uuid.uuid1().hex
bts_id = req.key()
px = req.price()
qty = req.quantity()
self.orders[req.key()] = OrderData(
bts_id=bts_id, exch_id=exch_ord_id, price=px, quantity=qty
)
sec_name = req.exchange_security_id()
sec = self.securities.get(sec_name)
if not sec:
return
params = {
"orderLinkId": exch_ord_id,
"category": "linear",
"symbol": sec_name,
"side": "Buy" if req.is_buy() else "Sell",
"orderType": req.order_type().title(),
"price": format_price(sec, px),
"qty": format_quantity(sec, qty),
}
self.logger.info("[bybit_gw_crt_ord_send.json]%s" % json.dumps(params))
self.trade_cli.place_order(lambda msg: self._on_order_ack("crt", msg), **params)
def cancel_replace_order(self, req: PyCancelReplaceOrderRequest):
self.logger.info("[bybit_gw_cxlrep_ord.json]%s" % req)
if not self.stream_cli or not self.trade_cli:
self._on_abort(req.key(), "cxlrep", "WebSocket client not created.")
return
bts_id = req.key()
ord_data = self.orders.get(bts_id)
if not ord_data:
self._on_abort(req.key(), "cxlrep", "Did not resolve exch order id")
return
sec_name = req.exchange_security_id()
sec = self.securities.get(sec_name)
if not sec:
return
params = {
"orderLinkId": ord_data.exch_id,
"category": "linear",
"symbol": req.exchange_security_id(),
"price": format_price(sec, req.price()),
"qty": format_quantity(sec, req.quantity()),
}
self.trade_cli.amend_order(
lambda msg: self._on_order_ack("cxlrep", msg), **params
)
def cancel_order(self, req: PyCancelOrderRequest):
self.logger.info("[bybit_gw_cxl_ord.json]%s" % str(req))
if not self.stream_cli or not self.trade_cli:
self._on_abort(req.key(), "cxl", "WebSocket client not created.")
return
bts_id = req.key()
ord_data = self.orders.get(bts_id)
if not ord_data:
self._on_abort(req.key(), "cxl", "Did not resolve exch order id")
return
params = {
"orderLinkId": ord_data.exch_id,
"category": "linear",
"symbol": req.exchange_security_id(),
}
self.trade_cli.cancel_order(
lambda msg: self._on_order_ack("cxl", msg), **params
)
def on_securities(self, securities: PySecurityList):
self.logger.info(
"[bybit_gw_sec.json][%s]" % ", ".join([str(sec) for sec in securities])
)
self.securities = {sec.name(): sec for sec in securities}
@@ -0,0 +1,263 @@
import json
import numpy as np
import time
import logging
import sys
import threading
import signal
from typing import Optional
from pybit.unified_trading import WebSocket
from python_interop_module import (
MdBridge,
MdProvider,
PyBook,
PyBookList,
PySecurityList,
)
from bybit_conf import CONTRACTS
from spark_core import (
configure_logger,
get_contract_spec,
get_price_info,
get_quantity_info,
get_security,
)
from py_md_utils import PERP_EXP_STRING
import requests
logger = logging.getLogger("bybit_md_provider")
configure_logger(logger, "md")
def normalize_contract_name(contract: str) -> str:
if len(contract) > 4:
return contract[:-4] + "_" + contract[-4:]
return contract
def fetch_contract_specs() -> dict:
default_specs = {
"tick_unit": 1,
"tick_base": 10,
"cash_per_point": 0.1,
"qty_tick_unit": 1,
"qty_tick_base": 10000
}
specs_dict = {contract: default_specs for contract in CONTRACTS}
try:
url = "https://api.bybit.com/v5/market/instruments-info?category=linear"
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
found_contracts = set()
for item in data.get("result", {}).get("list", []):
contract = item.get("symbol")
if contract in CONTRACTS:
price_tick = float(item.get("priceFilter", {}).get("tickSize", "0.1"))
lot_size = float(item.get("lotSizeFilter", {}).get("qtyStep", "0.001"))
tick_base = int(1 / price_tick) if price_tick != 0 else 10
if lot_size >= 1:
qty_tick_base = int(lot_size)
else:
qty_tick_base = int(1 / lot_size) if lot_size != 0 else 10000
specs_dict[contract] = {
"tick_unit": 1,
"tick_base": tick_base,
"cash_per_point": price_tick,
"qty_tick_unit": 1,
"qty_tick_base": qty_tick_base
}
found_contracts.add(contract)
for contract in CONTRACTS:
if contract not in found_contracts:
logger.warning(f"Contract {contract} not found in API response, using default specs")
return specs_dict
except Exception as e:
logger.error(f"Failed to fetch contract specs: {e}", exc_info=True)
return specs_dict
class BybitMdProvider(MdProvider):
def __init__(self):
super().__init__()
self.ws = None
self.ws_thread: Optional[threading.Thread] = None
self.bridge: Optional[MdBridge] = None
self.stop_event = threading.Event()
self._setup_signal_handlers()
def _setup_signal_handlers(self):
"""Set up signal handlers for SIGINT, SIGTERM, and SIGTSTP."""
def signal_handler(sig, frame):
logger.info(f"Received signal {sig}, stopping...")
self.stop()
signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGTSTP, signal_handler)
def start_impl(self, bridge: MdBridge):
self.bridge = bridge
logger.info("Starting BybitMdProvider")
contract_specs = fetch_contract_specs()
sec_list = PySecurityList()
for contract in CONTRACTS:
normalized_contract = normalize_contract_name(contract)
asset, quote = normalized_contract.split("_")
specs = contract_specs.get(contract, {})
try:
qty_info = get_quantity_info(
tick_unit=specs.get("qty_tick_unit", 1),
tick_base=specs.get("qty_tick_base", 10000)
)
except Exception as e:
logger.error(f"Failed to get quantity info for contract: {contract}, error: {e}")
continue
try:
spec = get_contract_spec(
price_info=get_price_info(
tick_unit=specs.get("tick_unit", 1),
tick_base=specs.get("tick_base", 10),
cash_per_point=specs.get("cash_per_point", 0.1)
),
qty_info=qty_info
)
except Exception as e:
logger.error(f"Failed to get contract spec for contract: {contract}, error: {e}")
continue
try:
sec = get_security(
venue="Bybit",
name=contract,
asset=asset,
group=asset,
currency=quote,
expiration=PERP_EXP_STRING,
option_kind=None,
strike=None,
spec=spec,
)
sec_list.append(sec)
logger.info(f"Created security: {contract} with specs: {specs}")
except Exception as e:
logger.error(f"Failed to create security for contract: {contract}, error: {e}")
continue
bridge.on_securities(sec_list)
logger.info("Sent securities list--bridge.on_securities called.")
try:
self.ws = WebSocket(
testnet=False,
channel_type="linear",
ping_interval=20,
)
logger.info("Connecting to Bybit WebSocket (mainnet, linear)")
for contract in CONTRACTS:
try:
self.ws.orderbook_stream(
symbol=contract,
depth=50,
callback=self._on_book_update
)
logger.info(f"Subscribed to orderbook.50.{contract}")
except Exception as e:
logger.error(f"Failed to subscribe to orderbook for {contract}, error: {e}")
logger.info("Subscribed to orderbook channels on Bybit unified trading WS.")
for t in threading.enumerate():
logger.debug(f"Thread (start): Name={t.name}, Alive={t.is_alive()}, Daemon={t.daemon}, Target={t._target}")
while not self.stop_event.is_set():
time.sleep(0.05)
except KeyboardInterrupt:
logger.info("KeyboardInterrupt received — shutting down.")
self.stop()
except Exception as e:
logger.error(f"Bybit WebSocket fatal error: {e}", exc_info=True)
self.stop()
def start(self, bridge: MdBridge):
def start_wrapper():
try:
self.start_impl(bridge)
except Exception as e:
logger.error(f"Error in BybitMdProvider start_impl: {e}", exc_info=True)
self.ws_thread = threading.Thread(target=start_wrapper)
self.ws_thread.start()
def _on_book_update(self, data: dict):
try:
topic = data.get("topic", "")
if not topic.startswith("orderbook.50."):
return
contract = topic.split(".")[-1]
payload = data.get("data", {})
raw_bids = [(float(x[0]), float(x[1])) for x in payload.get("b", [])]
raw_asks = [(float(x[0]), float(x[1])) for x in payload.get("a", [])]
# Sort and take top 5, padding with [float('nan'), float('nan')] if needed
sorted_bids = sorted(raw_bids, key=lambda x: -x[0])[:5]
sorted_bids += [(np.float64('nan'), np.float64('nan'))]
sorted_asks = sorted(raw_asks, key=lambda x: x[0])[:5]
sorted_asks += [(np.float64('nan'), np.float64('nan'))]
logger.debug(f"📘 BOOK DATA for {contract}: bids={sorted_bids} asks={sorted_asks}")
book = PyBook()
book.set_data(
contract,
"Bybit",
"FUTURE",
np.array(sorted_bids, dtype=np.float64),
np.array(sorted_asks, dtype=np.float64),
)
book_list = PyBookList()
book_list.append(book)
if self.bridge:
self.bridge.on_books(book_list)
logger.debug(f"✔ on_books called for {contract}")
else:
logger.warning("Bridge is None, cannot call on_books")
except Exception as e:
logger.error(f"Error processing book update for {contract}: {e}", exc_info=True)
def stop(self):
logger.info("Stopping BybitMdProvider...")
self.stop_event.set()
logger.debug("Threads before stopping WebSocket:")
for t in threading.enumerate():
logger.debug(f"Thread(stop): {t.name}, Alive: {t.is_alive()}, Daemon: {t.daemon}, Target={t._target}")
if self.ws:
try:
self.ws.exit()
logger.info("WebSocket exit called")
# Clean up pybit threads with longer timeout
for t in threading.enumerate():
if t.name in ['Thread-1 (<lambda>)', 'Thread-2 (_send_ping)'] and t.is_alive():
logger.info(f"Joining pybit thread: {t.name}")
t.join(timeout=1.0)
except Exception as e:
logger.error(f"Error while closing WebSocket: {e}", exc_info=True)
self.ws = None
if self.ws_thread:
self.ws_thread.join()
self.bridge = None
logger.info("✔ Stopped BybitMdProvider.")
logger.debug("Threads after stop:")
for t in threading.enumerate():
logger.debug(f"Thread(after stop): {t.name}, Alive: {t.is_alive()}, Daemon: {t.daemon}, Target={t._target}")
import os
os._exit(0)
@@ -0,0 +1,30 @@
# Gate.io WebSocket configuration
# WS_URL = "wss://fx-ws-testnet.gateio.ws/v4/ws/usdt" # Testnet url for perp futures
WS_URL = "wss://fx-ws.gateio.ws/v4/ws/usdt" # Mainnet url for perp futures
# API credentials
API_KEY = "e4a0c6b198549042b13faa7645640d60"
API_SECRET = "527c37877efc247bb0b6158cf3d5d18b84db720da894307f1e2ab88df3aaf0ee"
# Futures contract to subscribe to:
CONTRACTS = ['MELON_USDT', 'CAKE_USDT', 'ETH_USDT', 'KLAY_USDT', 'RON_USDT',
'MANA_USDT', 'BZZ_USDT', 'KEEP_USDT', 'OMG_USDT', 'GMX_USDT', 'ICP_USDT',
'LRC_USDT', 'MINA_USDT', 'ZIL_USDT', 'TURBO_USDT', 'OP_USDT', 'ZRX_USDT',
'VET_USDT', 'AR_USDT', 'IMX_USDT', 'HIGH_USDT', 'ARB_USDT', 'YFI_USDT',
'NEAR_USDT', 'OXY_USDT', 'WAVES_USDT', 'AMP_USDT', 'CELO_USDT', 'CRV_USDT',
'FIL_USDT', 'PEOPLE_USDT', 'IOTX_USDT', 'ATOM_USDT', 'NOT_USDT', 'HBAR_USDT',
'GMT_USDT', 'KAVA_USDT', 'WIF_USDT', 'UNI_USDT', 'GALA_USDT', 'AAVE_USDT',
'CRU_USDT', 'PEPE_USDT', 'TWT_USDT', 'IRIS_USDT', 'APE_USDT', 'BNB_USDT',
'ATLAS_USDT', 'BCHA_USDT', 'ETHW_USDT', 'DOT_USDT', 'TONCOIN_USDT', 'DOGS_USDT',
'MER_USDT', 'ADA_USDT', 'TRIBE_USDT', 'THETA_USDT', 'BTC_USDT', 'ORDI_USDT',
'ANKR_USDT', 'KIN_USDT', 'AMPL_USDT', 'TRX_USDT', 'SAND_USDT', 'SERO_USDT',
'LUNC_USDT', 'BAT_USDT', 'KSM_USDT', 'SOL_USDT', 'AXS_USDT', 'BCD_USDT',
'TIA_USDT', 'WOO_USDT', 'QTUM_USDT', 'OLDLUNA_USDT', 'AVAX_USDT', 'EOS_USDT',
'SNX_USDT', 'MKR_USDT', 'ZKS_USDT', 'XRP_USDT', 'STEP_USDT', 'COMP_USDT',
'UST_USDT', 'WSB_USDT', 'LINK_USDT', 'LUNA_USDT', 'ETC_USDT', 'JST_USDT',
'LON_USDT', 'PEARL_USDT', 'BTT_USDT', 'XLM_USDT', 'GRT_USDT', 'STX_USDT',
'NU_USDT', 'ALGO_USDT', 'ROSE_USDT', 'SHIB_USDT', 'BSV_USDT', 'ENJ_USDT',
'USTC_USDT', 'NEO_USDT', 'FLOW_USDT', 'RUNE_USDT', 'FIL6_USDT', 'APT_USDT',
'DOGE_USDT', 'CHZ_USDT', '1INCH_USDT', 'CSPR_USDT', 'XEC_USDT', 'ENS_USDT',
'IOST_USDT', 'LDO_USDT', 'XTZ_USDT']
@@ -0,0 +1,241 @@
import dotenv
import json
import logging
import math
import sys
import traceback
from typing import Any, Optional
import uuid
import websocket
from binance_client import BinanceClient
import python_interop_module as pyinter
BINANCE_API_KEY = "2RJ9H02ztFGWxeY2oRpnFYZNU8mki4V1yMvKrDU08HCiEhO4VLe2ePSUFqEYTNxc"
BINANCE_USDS_FUTURES_API_KEY = (
"b838c108cd2648e34a4f088a7fc635a9cbccf3c2faa6bcaabcb532ffacc087b0"
)
BINANCE_USDS_FUTURES_SECRET_KEY = (
"a68f0ab27403c42fa0306b6341b2ddd51d99ac42916f849404155449457d0492"
)
def to_bts_exec_type(exec_type: str, ord_status: str):
match exec_type:
case "TRADE_PREVENTION":
return "REJECTED"
case "NONE":
return "UNDEFINED"
case "TRADE":
return "FILL" if ord_status == "FILLED" else "PARTIAL_FILL"
case _:
return exec_type
def to_bts_order_status(ord_status: str):
match ord_status:
case "EXPIRED_IN_MATCH":
return "EXPIRED"
case _:
return ord_status
class DummyExecutionGateway(pyinter.ExecutionGateway):
def __init__(self):
super(DummyExecutionGateway, self).__init__()
self.bridge = None
self.client = None
self.start_th = None
self.ws = None
self.orders = {}
self.logger = logging.getLogger(__name__)
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.formatter = logging.Formatter(
"%(asctime)s | %(levelname)7s | %(filename)s:%(lineno)s | %(message)s"
)
self.logger.addHandler(stdout_handler)
self.logger.addHandler(
logging.FileHandler(
"/home/bts/sparkdata/data/logging.python_execution/dummy_log"
)
)
self.logger.setLevel(logging.INFO)
env_path = "/workspaces/spark/tools/binance/binance.env"
self.logger.info("DummyExecutionGateway ctr. Loading env")
dotenv.load_dotenv(env_path, interpolate=False)
def _index_transaction(self, bts_key: int):
exch_key = uuid.uuid1().hex
self.orders[exch_key] = bts_key
return exch_key
def _on_user_data_msg_core(self, ws: websocket.WebSocket, msg: Any):
self.logger.info("Recv usr data: %s", str(msg))
if not msg:
self.logger.error("Recv empty usr data msg")
return
if not self.bridge:
self.logger.error("Cannot process usr data. Bridge not created")
return
upd = json.loads(str(msg))
if "e" in upd:
if upd["e"] == "executionReport":
# Binance spots update
bts_ord_id = self.orders.get(upd["c"])
is_old_ord = False
if not bts_ord_id:
bts_ord_id = self.orders.get(upd["C"])
if not bts_ord_id:
self.logger.warning(
"Received update with unknown order id: %s", msg
)
return
is_old_ord = True
er = pyinter.PyExecutionReport()
er.set_key(bts_ord_id)
er.set_exchange_security_id(upd["s"])
er.set_security_type("SPOT")
er.set_exec_type(to_bts_exec_type(upd["x"], upd["X"]))
er.set_is_buy(upd["S"] == "BUY")
er.set_price(float(upd["p"]))
er.set_quantity(float(upd["q"]))
er.set_last_filled_price(upd["L"])
er.set_last_filled_quantity(upd["l"])
total_filled_qty = float(upd["z"])
er.set_filled_quantity(total_filled_qty)
total_filled_notional = float(upd["Z"])
avg_filled_price = (
total_filled_notional / total_filled_qty
if total_filled_qty > 0
else math.nan
)
er.set_avg_filled_price(avg_filled_price)
er.set_order_status(to_bts_order_status(upd["X"]))
er.set_order_type(upd["o"])
match upd["X"]:
case "CANCELED":
if not is_old_ord:
del self.orders[bts_ord_id]
case "FILLED":
del self.orders[bts_ord_id]
case "REJECTED":
del self.orders[bts_ord_id]
self.bridge.on_execution_report(er)
elif upd["e"] == "ORDER_TRADE_UPDATE":
data = upd["o"]
# Binance Usds Futures order update
bts_ord_id = self.orders.get(data["c"])
is_old_ord = False
if not bts_ord_id:
bts_ord_id = self.orders.get(data["C"])
if not bts_ord_id:
self.logger.warning(
"Received dataate with unknown order id: %s", msg
)
return
is_old_ord = True
def parse_float(s: Optional[str], default_val=math.nan):
if s is not None:
try:
return float(s)
except Exception:
pass
return default_val
er = pyinter.PyExecutionReport()
er.set_key(bts_ord_id)
er.set_venue("BINANCE_USDS_FUTURES")
er.set_security_type("FUTURE")
er.set_time_in_force("GTC")
er.set_exchange_order_id(str(data.get("i") or 0))
# er.set_exchange_order_id(data["i"])
er.set_exchange_security_id(data["s"])
er.set_exec_type(to_bts_exec_type(data["x"], data["X"]))
er.set_is_buy(data["S"] == "BUY")
er.set_price(parse_float(data["p"]))
er.set_quantity(parse_float(data["q"]))
er.set_last_filled_price(parse_float(data.get("L")))
er.set_last_filled_quantity(parse_float(data["l"]))
er.set_filled_quantity(parse_float(data["z"]))
er.set_avg_filled_price(parse_float(data["ap"]))
er.set_order_status(to_bts_order_status(data["X"]))
er.set_order_type(data["o"])
match data["X"]:
case "CANCELED":
if not is_old_ord:
del self.orders[bts_ord_id]
case "FILLED":
del self.orders[bts_ord_id]
case "REJECTED":
del self.orders[bts_ord_id]
print("Firing this exe report: %s" % str(er), flush=True)
self.bridge.on_execution_report(er)
def _on_user_data_msg(self, ws: websocket.WebSocket, msg: Any):
try:
self._on_user_data_msg_core(ws, msg)
except Exception:
self.logger.error("on_user_data_msg. %s" % traceback.format_exc())
def _on_user_data_open(self, ws: websocket.WebSocket):
self.ws = ws
if self.bridge:
self.bridge.on_is_ready(True)
def _on_user_data_err(self, ws: websocket.WebSocket, err: str):
self.logger.error("on_user_data_err: %s", err)
def _on_user_data_close(self, ws: websocket.WebSocket, code: int, msg: str):
self.logger.warning("on_user_data_close: %s %s", str(code), msg)
if self.bridge:
self.bridge.on_is_ready(False)
def start(self, bridge: pyinter.ExecutionGatewayBridge):
print("DummyExecutionGateway.start")
self.bridge = bridge
self.client = BinanceClient(venue="BINANCE_USDS_FUTURES")
print("DummyExecutionGateway.start. Subscribing to binance user data")
self.client.subscribe_user_data(
on_message=self._on_user_data_msg,
on_open=self._on_user_data_open,
on_close=self._on_user_data_close,
)
print("DummyExecutionGateway.start. Subscribed to binance user data")
def cancel_order(self, req: pyinter.PyCancelOrderRequest):
if not self.client:
self.logger.error("Cannot cancel order. BinanceClient not created.")
return
self.logger.info("[cancel_order.json]%s", str(req))
try:
self.client.request(
"CANCEL_ORDER",
symbol=req.exchange_security_id(),
orig_client_order_id=req.exchange_order_id(),
new_client_order_id=self._index_transaction(req.key()),
)
except Exception as ex:
self.logger.error("Create order request threw. %s" % str(ex))
def create_order(self, req: pyinter.PyCreateOrderRequest):
if not self.client:
self.logger.error("Cannot create order. BinanceClient not created.")
return
print("hey bud")
self.logger.info("[create_order.json]%s", str(req))
side = "BUY" if req.is_buy() else "SELL"
try:
self.client.request(
op="CREATE_ORDER",
symbol=req.exchange_security_id(),
price=req.price(),
quantity=f"{req.quantity():.6f}",
side=side,
time_in_force="GTC",
type="LIMIT",
new_client_order_id=self._index_transaction(req.key()),
)
except Exception as ex:
self.logger.error("Create order request threw. %s" % str(ex))
@@ -0,0 +1,79 @@
from python_interop_module import (
MdBridge,
MdProvider,
PySecurityList,
PyBook,
PyBookList,
PyTradeList,
)
from spark_core import (
get_security,
get_contract_spec,
get_price_info,
get_quantity_info,
)
from py_md_utils import get_book_side, get_trade
class DummyMdProvider(MdProvider):
def __init__(self):
super(DummyMdProvider, self).__init__()
pass
def start(self, bridge: MdBridge):
# start subscriptions, bind callbacks etc.
def add_fake_securities():
sec_list = PySecurityList()
sec_list.append(
get_security(
venue="Glbx",
name="ESH8",
asset="ES",
group="ES",
currency="USD",
expiration="2026-03-28",
spec=get_contract_spec(
price_info=get_price_info(
tick_unit=25,
tick_base=1000,
cash_per_point=50,
),
qty_info=get_quantity_info(tick_unit=1, tick_base=1),
),
)
)
bridge.on_securities(sec_list)
def add_fake_books():
bids = get_book_side([(99.75, 8), (99.50, 11)], is_bid_side=True)
asks = get_book_side([(100.25, 3)], is_bid_side=False)
book = PyBook()
book.set_venue("Glbx")
book.set_security_type("FUTURE")
book.set_exchange_security_id("ESH8")
book.set_bids(bids)
book.set_asks(asks)
# pass book to sink
book_list = PyBookList()
book_list.append(book)
bridge.on_books(book_list)
def add_fake_trades():
trade = get_trade(
venue="Glbx",
security_type="FUTURE",
exchange_security_id="ESH8",
price=100,
quantity=2,
aggressor_side="Buy",
)
trade_list = PyTradeList()
trade_list.append(trade)
bridge.on_trades(trade_list)
add_fake_securities()
add_fake_books()
add_fake_trades()
def stop(self):
print("Stopping python DummyMdProvider")
@@ -0,0 +1,226 @@
import json
import numpy as np
import time
import logging
import sys
import threading
import requests
from websocket import create_connection, WebSocketConnectionClosedException
from typing import Optional, Any
from python_interop_module import (
MdBridge,
MdProvider,
PyBook,
PyBookList,
PySecurityList,
)
from config import WS_URL, CONTRACTS
from spark_core import (
configure_logger,
get_contract_spec,
get_price_info,
get_quantity_info,
get_security,
)
logger = logging.getLogger("gateio_md_provider")
configure_logger(logger, "md")
def fetch_contract_specs() -> dict:
"""Fetch specs for all contracts in one request."""
default_specs = {
"tick_unit": 1,
"tick_base": 10,
"cash_per_point": 0.1,
"qty_tick_unit": 1,
"qty_tick_base": 10000
}
specs_dict = {contract: default_specs for contract in CONTRACTS}
try:
url = "https://api.gateio.ws/api/v4/futures/usdt/contracts"
response = requests.get(url, timeout=5)
response.raise_for_status()
data = response.json()
for item in data:
contract = item.get("name")
if contract in CONTRACTS:
price_tick = float(item.get("price_tick", "0.1"))
contract_size = float(item.get("contract_size", "0.0001"))
tick_base = int(1 / price_tick) if price_tick != 0 else 10
qty_tick_base = int(1 / contract_size) if contract_size != 0 else 10000
specs_dict[contract] = {
"tick_unit": 1,
"tick_base": tick_base,
"cash_per_point": price_tick,
"qty_tick_unit": 1,
"qty_tick_base": qty_tick_base
}
return specs_dict
except Exception as e:
logger.error(f"Failed to fetch batch specs: {e}", exc_info=True)
return specs_dict
class GateIoMdProvider(MdProvider):
def __init__(self):
super().__init__()
self.ws = None
self.ws_thread: Optional[threading.Thread] = None
self.bridge: Optional[MdBridge] = None
self.stop_event = threading.Event()
self.ping_thread = None
def send_ping(self):
"""Send ping messages every 15 seconds to keep connection alive."""
while not self.stop_event.is_set():
try:
ping_msg = {
"time": int(time.time() * 1000),
"channel": "futures.ping",
"event": "",
"payload": []
}
self.ws.send(json.dumps(ping_msg))
logger.debug("Sent ping")
except Exception as e:
logger.error(f"Ping error: {e}", exc_info=True)
break
time.sleep(15)
def start_impl(self, bridge: MdBridge):
self.bridge = bridge
logger.info(f"Connecting to {WS_URL}")
self.ws = create_connection(WS_URL)
self.ws.settimeout(5)
logger.info("Connected.")
# Start ping thread
self.ping_thread = threading.Thread(target=self.send_ping, daemon=True)
self.ping_thread.start()
logger.info("Started ping thread")
# Fetch all contract specs at once
contract_specs = fetch_contract_specs()
# 1) Send security info
sec_list = PySecurityList()
for contract in CONTRACTS:
asset, quote = contract.split("_")
specs = contract_specs[contract]
spec = get_contract_spec(
price_info=get_price_info(
tick_unit=specs["tick_unit"],
tick_base=specs["tick_base"],
cash_per_point=specs["cash_per_point"]
),
qty_info=get_quantity_info(
tick_unit=specs["qty_tick_unit"],
tick_base=specs["qty_tick_base"]
)
)
sec = get_security(
venue="GateIO",
name=contract,
asset=asset,
group=asset,
currency=quote,
expiration=None,
option_kind=None,
strike=None,
spec=spec,
)
sec_list.append(sec)
logger.info(f"Created security: {contract} with specs: {specs}")
bridge.on_securities(sec_list)
logger.info("Sent securities list--sink.on_securities called.")
# 2) Subscribe to order books
for contract in CONTRACTS:
sub_msg = {
"time": int(time.time() * 1000),
"channel": "futures.order_book",
"event": "subscribe",
"payload": [contract, "5", "0"]
}
self.ws.send(json.dumps(sub_msg))
logger.info(f"✔ Sent subscribe for {contract}: {sub_msg}")
# 3) Listen for incoming messages in main thread
logger.info("▶ Listening for data")
try:
while not self.stop_event.is_set():
try:
msg = self.ws.recv()
except Exception as e:
logger.debug(f"⚠ recv() error or timeout: {e}")
continue
logger.debug(f"⟵ raw WS: {msg}")
try:
data = json.loads(msg)
logger.debug(f"Parsed JSON: {data}")
except Exception as e:
logger.error(f"JSON parse error: {e}", exc_info=True)
continue
event = data.get("event")
channel = data.get("channel")
# Handle subscription acknowledgements
if event == "subscribe":
logger.info(f"🔔 Subscribe ACK: {data}")
continue
# Handle order book updates
if channel == "futures.order_book" and event in ["all", "update"]:
logger.debug(f"Handling '{event}' event")
payload = data.get("result", {})
if isinstance(payload, dict) and ("bids" in payload or "asks" in payload):
contract = payload.get("contract", "<no-contract>")
bids = [(np.float64(l["p"]), np.float64(l["s"])) for l in payload.get("bids", [])]
asks = [(np.float64(l["p"]), np.float64(l["s"])) for l in payload.get("asks", [])]
logger.debug(f"📘 BOOK DATA for {contract}: bids={bids} asks={asks}")
book = PyBook()
book.set_data(
contract,
"GateIO",
"SPOT",
np.array(bids, dtype=np.float64),
np.array(asks, dtype=np.float64),
)
book_list = PyBookList()
book_list.append(book)
self.bridge.on_books(book_list)
logger.debug(f"✔ on_books called for {contract}")
else:
logger.debug("… no bids/asks in payload; skipping")
except WebSocketConnectionClosedException:
logger.info("WebSocket closed.")
except Exception as e:
logger.error("Fatal error in listen loop:", exc_info=True)
def start(self, bridge: MdBridge):
self.ws_thread = threading.Thread(target=lambda: self.start_impl(bridge))
self.ws_thread.start()
def stop(self):
"""Disconnect and cleanup."""
logger.info("Stopping GateIoMdProvider...")
self.stop_event.set()
if self.ping_thread and self.ping_thread.is_alive():
self.ping_thread.join()
logger.info("Ping thread stopped")
if self.ws:
try:
self.ws.close()
except Exception:
pass
self.ws = None
if self.ws_thread:
self.ws_thread.join()
self.bridge = None
logger.info("✔ Stopped GateIoMdProvider.")
@@ -0,0 +1,12 @@
import json
import string
import os
def load_gateway_config(name: str, expand_vars: bool = True):
s = ""
with open(f"/home/bts/sparkdata/conf/py_{name}.json") as f:
s = f.read()
if expand_vars:
s = string.Template(s).substitute(os.environ)
return json.loads(s)
@@ -0,0 +1,40 @@
import logging
import os
import sys
import threading
import time
from typing import Optional
from python_interop_module import PyBookSide, PyBookLevel, PyTrade
PERP_EXP_STRING = "2100-01-01T00:00"
def get_book_side(levels: list[tuple[float, float]], is_bid_side: bool):
book_side = PyBookSide()
book_side.set_is_ascending(not is_bid_side)
for price, qty in levels:
book_level = PyBookLevel()
book_level.set_price(price)
book_level.set_quantity(qty)
book_side.set_level(book_level)
return book_side
def get_trade(
venue: str,
security_type: str,
exchange_security_id: str,
price: float,
quantity: float,
aggressor_side: str,
):
trade = PyTrade()
trade.set_venue(venue)
trade.set_security_type(security_type)
trade.set_exchange_security_id(exchange_security_id)
trade.set_price(price)
trade.set_quantity(quantity)
trade.set_aggressor_side(aggressor_side)
return trade
@@ -0,0 +1,372 @@
import json
import math
import string
import logging
import os
import sys
import threading
import time
from typing import Callable, Optional
import websocket
from python_interop_module import (
PyContractPriceInfo,
PyContractPriceInfoList,
PyContractQuantityInfo,
PyContractQuantityInfoList,
PyContractSpecification,
PyFuture,
PyOption,
PySecurity,
PySpot,
)
class JsonElement:
def __init__(self, value):
self.value = value
def __str__(self):
return json.dumps(self.value)
class StructuredLog:
def __init__(self, **kwargs):
self.kwargs = kwargs
if "ts" not in kwargs:
kwargs["ts"] = time.time_ns()
def __str__(self):
return json.dumps(self.kwargs)
def _log_level_name(level: int):
match level:
case logging.DEBUG:
return "Diag"
case logging.INFO:
return "Info"
case logging.WARNING:
return "Warn"
case logging.ERROR:
return "Error"
case logging.CRITICAL:
return "Fatal"
case _:
return str(level)
class FileLogFormatter(logging.Formatter):
def __init__(self, subsystem: str):
self.subsystem = subsystem
def formatTime(self, record, datefmt=None):
ct = self.converter(record.created)
if datefmt:
return time.strftime(datefmt, ct)
else:
t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
return f"{t}.{int(record.msecs * 1000):06d}"
def format(self, record):
env = "dev"
thread_id = threading.get_ident()
filename_line = f"{record.filename}:{record.lineno}"
timestamp = self.formatTime(record)
timestamp_ns = int(record.created * 1e9)
return f"{timestamp}|{timestamp_ns}|{self.subsystem:<8}|{_log_level_name(record.levelno):<5}|{os.getpid():<3}|{record.threadName:<20}|{thread_id}|{filename_line:<35}|{env}| {record.getMessage()}"
class ConsoleLogFormatter(logging.Formatter):
def __init__(self, subsystem: str):
self.subsystem = subsystem
def formatTime(self, record, datefmt=None):
ct = self.converter(record.created)
if datefmt:
return time.strftime(datefmt, ct)
else:
t = time.strftime("%Y-%m-%d %H:%M:%S", ct)
return f"{t}.{int(record.msecs * 1000):06d}"
def format(self, record):
filename_line = f"{record.filename}:{record.lineno}"
timestamp = self.formatTime(record)
return f"{timestamp}|{self.subsystem:<8}|{_log_level_name(record.levelno):<5}|{filename_line:<35}| {record.getMessage()}"
def as_bool(*args):
(True if arg else False for arg in args)
def configure_logger(logger: logging.Logger, subsystem: str):
stdout_handler = logging.StreamHandler(sys.stdout)
file_handler = logging.FileHandler(
f"/home/bts/sparkdata/data/logging.sparkd/{logger.name}_log"
)
stdout_handler.formatter = ConsoleLogFormatter(subsystem=subsystem)
file_handler.formatter = FileLogFormatter(subsystem=subsystem)
logger.handlers.clear()
logger.addHandler(stdout_handler)
logger.addHandler(file_handler)
logger.setLevel(logging.INFO)
return logger
def parse_float(s: Optional[str], default_val=math.nan):
if s is not None:
try:
return float(s)
except Exception:
pass
return default_val
def run_websocket(
url: str,
stop_event: threading.Event,
logger: logging.Logger,
on_message: Callable[[str], None],
send_ping: Optional[Callable[[websocket.WebSocket], None]] = None,
ping_interval_secs: Optional[int] = 25,
):
conn_num = 0
if send_ping is None:
def send_empty_ping(ws):
return ws.ping()
send_ping = send_empty_ping
while not stop_event.is_set():
if conn_num > 0:
# Gradually increase sleep time between attempts, capping at 5min.
sleep_secs = min(conn_num * 5, 300)
logger.warning("Reconnecting to %s in %ds." % (url, sleep_secs))
time.sleep(sleep_secs)
logger.info("Connecting to %s (attempt no %d)" % (url, conn_num))
ws = websocket.create_connection(url)
ws.settimeout(5)
logger.info("Connected to %s. Listening for data" % url)
if ping_interval_secs:
def ping_loop(orig_conn_num):
time.sleep(ping_interval_secs)
while not stop_event.is_set() and conn_num == orig_conn_num:
send_ping(ws)
time.sleep(ping_interval_secs)
threading.Thread(target=ping_loop(conn_num), daemon=True).start()
try:
msg = ws.recv()
if isinstance(msg, bytes):
on_message(msg.decode("utf-8"))
elif isinstance(msg, str):
on_message(msg)
else:
logger.error("Websocket recv unexpected msg type %s" % str(type(msg)))
except Exception:
logging.error("Websocket recv threw.", exc_info=True)
continue
def get_tick_factors(tick_sz: float):
"""
Returns a tuple of (tick unit, tick base) that represents the tick size as a
fraction. For example, if tick_sz is 0.01, returns (1, 100), as 1/100 == 0.01.
"""
if not (tick_sz > 0):
raise ValueError(f"Invalid tick size {tick_sz}")
tick_denom = 1.0 / tick_sz
match tick_denom:
case 1:
return (1, 1)
case 10:
return (10, 100)
case 100:
return (1, 100)
case 1_000:
return (1, 1_000)
case 10_000:
return (1, 10_000)
case 100_000:
return (1, 100_000)
case 1_000_000:
return (1, 1_000_000)
case 10_000_000:
return (1, 10_000_000)
case 100_000_000:
return (1, 100_000_000)
case 1_000_000_000:
return (1, 1_000_000_000)
case 4:
return (25, 100)
case 40:
return (25, 1_000)
case 400:
return (25, 10_000)
case 4_000:
return (25, 100_000)
case 20:
return (5, 100)
case 200:
return (5, 1_000)
case 2_000:
return (5, 10_000)
case 20_000:
return (5, 100_000)
case _:
raise ValueError(f"Invalid tick size {tick_sz}")
def get_num_decimal_places(tick_sz: float):
return math.pow(10, -math.floor(math.log10(tick_sz)))
def get_qty_decimal_places(sec: PySecurity) -> int:
qty_infos = sec.contract_spec().quantity_infos()
if not qty_infos:
return 2
tick_sz = qty_infos[0].tick_unit() / qty_infos[0].tick_base()
return round(-math.floor(math.log10(tick_sz)))
def _get_decimal_places(tick_base: float) -> int:
# tick base of 100 -> 2 decimals, tick base of 1000 -> 3 decimals, etc.
tick_sz = 1 / tick_base
return round(-math.floor(math.log10(tick_sz)))
def get_price_decimal_places(sec: PySecurity) -> int:
price_infos = sec.contract_spec().price_infos()
if not price_infos:
return 2
return _get_decimal_places(price_infos[0].tick_base())
def format_price(sec: PySecurity, value: float) -> str:
decimals = get_price_decimal_places(sec)
return f"{value:.{decimals}f}"
def format_quantity(sec: PySecurity, value: float) -> str:
decimals = get_qty_decimal_places(sec)
return f"{value:.{decimals}f}"
def get_price_info(
tick_unit: float,
tick_base: float,
cash_per_point: Optional[float] = None,
multiplier: Optional[float] = None,
max_price: Optional[float] = None,
):
# Use 'not' checks to account for nans (for which every op evaluates to false)
if not (tick_unit > 0) or not (tick_base >= tick_unit):
raise ValueError(f"Invalid tick factors {tick_unit}/{tick_base}")
price_info = PyContractPriceInfo()
price_info.set_tick_unit(tick_unit)
price_info.set_tick_base(tick_base)
if cash_per_point:
price_info.set_cash_per_point(cash_per_point)
if max_price:
price_info.set_max_price(max_price)
if multiplier:
price_info.set_multiplier(multiplier)
return price_info
def get_quantity_info(
tick_unit: float,
tick_base: float,
min_value: Optional[float] = None,
max_value: Optional[float] = None,
multiplier: Optional[float] = None,
max_price: Optional[float] = None,
):
# Use 'not' checks to account for nans (for which every op evaluates to false)
if not (tick_unit > 0) or not (tick_base >= tick_unit):
raise ValueError(f"Invalid tick factors {tick_unit}/{tick_base}")
if not min_value:
min_value = tick_unit / tick_base
elif min_value < (tick_unit / tick_base):
# min_value should at least be 1 tick
raise ValueError(f"min_value ({min_value}) < tick ({tick_unit}/{tick_base})")
qty_info = PyContractQuantityInfo()
qty_info.set_tick_unit(tick_unit)
qty_info.set_tick_base(tick_base)
qty_info.set_min_value(min_value)
if max_value:
if not (max_value > min_value):
raise ValueError(
f"max_value ({max_value}) not greater than min_value ({min_value})"
)
qty_info.set_max_value(max_value)
if max_price:
qty_info.set_max_price(max_price)
if multiplier:
qty_info.set_multiplier(multiplier)
return qty_info
def get_contract_spec(
price_info: Optional[PyContractPriceInfo] = None,
qty_info: Optional[PyContractQuantityInfo] = None,
notional_qty_info: Optional[PyContractQuantityInfo] = None,
):
price_infos = PyContractPriceInfoList()
if not price_info:
# Default to 3 decimal places
price_info = get_price_info(1, 1000)
price_infos.append(price_info)
qty_infos = PyContractQuantityInfoList()
if not qty_info:
# Default to 3 decimal places
qty_info = get_quantity_info(1, 1000)
qty_infos.append(qty_info)
notional_qty_infos = PyContractQuantityInfoList()
if notional_qty_info:
notional_qty_infos.append(notional_qty_info)
spec = PyContractSpecification()
spec.set_price_infos(price_infos)
spec.set_quantity_infos(qty_infos)
spec.set_notional_quantity_infos(notional_qty_infos)
return spec
def get_security(
venue: str,
name: str,
asset: str,
group: str,
currency: str,
expiration: Optional[str] = None,
option_kind: Optional[str] = None,
strike: Optional[float] = None,
spec: Optional[PyContractSpecification] = None,
):
sec = PySecurity()
sec.set_venue(venue)
sec.set_exchange_id(name)
sec.set_name(name)
sec.set_currency(currency)
sec.set_asset(asset)
sec.set_group(group)
if not spec:
spec = get_contract_spec()
sec.set_contract_spec(spec)
if expiration and strike and option_kind:
opt = PyOption()
opt.set_expiration(expiration)
opt.set_is_call(option_kind[0].lower() == "c")
opt.set_strike(strike)
sec.set_option(opt)
elif expiration and not strike and not option_kind:
fut = PyFuture()
fut.set_expiration(expiration)
sec.set_future(fut)
elif not expiration and not strike and not option_kind:
sec.set_spot(PySpot())
else:
raise ValueError("Invalid parameters (unable to deduce security type)")
return sec