Перейти к содержимому
Все документы библиотеки

Многоуровневый контроль торговых рисков и теневой учёт портфеля

Блокнот Machine Learning for Trading

Сводка

В документе показана брокерская оболочка, которая проверяет заявки и состояние портфеля перед отправкой сделок. Контроль включает ограничения числа акций и стоимости каждой заявки, лимиты экспозиции позиции, частоты заявок, списки разрешённых и запрещённых активов, а также аварийную остановку. Синтетический брокер показывает принятые и отклонённые запросы, а проверки-утверждения подтверждают ожидаемый результат каждого сценария.

Также описан теневой режим: виртуальный портфель записывает исполнения и учётную стоимость позиции без отправки сделок базовому брокеру. Состояние остановки и дневные счётчики могут сохраняться при перезапуске процесса. Примеры демонстрируют работу системы, но не её результаты на реальных рынках: брокер исполняет заявки немедленно, а некоторые средства контроля, включая фильтрацию дубликатов, проверку отклонения цены и мониторинг дневного убытка, здесь только упомянуты и не протестированы. Предложенный запуск начинается с теневого режима, затем переходит к торговле на бумажном счёте и предусматривает осторожные лимиты перед переходом к реальной торговле.

Ключевые идеи

  • Независимые проверки могут блокировать небезопасные заявки до того, как они попадут к брокеру.
  • Лимиты заявок регулируют отдельные сделки, а лимиты позиций ограничивают накопленную экспозицию.
  • Ограничения частоты и правила для активов сдерживают торговую активность и определяют допустимую вселенную.
  • Постоянный аварийный выключатель может не допустить возобновления остановленной стратегии после перезапуска.
  • Теневой учёт помогает выявлять проблемы с позициями и учётной стоимостью позиции, не отправляя сделки брокеру.

Теги

Полный текст
# SafeBroker Risk Controls Demo


# SafeBroker Risk Controls Demo

**Docker image**: `ml4t`

**Section Reference**: 25.7 (Operational readiness)

**Implementation Skills**:
- ml4t.live.safety: SafeBroker, LiveRiskConfig, RiskState, RiskLimitError
- ml4t.live.safety: VirtualPortfolio for shadow mode position tracking
- Kill switch, position limits, order limits, drawdown monitoring

**Key Learning**:
This notebook drives six of SafeBroker's risk controls into their
failure modes against a synthetic broker:

1. **Order Size Limits**: Max shares and max value per order
2. **Position Limits**: Max value, shares, and total exposure
3. **Rate Limiting**: Max orders per minute
4. **Asset Restrictions**: Allowed/blocked asset lists
5. **Kill Switch**: Emergency halt that persists across restarts
6. **Shadow Mode**: VirtualPortfolio tracks fills without touching the broker

Three further controls, duplicate-order filtering, price-deviation checks
and daily-loss monitoring, are available through `LiveRiskConfig` and are
not demonstrated here. `13_runtime_safety_showcase` drives the daily-loss
kill-switch trip and stale-data rejection.

**Why This Matters**:
Live trading can lose real money. These safeguards provide defense-in-depth
against bugs, API errors, fat-finger mistakes, and runaway strategies.

**Learning Objectives**
- See how each protection layer fails closed when the strategy asks for something unsafe.
- Distinguish order-level checks from portfolio-level checks and persistent kill switches.
- Read the risk-control output as an operational checklist, not as a library feature tour.

**Prerequisites**
- Review Chapter 25.7 on operational readiness and the broker wrappers introduced earlier in the chapter.
- Familiarity with shadow mode and why paper trading alone is not enough for release gating.

## Setup

The setup is intentionally minimal because the point of the notebook is to expose the risk engine's
decisions. A lightweight mock broker is enough to make the failure modes and guardrails visible.

```python
"""SafeBroker Risk Controls Demo: exercise six controls plus shadow accounting."""

import logging
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any

from async_utils import run_async
from ml4t.backtest.types import Order, OrderSide, OrderStatus, OrderType, Position
from ml4t.live import LiveRiskConfig, RiskLimitError, SafeBroker, VirtualPortfolio
from ml4t.live.protocols import ExecutionCapability

from utils.paths import get_output_dir

print("[OK] ml4t.live risk controls imported")

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    stream=sys.stdout,
)
logger = logging.getLogger(__name__)
```

