Skip to content
Merged

Dev #23

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,10 @@ account reports.
- `use_pyramiding=False` snaps signals to `-1/0/1`; `True` preserves fractional
scales such as `1.4`.
- For crypto, `contract_size` is a notional/PnL multiplier. Exchange fractional
lots are governed by `qty_step`/`lot_size`/`min_qty`/`min_notional`.
lots are governed by shared venue constraints:
`qty_step`/`lot_size`/`slot_size`/`min_qty`/`min_notional`, applied across
native legacy, native vectorized, native event/order, native portfolio, and
Nautilus validation routes.

### DCA And Grid

Expand Down
4 changes: 4 additions & 0 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
build_arbitrage_order_plan,
round_down_to_step,
)
from .core.constraints import QuantityConstraints, build_quantity_constraints, quantize_signed_quantity
from .core.schema import (
AccountConfig,
AssetType,
Expand Down Expand Up @@ -310,6 +311,7 @@
"OrderIntent",
"OrderSide",
"OrderType",
"QuantityConstraints",
"OptionsVolArbSpec",
"PackageExecutionKind",
"PackageRejection",
Expand All @@ -329,12 +331,14 @@
"TriangularArbSpec",
"build_arbitrage_order_plan",
"build_bracket_order_plan",
"build_quantity_constraints",
"build_dca_grid_order_plan",
"build_frozen_basket_orders",
"normalize_portfolio_mode",
"normalize_portfolio_sizing_mode",
"normalize_rebalance_policy",
"portfolio_capability_matrix",
"quantize_signed_quantity",
"round_down_to_step",
"simulate_nautilus_order_package_depth",
"validate_portfolio_result_contract",
Expand Down
114 changes: 111 additions & 3 deletions backends/native_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
TIF_IOC,
_engine_event_v1,
)
from ..core.constraints import build_quantity_constraints, quantize_signed_quantity
from ..core.arbitrage import (
ArbitrageSpec,
ArbitragePlan,
Expand Down Expand Up @@ -59,6 +60,7 @@
OrderSide,
OrderType,
TimeInForce,
InstrumentSpec,
)


