Document and abstract portfolio trading costs
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
"""Trading cost models for portfolio execution simulation."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Mapping
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class CostModel(ABC):
|
||||
"""Interface for per-name execution cost models."""
|
||||
|
||||
@abstractmethod
|
||||
def compute(
|
||||
self,
|
||||
traded_shares: np.ndarray,
|
||||
execution_price: np.ndarray,
|
||||
side: np.ndarray,
|
||||
date,
|
||||
metadata: Mapping[str, object] | None = None,
|
||||
) -> np.ndarray:
|
||||
"""Return per-name trading cost in yuan."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SimpleProportionalCostModel(CostModel):
|
||||
"""Simplified open-execution proportional cost model.
|
||||
|
||||
Slippage is represented as an additional cash cost. The execution price is
|
||||
not adjusted by slippage, which avoids double-counting.
|
||||
"""
|
||||
|
||||
cost_bps: float = 0.0
|
||||
slippage_bps: float = 0.0
|
||||
|
||||
def compute(
|
||||
self,
|
||||
traded_shares: np.ndarray,
|
||||
execution_price: np.ndarray,
|
||||
side: np.ndarray,
|
||||
date,
|
||||
metadata: Mapping[str, object] | None = None,
|
||||
) -> np.ndarray:
|
||||
shares = np.asarray(traded_shares, dtype=np.float64)
|
||||
price = np.asarray(execution_price, dtype=np.float64)
|
||||
open_price = np.where(np.isfinite(price), price, 0.0)
|
||||
trade_value = np.abs(shares * open_price)
|
||||
return trade_value * (self.cost_bps + self.slippage_bps) / 1e4
|
||||
@@ -4,7 +4,8 @@ Execution model (documented convention): a position book targeted from
|
||||
information available on date ``t`` is executed at ``open[t+1]``. Trades that
|
||||
violate a :class:`~pipeline.portfolio.constraints.TradeConstraint` (suspension,
|
||||
price limit, volume cap, …) are clipped; a fully blocked buy leaves the position
|
||||
at its previous level. Realized PnL marks the *actually filled* book.
|
||||
at its previous level. Realized PnL marks the *actually filled* book. Trading
|
||||
cost defaults to a simplified open-execution proportional cash-cost model.
|
||||
|
||||
The simulator is an ABC + a :class:`ReferenceSimulator`; constraints compose by
|
||||
intersecting their per-name signed delta bounds.
|
||||
@@ -21,6 +22,7 @@ import pandas as pd
|
||||
|
||||
from pipeline.common.schema import FILL_COLUMNS, PNL_COLUMNS
|
||||
from pipeline.portfolio.constraints import TradeConstraint
|
||||
from pipeline.portfolio.costs import CostModel, SimpleProportionalCostModel
|
||||
from pipeline.portfolio.market_rules import MarketRule, compute_limit_status
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -65,10 +67,12 @@ class ExecutionSimulator(ABC):
|
||||
"""Abstract execution layer. Subclasses define how a target gets filled."""
|
||||
|
||||
def __init__(self, constraints: list[TradeConstraint] | None = None,
|
||||
cost_bps: float = 0.0, slippage_bps: float = 0.0):
|
||||
cost_bps: float = 0.0, slippage_bps: float = 0.0,
|
||||
cost_model: CostModel | None = None):
|
||||
self.constraints = constraints or []
|
||||
self.cost_bps = cost_bps
|
||||
self.slippage_bps = slippage_bps
|
||||
self.cost_model = cost_model or SimpleProportionalCostModel(
|
||||
cost_bps=cost_bps, slippage_bps=slippage_bps
|
||||
)
|
||||
|
||||
@abstractmethod
|
||||
def fill(self, ctx: TradeContext) -> FillResult:
|
||||
@@ -104,9 +108,17 @@ class ReferenceSimulator(ExecutionSimulator):
|
||||
blocked = (traded != desired).astype(np.int64)
|
||||
|
||||
realized = prev + traded
|
||||
open_px = np.where(np.isfinite(ctx.slice.price), ctx.slice.price, 0.0)
|
||||
trade_value = np.abs(traded.astype(np.float64) * open_px)
|
||||
cost = trade_value * (self.cost_bps + self.slippage_bps) / 1e4
|
||||
cost = self.cost_model.compute(
|
||||
traded_shares=traded,
|
||||
execution_price=ctx.slice.price,
|
||||
side=np.sign(traded),
|
||||
date=ctx.slice.date,
|
||||
metadata={
|
||||
"symbol_ids": ctx.slice.symbol_ids,
|
||||
"booksize": ctx.booksize,
|
||||
"market_slice": ctx.slice,
|
||||
},
|
||||
)
|
||||
return FillResult(realized, traded, cost, blocked)
|
||||
|
||||
def run(
|
||||
|
||||
Reference in New Issue
Block a user