```python
# Production defaults; papermill injects overrides for CI
RATE_LIMIT_PER_MINUTE = 3
STATE_DIR = get_output_dir(25, "safety_risk_demo") / "temporary_state"
STATE_DIR.mkdir(parents=True, exist_ok=True)
MANAGED_STATE_PATHS: list[Path] = []
```

Each risk-control scenario receives a unique state path so persistence is testable without shared files.

```python
def _temporary_state_path(prefix: str = "nb10_state_") -> str:
    """Return one isolated state path and track its state and journal files."""
    state_path = STATE_DIR / f"{prefix}{len(MANAGED_STATE_PATHS) // 2 + 1:02d}.json"
    journal_path = state_path.with_name(f"{state_path.stem}-journal{state_path.suffix}l")
    for path in (state_path, journal_path):
        path.unlink(missing_ok=True)
        MANAGED_STATE_PATHS.append(path)
    return str(state_path)
```

## 1. Mock Broker for Testing

The query surface separates broker state from fill mechanics. Every blocked order can therefore be
attributed to the risk layer rather than connectivity or market-data noise.

```python
class MockBrokerQueries:
    """Read-only and connection methods required by `SafeBroker`."""

    async def connect(self) -> None:
        self._connected = True
        logger.info("MockBroker: Connected")

    async def disconnect(self) -> None:
        self._connected = False
        logger.info("MockBroker: Disconnected")

    async def is_connected_async(self) -> bool:
        return self._connected

    @property
    def execution_capabilities(self):
        return frozenset({ExecutionCapability.LIMIT})

    def assert_paper_trading(self) -> None:
        return None

    @property
    def positions(self) -> dict[str, Position]:
        return self._positions.copy()

    @property
    def pending_orders(self) -> list[Order]:
        return self._pending_orders.copy()

    def get_position(self, asset: str) -> Position | None:
        return self._positions.get(asset)

    async def get_positions_async(self) -> dict[str, Position]:
        return self._positions.copy()

    async def get_pending_orders_async(self) -> list[Order]:
        return self._pending_orders.copy()

    async def get_position_async(self, asset: str) -> Position | None:
        return self._positions.get(asset)

    async def get_account_value_async(self) -> float:
        position_value = sum(
            abs(p.quantity) * (p.current_price or p.entry_price) for p in self._positions.values()
        )
        return self._cash + position_value

    async def get_cash_async(self) -> float:
        return self._cash
```

Order construction is pure apart from the broker-local sequence number.

```python
def make_filled_order(
    broker: Any,
    asset: str,
    quantity: int,
    side: OrderSide,
    order_type: OrderType,
    limit_price: float | None,
    stop_price: float | None,
) -> Order:
    """Construct one immediately filled synthetic order."""
    broker._order_counter += 1
    price = limit_price or 100.0
    return Order(
        asset=asset,
        side=side,
        quantity=quantity,
        order_type=order_type,
        limit_price=limit_price,
        stop_price=stop_price,
        order_id=f"MOCK-{broker._order_counter}",
        status=OrderStatus.FILLED,
        filled_quantity=quantity,
        filled_price=price,
        filled_at=datetime.now(UTC),
    )
```

Fill accounting updates position quantity and cash from the same signed transaction.

```python
def apply_mock_fill(broker: Any, order: Order) -> None:
    """Apply one filled order to synthetic broker state."""
    asset, quantity, side = order.asset, order.quantity, order.side
    price = float(order.filled_price)
    signed_qty = quantity if side == OrderSide.BUY else -quantity
    current = broker._positions.get(asset)
    if current:
        new_qty = current.quantity + signed_qty
        if new_qty == 0:
            del broker._positions[asset]
        else:
            current.quantity = new_qty
    else:
        broker._positions[asset] = Position(
            asset=asset,
            quantity=signed_qty,
            entry_price=price,
            entry_time=datetime.now(UTC),
            current_price=price,
        )
    transaction = quantity * price
    broker._cash += transaction if side == OrderSide.SELL else -transaction
```

The mutable broker adds immediate-fill submission to the query surface.