Expand Down Expand Up @@ -154,6 +156,12 @@ def run_orders(
symbols: Optional[List[str]] = None,
market_arrays: Optional[PreparedMarketArrays] = None,
compiled_orders: Optional[CompiledOrderArrays] = None,
instruments: Optional[Union[Dict[str, InstrumentSpec], List[InstrumentSpec]]] = None,
qty_step: Optional[Union[float, Dict[str, float]]] = None,
lot_size: Optional[Union[float, Dict[str, float]]] = None,
slot_size: Optional[Union[float, Dict[str, float]]] = None,
min_qty: Optional[Union[float, Dict[str, float]]] = None,
min_notional: Optional[Union[float, Dict[str, float]]] = None,
) -> BacktestResultV2:
idx = validate_datetime(datetime_index)
symbol_list = symbols or list(closes.keys())
Expand All @@ -170,16 +178,38 @@ def run_orders(
elif market_arrays.signature != self._market_signature(idx, symbol_list):
raise ValueError("prepared market arrays do not match datetime_index/symbols")

contract_sizes = self._per_symbol_array(contract_size, symbol_list, default=1.0)
constraints = build_quantity_constraints(
symbol_list,
instruments=instruments,
qty_step=qty_step,
lot_size=lot_size,
slot_size=slot_size,
min_qty=min_qty,
min_notional=min_notional,
)
effective_orders, quantity_preflight = self._apply_order_quantity_constraints(
idx=idx,
orders=orders,
closes=market_arrays.closes,
symbol_list=symbol_list,
contract_sizes=contract_sizes,
constraints=constraints,
)
if quantity_preflight["changed_count"] or quantity_preflight["dropped_count"]:
compiled_orders = None
orders = tuple(effective_orders)
else:
effective_orders = tuple(orders)

if compiled_orders is None:
compiled_orders = self.compile_orders(datetime_index=idx, orders=orders, symbols=symbol_list)
compiled_orders = self.compile_orders(datetime_index=idx, orders=effective_orders, symbols=symbol_list)
elif (
compiled_orders.index_signature != market_arrays.signature
or compiled_orders.symbols != tuple(symbol_list)
):
raise ValueError("compiled orders do not match prepared market arrays")
n_orders = compiled_orders.n_orders

contract_sizes = self._per_symbol_array(contract_size, symbol_list, default=1.0)
leverages = self._per_symbol_array(
self.config.account.leverage if leverage is None else leverage,
symbol_list,
Expand Down Expand Up @@ -294,11 +324,77 @@ def run_orders(
"fee_rate_oneway": self._fee_rate_metadata(fee_rates, symbol_list),
"slippage_bps": self.config.execution.slippage_bps,
"order_report": order_report,
"quantity_constraints": constraints.as_dict(),
"quantity_preflight": quantity_preflight,
"initial_buying_power": self.config.account.initial_capital * float(np.mean(leverages)),
"liquidation_reason": int(liq_reason),
},
)

@staticmethod
def _apply_order_quantity_constraints(
*,
idx: pd.DatetimeIndex,
orders: Sequence[OrderIntent],
closes: np.ndarray,
symbol_list: List[str],
contract_sizes: np.ndarray,
constraints,
) -> tuple[tuple[OrderIntent, ...], Dict]:
if not constraints.enabled:
return tuple(orders), {"changed_count": 0, "dropped_count": 0, "dropped_orders": []}
sym_to_col = {symbol: j for j, symbol in enumerate(symbol_list)}
changed = 0
dropped = []
out: list[OrderIntent] = []
idx_ns = idx.view("int64")
for order_idx, order in enumerate(orders):
col = sym_to_col[order.symbol]
ts = pd.Timestamp(order.timestamp)
if ts.tz is None:
ts = ts.tz_localize("UTC")
else:
ts = ts.tz_convert("UTC")
bar = int(np.searchsorted(idx_ns, ts.value, side="left"))
if bar >= len(idx):
bar = len(idx) - 1
price = float(order.price) if order.price is not None else float(closes[bar, col])
signed = order.signed_qty
q = abs(
quantize_signed_quantity(
signed,
price,
float(contract_sizes[col]),
float(constraints.qty_step[col]),
float(constraints.min_qty[col]),
float(constraints.min_notional[col]),
)
)
if q <= 0.0:
dropped.append({"original_index": order_idx, "symbol": order.symbol, "requested_qty": float(order.qty)})
continue
if abs(q - float(order.qty)) > 1e-12:
changed += 1
out.append(
OrderIntent(
timestamp=order.timestamp,
symbol=order.symbol,
side=order.side,
order_type=order.order_type,
qty=q,
price=order.price,
trigger_price=order.trigger_price,
tif=order.tif,
reduce_only=order.reduce_only,
order_id=order.order_id,
tag=order.tag,
metadata={**order.metadata, "requested_qty": float(order.qty), "quantity_quantized": True},
)
)
else:
out.append(order)
return tuple(out), {"changed_count": changed, "dropped_count": len(dropped), "dropped_orders": dropped}

def run_basket(
self,
datetime_index: Union[pd.DatetimeIndex, pd.Series],
Expand All @@ -314,6 +410,12 @@ def run_basket(
fee_rate: Optional[Union[float, Dict[str, float]]] = None,
rebalance_threshold: Optional[float] = None,
symbols: Optional[List[str]] = None,
instruments: Optional[Union[Dict[str, InstrumentSpec], List[InstrumentSpec]]] = None,
qty_step: Optional[Union[float, Dict[str, float]]] = None,
lot_size: Optional[Union[float, Dict[str, float]]] = None,
slot_size: Optional[Union[float, Dict[str, float]]] = None,
min_qty: Optional[Union[float, Dict[str, float]]] = None,
min_notional: Optional[Union[float, Dict[str, float]]] = None,
) -> BacktestResultV2:
"""
Build frozen basket orders from a scalar signal and execute them.
Expand Down Expand Up @@ -343,6 +445,12 @@ def run_basket(
leverage=leverage,
fee_rate=fee_rate,
symbols=symbols,
instruments=instruments,
qty_step=qty_step,
lot_size=lot_size,
slot_size=slot_size,
min_qty=min_qty,
min_notional=min_notional,
)
result.metadata["basket_plan"] = plan
result.metadata["basket_target_units"] = plan.target_units
Expand Down
25 changes: 24 additions & 1 deletion backends/native_portfolio.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import pandas as pd

from ..core.engine import _engine_portfolio, _engine_portfolio_equity_sizing
from ..core.constraints import build_quantity_constraints, quantize_target_units_matrix
from ..core.portfolio import (
NATIVE_PORTFOLIO_SUPPORTED_SIZING_MODES,
PortfolioDomainSpec,
Expand All @@ -37,7 +38,7 @@
)
from ..core.results import BacktestResultV2
from ..core.schema import AccountConfig
from ..core.schema import ExecutionConfig
from ..core.schema import ExecutionConfig, InstrumentSpec
from ..sizing.fast import scale_signal_notional_matrix


Expand Down Expand Up @@ -87,6 +88,12 @@ def run_signals(
risk_lookback: int = 60,
market_arrays: Optional[PreparedMarketArrays] = None,
raw_signal_matrix: Optional[np.ndarray] = None,
instruments: Optional[Union[Dict[str, InstrumentSpec], List[InstrumentSpec]]] = None,
qty_step: Optional[Union[float, Dict[str, float]]] = None,
lot_size: Optional[Union[float, Dict[str, float]]] = None,
slot_size: Optional[Union[float, Dict[str, float]]] = None,
min_qty: Optional[Union[float, Dict[str, float]]] = None,
min_notional: Optional[Union[float, Dict[str, float]]] = None,
) -> BacktestResultV2:
idx = validate_datetime(datetime_index)
if positions is None and raw_signal_matrix is None:
Expand Down Expand Up @@ -128,6 +135,15 @@ def run_signals(
raise ValueError("raw_signal_matrix shape does not match prepared market arrays")

cs_arr = self._per_symbol_array(contract_size, symbol_list, default=1.0)
constraints = build_quantity_constraints(
symbol_list,
instruments=instruments,
qty_step=qty_step,
lot_size=lot_size,
slot_size=slot_size,
min_qty=min_qty,
min_notional=min_notional,
)
lev_arr = self._per_symbol_array(
self.config.account.leverage if leverage is None else leverage,
symbol_list,
Expand Down Expand Up @@ -173,6 +189,9 @@ def run_signals(
exposure_scalar=float(np.mean(alloc_arr)) if len(alloc_arr) else 1.0,
beta=beta_arr,
inv_vol=inv_vol,
qty_steps=constraints.qty_step,
min_qtys=constraints.min_qty,
min_notionals=constraints.min_notional,
)
else:
target_units = self._scale_target_units(
Expand All @@ -191,6 +210,7 @@ def run_signals(
betas=beta_arr,
risk_vol=risk_vol,
)
target_units = quantize_target_units_matrix(target_units, market.closes, cs_arr, constraints)

(
equity_arr,
Expand Down Expand Up @@ -239,6 +259,7 @@ def run_signals(
maintenance_ratio=maint_ratio,
liquidated=bool(liq_flag),
liquidation_bar=int(liq_idx),
quantity_constraints=constraints.as_dict(),
)
spec = PortfolioDomainSpec(mode=portfolio_mode, sizing_mode=sizing_mode)
result.metadata["portfolio_contract_report"] = validate_portfolio_result_contract(result, spec, tolerance=1e-8)
Expand Down Expand Up @@ -423,6 +444,7 @@ def _build_result(
maintenance_ratio: float,
liquidated: bool,
liquidation_bar: int,
quantity_constraints: Dict[str, Dict[str, float]],
) -> BacktestResultV2:
equity = pd.Series(equity_arr, index=idx, name="equity")
close_report = pd.DataFrame(closes_m, index=idx, columns=symbol_list, copy=False)
Expand Down Expand Up @@ -519,6 +541,7 @@ def _build_result(
"turnover_total": float(np.sum(turnover_arr)),
"fee_rate_oneway": float(self.config.fee_rate),
"contract_size": {s: float(contract_sizes[j]) for j, s in enumerate(symbol_list)},
"quantity_constraints": quantity_constraints,
},
)

Expand Down
Loading
Loading