Files
Auto-Trader/agent/backtest/engines/brazil_equity.py
T
Haozhe Wu d3c76819c3 fix: complete October 11-12 maintenance and research updates
Fix live option guards, grounding operand evidence, source-bound benchmarks, trade artifacts, attribution, drawdowns, valuation tolerances, and SEC point-in-time factor inputs.

Add Brazil research data and market support, 13 fundamental and valuation factors, channel progress forwarding, and opt-in ZimaOS templates. Update seven README News sections with three visible dates.

Validation: 17,635 backend tests passed; 1,084 final targeted tests and 103 frontend tests passed. Frontend build, package build, documentation checks, and CI gates passed.
2026-10-12 02:29:46 +08:00

96 lines
3.9 KiB
Python

"""Brazil equity (B3) backtest engine.
Covers everything B3 lists under Yahoo's ``.SA`` suffix: shares (PETR4.SA),
units, ETFs (BOVA11.SA), FIIs (HGLG11.SA) and BDRs (AAPL34.SA). All settle in
BRL.
Market rules:
- Same-session round trips are allowed (day trade). Settlement is D+2, which
restricts cash, not the ability to sell shares bought today.
- Short selling is allowed (via the B3 securities-lending facility, BTC).
Set ``allow_short=False`` to model a long-only account.
- No hard daily price limits: B3 uses auction tunnels that pause trading
rather than blocking a price, so every bar is executable.
- Whole shares. The standard lot is 100, but the odd-lot (fractional)
market accepts single shares, so the default lot is 1. Set
``br_lot_size=100`` to trade only round lots.
- Price increment R$0.01, rounded against the trader.
Cost stack (all config-driven). B3 tariffs change; verify against the current
B3 price list before relying on absolute figures:
- B3 trading fee (emolumentos): 0.005% bilateral [br_trading_fee]
- B3 settlement fee (liquidação): 0.025% bilateral [br_settlement_fee]
- Broker commission: 0% (most retail brokers) [br_brokerage_rate]
- Fixed broker fee per order: R$0 [br_brokerage_fixed]
Income tax (15% swing / 20% day trade on net gains, monthly R$20k sales
exemption for shares) is assessed on the investor's monthly result, not per
trade, so it is not charged here.
"""
from __future__ import annotations
import math
import pandas as pd
from backtest.engines.base import BaseEngine
_TICK = 0.01
class BrazilEquityEngine(BaseEngine):
"""B3 cash-equity engine.
Config keys (all optional; defaults shown in the module docstring):
- allow_short: bool, default True
- slippage_br: default 0.0005
- br_lot_size: default 1
- br_trading_fee / br_settlement_fee / br_brokerage_rate /
br_brokerage_fixed
"""
def __init__(self, config: dict):
config = {**config, "leverage": config.get("leverage", 1.0)}
super().__init__(config)
self.allow_short: bool = bool(config.get("allow_short", True))
self.slippage_br: float = config.get("slippage_br", 0.0005)
self.br_lot_size: int = max(int(config.get("br_lot_size", 1)), 1)
self.br_trading_fee: float = config.get("br_trading_fee", 0.00005)
self.br_settlement_fee: float = config.get("br_settlement_fee", 0.00025)
self.br_brokerage_rate: float = config.get("br_brokerage_rate", 0.0)
self.br_brokerage_fixed: float = config.get("br_brokerage_fixed", 0.0)
def can_execute(self, symbol: str, direction: int, bar: pd.Series) -> bool:
"""Every bar is executable; only opening a short can be disabled."""
if direction == -1 and not self.allow_short:
return False
return True
def round_size(self, raw_size: float, price: float) -> float:
"""Floor to whole lots (1 share by default, 100 for round lots)."""
lot = self.br_lot_size
return float(math.floor(max(raw_size, 0.0) / lot) * lot)
def calc_commission(self, size: float, price: float, direction: int, is_open: bool) -> float:
"""B3 fees (bilateral) plus the configured broker commission."""
notional = size * price
if notional <= 0:
return 0.0
comm = notional * (self.br_trading_fee + self.br_settlement_fee)
comm += notional * self.br_brokerage_rate
comm += self.br_brokerage_fixed
return comm
def apply_slippage(self, price: float, direction: int) -> float:
"""Apply slippage, then snap to the R$0.01 grid against the trader."""
slipped = price * (1 + direction * self.slippage_br)
units = slipped / _TICK
if direction > 0:
steps = math.ceil(units - 1e-9)
elif direction < 0:
steps = math.floor(units + 1e-9)
else:
steps = round(units)
return round(steps * _TICK, 2)