```python
class MockBroker(MockBrokerQueries):
    """Synthetic broker for exercising `SafeBroker` controls."""

    def __init__(self, initial_cash: float = 100_000.0):
        self._cash = initial_cash
        self._positions: dict[str, Position] = {}
        self._pending_orders: list[Order] = []
        self._connected = False
        self._order_counter = 0

    async def submit_order_async(
        self,
        asset: str,
        quantity: int,
        side: OrderSide | None = None,
        order_type: OrderType = OrderType.MARKET,
        limit_price: float | None = None,
        stop_price: float | None = None,
        **_: Any,
    ) -> Order:
        """Fill one order immediately and update synthetic account state."""
        if side is None:
            side = OrderSide.BUY if quantity > 0 else OrderSide.SELL
            quantity = abs(quantity)
        order = make_filled_order(self, asset, quantity, side, order_type, limit_price, stop_price)
        apply_mock_fill(self, order)
        logger.info("MockBroker: %s %s %s @ $%.2f", side.value, quantity, asset, order.filled_price)
        return order

    async def cancel_order_async(self, order_id: str) -> bool:
        return False

    async def close_position_async(self, asset: str) -> Order | None:
        pos = self._positions.get(asset)
        if pos and pos.quantity != 0:
            side = OrderSide.SELL if pos.quantity > 0 else OrderSide.BUY
            return await self.submit_order_async(asset, abs(pos.quantity), side)
        return None
```

## 2. Demo Helper Function

One helper runs every attempted order and prints its outcome, so the notebook reads like an
operator console: either the request is allowed, or the control layer explains why it is
blocked. Each expected allow or block becomes an executable assertion.

```python
def run_demo(coro, expect_error: bool = False):
    """Run an async demo and fail the notebook on an unexpected outcome."""
    try:
        result = run_async(coro)
    except RiskLimitError as e:
        if not expect_error:
            raise AssertionError(f"Unexpected risk block: {e}") from e
        print(f"   [OK] Blocked as expected: {e}")
        return None
    if expect_error:
        raise AssertionError("Expected RiskLimitError, but the order was accepted")
    print("   [OK] Order accepted by control layer")
    return result
```

Every demo below reports its outcome through the same helper, so an allowed order and a
blocked one are distinguishable at a glance. A production risk layer needs the same property:
inconsistent error reporting is what makes a real incident hard to diagnose under time pressure.

## 3. Demonstration: Order Size Limits

SafeBroker enforces maximum order size (shares and value).

This first demo answers the simplest production question: can one buggy order wipe out the session before
anything else has a chance to react?

```python
print("\n" + "=" * 70)
print("DEMO 1: Order Size Limits")
print("=" * 70)

# Use temp file for state to avoid conflicts
state_file = _temporary_state_path()

broker = MockBroker()
run_async(broker.connect())

config = LiveRiskConfig(
    execution_mode="paper",
    max_order_shares=50,  # Max 50 shares per order
    max_order_value=5_000.0,  # Max $5,000 per order
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)
# Prime the staleness guard with reference quotes for every asset this demo touches.
safe_broker.record_market_snapshot("AAPL", 100.0)
safe_broker.record_market_snapshot("TSLA", 600.0)

print("\n1. Order within limits (10 shares @ $100 = $1,000):")
run_demo(safe_broker.submit_order_async("AAPL", 10, OrderSide.BUY))

print("\n2. Order exceeds share limit (100 shares, limit is 50):")
run_demo(
    safe_broker.submit_order_async("AAPL", 100, OrderSide.BUY),
    expect_error=True,
)

print("\n3. Order exceeds value limit (10 shares @ $600 = $6,000):")
# First set a price by creating a position
broker._positions["TSLA"] = Position(
    asset="TSLA",
    quantity=1,
    entry_price=600.0,
    entry_time=datetime.now(UTC),
    current_price=600.0,
)
run_demo(
    safe_broker.submit_order_async(
        "TSLA", 10, OrderSide.BUY, order_type=OrderType.LIMIT, limit_price=600.0
    ),
    expect_error=True,
)
```

Order-size controls stop both an oversized share count and an oversized notional before the
broker sees the request, which is what keeps a single mistyped instruction too small to matter.

## 4. Demonstration: Position Limits

SafeBroker enforces maximum position size and total exposure. This answers the portfolio-level question of
whether a sequence of valid orders can still push the account into an unsafe aggregate state.

```python
print("\n" + "=" * 70)
print("DEMO 2: Position Limits")
print("=" * 70)

# Isolate the share cap by setting the dollar limits well above the attempted position.
state_file = _temporary_state_path()
broker = MockBroker()
run_async(broker.connect())

config = LiveRiskConfig(
    execution_mode="paper",
    max_position_value=100_000.0,
    max_position_shares=100,
    max_total_exposure=200_000.0,
    max_order_value=20_000.0,
    max_order_shares=200,
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)
safe_broker.record_market_snapshot("AAPL", 100.0)

print("\n1. Share cap: start with 50 shares @ $100:")
run_demo(safe_broker.submit_order_async("AAPL", 50, OrderSide.BUY))
print("\n2. Share cap: adding 60 would reach 110 shares:")
run_demo(
    safe_broker.submit_order_async("AAPL", 60, OrderSide.BUY),
    expect_error=True,
)
```

A separate broker isolates the per-position dollar cap from the share and total-exposure gates.

```python
state_file = _temporary_state_path()
broker = MockBroker()
run_async(broker.connect())
config = LiveRiskConfig(
    execution_mode="paper",
    max_position_value=10_000.0,
    max_position_shares=200,
    max_total_exposure=200_000.0,
    max_order_value=20_000.0,
    max_order_shares=200,
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)
safe_broker.record_market_snapshot("MSFT", 150.0)

print("\n3. Position-value cap: 100 MSFT @ $150 would be $15,000:")
run_demo(
    safe_broker.submit_order_async(
        "MSFT", 100, OrderSide.BUY, order_type=OrderType.LIMIT, limit_price=150.0
    ),
    expect_error=True,
)
```

A third broker builds two valid positions before a new order crosses only the total-exposure cap.

```python
state_file = _temporary_state_path()
broker = MockBroker()
run_async(broker.connect())
config = LiveRiskConfig(
    execution_mode="paper",
    max_position_value=20_000.0,
    max_position_shares=200,
    max_total_exposure=15_000.0,
    max_order_value=20_000.0,
    max_order_shares=200,
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)
for symbol in ("AAPL", "MSFT", "GOOGL"):
    safe_broker.record_market_snapshot(symbol, 100.0)

print("\n4. Total exposure: build valid $5,000 and $8,000 positions:")
run_demo(safe_broker.submit_order_async("AAPL", 50, OrderSide.BUY))
run_demo(safe_broker.submit_order_async("MSFT", 80, OrderSide.BUY))
print("\n5. Total exposure: another $3,000 would raise exposure to $16,000:")
run_demo(
    safe_broker.submit_order_async("GOOGL", 30, OrderSide.BUY),
    expect_error=True,
)
```

Each scenario above uses its own broker so the share cap, the per-position value cap and the
total-exposure cap each raise on their own account, with no earlier control masking the
rejection under test. Portfolio-level caps exist because a failure often arrives as a sequence
of individually reasonable orders that add up to an unreasonable position.

## 5. Demonstration: Rate Limiting

SafeBroker limits the number of orders per minute. Rate limiting matters because repeated small errors can
be just as destructive as one oversized order when the loop is running unattended.

```python
print("\n" + "=" * 70)
print("DEMO 3: Rate Limiting")
print("=" * 70)

state_file = _temporary_state_path()
broker = MockBroker()
run_async(broker.connect())

config = LiveRiskConfig(
    execution_mode="paper",
    max_orders_per_minute=RATE_LIMIT_PER_MINUTE,
    max_order_value=50_000.0,
    max_position_value=100_000.0,
    dedup_window_seconds=0.0,  # Disable dedup for this demo
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)

print("\nSubmitting 5 orders rapidly (limit is 3/minute):")
symbols = ["AAPL", "MSFT", "GOOGL", "AMZN", "META"]  # Different symbols to avoid dedup
for sym in symbols:
    safe_broker.record_market_snapshot(sym, 100.0)
for i in range(5):
    print(f"\nOrder {i + 1} ({symbols[i]}):")
    run_demo(
        safe_broker.submit_order_async(symbols[i], 10, OrderSide.BUY),
        expect_error=(i >= 3),  # Expect error after 3rd order
    )
```

The burst is rejected on the fourth order even though every order in it passes every other
check. A throughput cap is what stands between the account and a runaway loop, a duplicated
signal, or a broker rate-limit ban earned by chatty execution code.

## 6. Demonstration: Asset Restrictions

SafeBroker can restrict trading to allowed assets or block specific assets. This is how a live system keeps
a strategy inside its approved mandate even if symbol selection logic goes wrong.

```python
print("\n" + "=" * 70)
print("DEMO 4: Asset Restrictions")
print("=" * 70)

state_file = _temporary_state_path()
broker = MockBroker()
run_async(broker.connect())

# Only allow specific ETFs
config = LiveRiskConfig(
    execution_mode="paper",
    allowed_assets={"SPY", "QQQ", "IWM"},  # Whitelist
    max_order_value=50_000.0,
    max_position_value=100_000.0,
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)
# Only prime SPY: AAPL is blocked at the asset-restriction check before staleness ever runs.
safe_broker.record_market_snapshot("SPY", 100.0)

print("\n1. Trading SPY (allowed):")
run_demo(safe_broker.submit_order_async("SPY", 10, OrderSide.BUY))

print("\n2. Trading AAPL (not in allowed list):")
run_demo(
    safe_broker.submit_order_async("AAPL", 10, OrderSide.BUY),
    expect_error=True,
)
```

Now exercise the blocklist path: trades to non-blocked assets are
accepted, trades to any listed symbol are rejected before they reach
the broker.

```python
print("\n--- Testing Blocked Assets ---")
state_file = _temporary_state_path()
broker = MockBroker()
run_async(broker.connect())

config = LiveRiskConfig(
    execution_mode="paper",
    blocked_assets={"TSLA", "GME", "AMC"},  # Blacklist volatile stocks
    max_order_value=50_000.0,
    max_position_value=100_000.0,
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)
# Only prime AAPL: GME is blocked at the asset-restriction check before staleness runs.
safe_broker.record_market_snapshot("AAPL", 100.0)

print("\n3. Trading AAPL (not blocked):")
run_demo(safe_broker.submit_order_async("AAPL", 10, OrderSide.BUY))

print("\n4. Trading GME (blocked):")
run_demo(
    safe_broker.submit_order_async("GME", 10, OrderSide.BUY),
    expect_error=True,
)
```

The allowlist and the blocklist put a hard boundary around what the strategy may touch. That
boundary is an operational safeguard rather than a research convenience: it is what stops a
symbol-selection bug from routing an order into an unsupported or explicitly banned instrument.

## 7. Demonstration: Kill Switch

The kill switch is an emergency halt that persists across restarts. It exists for the scenarios where the
safest action is to stop every new order until a human explicitly clears the system.

```python
print("\n" + "=" * 70)
print("DEMO 5: Kill Switch")
print("=" * 70)

state_file = _temporary_state_path()
broker = MockBroker()
run_async(broker.connect())

config = LiveRiskConfig(
    execution_mode="paper",
    max_order_value=50_000.0,
    max_position_value=100_000.0,
    dedup_window_seconds=0.0,  # Disable for demo
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)
safe_broker.record_market_snapshot("AAPL", 100.0)

print("\n1. Normal trading before kill switch:")
run_demo(safe_broker.submit_order_async("AAPL", 10, OrderSide.BUY))

print("\n2. Activating kill switch (manual emergency halt):")
safe_broker.enable_kill_switch("Manual test - simulating emergency")
print("   Kill switch activated!")

print("\n3. Attempting to trade with kill switch active:")
run_demo(
    safe_broker.submit_order_async("AAPL", 10, OrderSide.BUY),
    expect_error=True,
)
```

Reconstruct `SafeBroker` from the same state file. The in-memory toggle is
irrelevant if the latch does not come back with the new instance.

```python
print("\n4. Checking state persistence...")
safe_broker.close_persistence()
new_safe_broker = SafeBroker(MockBroker(), config)
print(f"   Kill switch still active: {new_safe_broker._state.kill_switch_activated}")
print(f"   Reason: {new_safe_broker._state.kill_switch_reason}")
assert new_safe_broker._state.kill_switch_activated

print("\n5. Disabling kill switch (manual recovery):")
new_safe_broker.disable_kill_switch()
print("   Kill switch disabled!")

print("\n6. Trading after recovery:")
new_safe_broker.record_market_snapshot("AAPL", 100.0)
_ = run_demo(new_safe_broker.submit_order_async("AAPL", 10, OrderSide.BUY))
new_safe_broker.close_persistence()
```

The second `SafeBroker` reads the latch from the state file, so the halt outlives the process
that set it rather than disappearing with the kernel. A halt that did not outlive its process
would let a restart quietly reactivate a strategy that was stopped for a real risk event.

## 8. Demonstration: Shadow Mode

Shadow mode logs orders but doesn't execute them. Uses VirtualPortfolio
for realistic position tracking to prevent infinite buy loops.

```python
print("\n" + "=" * 70)
print("DEMO 6: Shadow Mode (No Broker Submission)")
print("=" * 70)

state_file = _temporary_state_path()
broker = MockBroker()
run_async(broker.connect())

config = LiveRiskConfig(
    shadow_mode=True,  # Enable shadow mode
    max_order_value=50_000.0,
    max_position_value=100_000.0,
    state_file=state_file,
)
safe_broker = SafeBroker(broker, config)
safe_broker.record_market_snapshot("AAPL", 100.0)

print("\n1. Submitting order in shadow mode:")
order = run_demo(safe_broker.submit_order_async("AAPL", 100, OrderSide.BUY))

print("\n2. Checking virtual position (shadows real broker):")
pos = safe_broker.get_position("AAPL")
if pos:
    print(f"   Virtual position: {pos.quantity} shares @ ${pos.entry_price:.2f}")
else:
    raise AssertionError("Shadow order did not update the virtual position")

print("\n3. Checking real broker position (should be empty):")
real_pos = broker.get_position("AAPL")
if real_pos:
    raise AssertionError(f"Shadow order reached the real broker: {real_pos.quantity} shares")
else:
    print("   Real broker has NO position (correct - shadow mode)")

print("\n4. Virtual portfolio account value:")
value = run_async(safe_broker.get_account_value_async())
print(f"   Virtual account value: ${value:,.2f}")
```

Shadow mode updates the virtual portfolio while the underlying broker stays flat, so the whole
routing path is observable without any change to real inventory. That is what makes it the
first place to validate end-to-end routing, ahead of paper and well ahead of live.

## 9. Demonstration: VirtualPortfolio Details

VirtualPortfolio handles position tracking for shadow mode,
including weighted average cost basis and position flipping.

```python
print("\n" + "=" * 70)
print("DEMO 7: VirtualPortfolio Position Tracking")
print("=" * 70)

portfolio = VirtualPortfolio(initial_cash=100_000.0)

print("\n1. Initial state:")
print(f"   Cash: ${portfolio.cash:,.2f}")
print(f"   Positions: {len(portfolio.positions)}")

# Simulate buy order
buy_order = Order(
    asset="AAPL",
    side=OrderSide.BUY,
    quantity=100,
    filled_quantity=100,
    filled_price=150.0,
    status=OrderStatus.FILLED,
)
portfolio.process_fill(buy_order)

print("\n2. After buying 100 AAPL @ $150:")
pos = portfolio.positions.get("AAPL")
print(f"   Position: {pos.quantity} shares @ ${pos.entry_price:.2f}")
print(f"   Cash: ${portfolio.cash:,.2f}")
print(f"   Account value: ${portfolio.account_value:,.2f}")
assert pos.quantity == 100
assert pos.entry_price == 150.0
assert portfolio.account_value == 100_000.0
```

```python
# Add to position at higher price
buy_order2 = Order(
    asset="AAPL",
    side=OrderSide.BUY,
    quantity=100,
    filled_quantity=100,
    filled_price=160.0,
    status=OrderStatus.FILLED,
)
portfolio.process_fill(buy_order2)

print("\n3. After buying 100 more AAPL @ $160 (weighted avg cost):")
pos = portfolio.positions.get("AAPL")
print(f"   Position: {pos.quantity} shares @ ${pos.entry_price:.2f}")
print(f"   Cash: ${portfolio.cash:,.2f}")
assert pos.quantity == 200
assert pos.entry_price == 155.0

# Partial sell
sell_order = Order(
    asset="AAPL",
    side=OrderSide.SELL,
    quantity=50,
    filled_quantity=50,
    filled_price=170.0,
    status=OrderStatus.FILLED,
)
portfolio.process_fill(sell_order)

print("\n4. After selling 50 AAPL @ $170:")
pos = portfolio.positions.get("AAPL")
print(f"   Position: {pos.quantity} shares @ ${pos.entry_price:.2f}")
print(f"   Cash: ${portfolio.cash:,.2f}")
print(f"   Account value: ${portfolio.account_value:,.2f}")
assert pos.quantity == 150
assert pos.entry_price == 155.0
assert portfolio.account_value == 103_000.0
```

```python
for managed_path in MANAGED_STATE_PATHS:
    for candidate in (
        managed_path,
        managed_path.with_name(f"{managed_path.name}.lock"),
        managed_path.with_name(f"{managed_path.name}.head"),
    ):
        candidate.unlink(missing_ok=True)

unexpected_state_files = list(STATE_DIR.iterdir())
assert not unexpected_state_files, f"Temporary state residue: {unexpected_state_files}"
STATE_DIR.rmdir()
print(f"[OK] Cleaned {len(MANAGED_STATE_PATHS)} managed state and journal paths")
```

The virtual portfolio carries cost basis and partial exits explicitly rather than leaving them
to an implementation detail. A shadow or paper environment without that accounting hides the
state-management bugs it exists to surface, and those are the bugs that cost money live.

## Summary: Controls Exercised Above

| Control | What this notebook showed |
|---------|---------------------------|
| **Order Size Limits** | Max shares and value per order rejected before the broker sees them |
| **Position Limits** | Per-position value + share caps blocked unsafe accumulation |
| **Rate Limiting** | Burst of valid orders blocked at the per-minute cap |
| **Asset Restrictions** | Allowed / blocked asset lists held the universe boundary |
| **Kill Switch** | Manual halt, latch persisted across `SafeBroker` reconstruction |
| **Shadow Mode** | `VirtualPortfolio` tracked fills while the underlying broker stayed flat |

**Controls covered elsewhere**:
- **Duplicate Order Filter** and **Price Deviation** are configured through
  `LiveRiskConfig.dedup_window_seconds` and `LiveRiskConfig.max_price_deviation_pct`;
  this notebook does not present them as executed results.
- **Daily-Loss Drawdown Monitor** + **Stale-Data Rejection** are demonstrated in
  `13_runtime_safety_showcase`, which drives the same `SafeBroker` instance
  through both failure modes with full kill-switch latch.

**Additional features the demos rely on**:
- **State Persistence**: kill switch and daily counters survive restarts.
- **Atomic Writes**: the state file uses atomic JSON writes to prevent corruption.

**Best Practices**:
1. Always start with `shadow_mode=True`.
2. Graduate to paper trading.
3. Use conservative limits when going live.
4. Monitor the state file for kill-switch activations.

## Key Takeaways

1. **Defense-in-depth**: SafeBroker layers independent controls so that no
   single misconfiguration can produce an unchecked order. Six of those
   controls are exercised above; the remaining duplicate-order, fat-finger,
   and drawdown gates are wired through the same `LiveRiskConfig` surface
   without being claimed as outputs of this notebook.
2. **Configurable via LiveRiskConfig**: order size, position exposure, rate
   caps and drawdown thresholds are all parameters rather than hard-coded
   logic.
3. **Kill switch persists across restarts**: Emergency halts survive process
   restarts and must be manually cleared, preventing accidental
   reactivation of a halted strategy.
4. **Shadow mode with VirtualPortfolio**: Orders are logged and tracked with
   realistic cost-basis accounting without touching the broker, making it
   the safest first deployment step.

**Next**: Combine these controls with the parity checks in `08_pipeline_verification` and then keep the
same SafeBroker configuration when moving from shadow mode to paper trading.

Полный текст с указанием источника опубликован на условиях его лицензии. Лицензия: MIT

Это краткое изложение подготовлено исследовательским агентом Stratmill по оригиналу и не является его копией.