diff --git a/src/risk/position_sizer.py b/src/risk/position_sizer.py index caedb3d..35a8f04 100644 --- a/src/risk/position_sizer.py +++ b/src/risk/position_sizer.py @@ -16,6 +16,8 @@ import numpy as np +from src.utils.ashare_constants import LOT_SIZE, MAX_SINGLE_POSITION + logger = logging.getLogger(__name__) @@ -39,8 +41,8 @@ class SizingResult: class PositionSizingConfig: """Configuration for position sizing.""" - max_single_weight: float = 0.30 - min_lot_size: int = 100 + max_single_weight: float = MAX_SINGLE_POSITION + min_lot_size: int = LOT_SIZE kelly_fraction: float = 0.25 target_volatility: float = 0.15 max_leverage: float = 1.0 diff --git a/src/strategy/base.py b/src/strategy/base.py index 99d75a2..10e72a0 100644 --- a/src/strategy/base.py +++ b/src/strategy/base.py @@ -12,6 +12,12 @@ import pandas as pd +from src.utils.ashare_constants import ( + COMMISSION_RATE, + LOT_SIZE, + PRICE_LIMITS, + STAMP_DUTY_RATE, +) from src.utils.config import load_config from src.utils.logger import get_logger @@ -27,12 +33,9 @@ BOARD_CHINEXT: str = "chinext" BOARD_STAR: str = "star" -# Price limit percentages by board type -PRICE_LIMITS: dict[str, float] = { - BOARD_MAIN: 0.10, - BOARD_CHINEXT: 0.20, - BOARD_STAR: 0.20, -} +# Price limits by board live in the canonical constants module and are imported +# above (re-exported here for back-compat; backtest/engine imports PRICE_LIMITS +# from this module). class BaseStrategy(ABC): @@ -50,10 +53,14 @@ class BaseStrategy(ABC): def __init__(self, config_path: str = "strategy") -> None: self.config: dict[str, Any] = load_config(config_path) self.common_config: dict[str, Any] = self.config.get("common", {}) - self.min_lot_size: int = self.common_config.get("min_lot_size", 100) - self.commission_rate: float = self.common_config.get("commission_rate", 0.0003) + self.min_lot_size: int = self.common_config.get("min_lot_size", LOT_SIZE) + self.commission_rate: float = self.common_config.get( + "commission_rate", COMMISSION_RATE + ) self.min_commission: float = self.common_config.get("min_commission", 5.0) - self.stamp_tax_rate: float = self.common_config.get("stamp_tax_rate", 0.001) + self.stamp_tax_rate: float = self.common_config.get( + "stamp_tax_rate", STAMP_DUTY_RATE + ) self.stop_loss: float = self.common_config.get("stop_loss", 0.08) self.take_profit: float = self.common_config.get("take_profit", 0.15) self.position_size: float = self.common_config.get("position_size", 0.3) diff --git a/src/trading/constraints.py b/src/trading/constraints.py index 9175e29..94984e7 100644 --- a/src/trading/constraints.py +++ b/src/trading/constraints.py @@ -15,13 +15,18 @@ from datetime import date, datetime from typing import Any +from src.utils.ashare_constants import ( + CHINEXT_PREFIXES, + COMMISSION_RATE, + LOT_SIZE, + STAMP_DUTY_RATE, + STAR_PREFIXES, +) from src.utils.config import load_config logger = logging.getLogger(__name__) -# A-share exchange prefixes -CHINEXT_PREFIXES = ("300", "301") # 创业板 -STAR_PREFIXES = ("688", "689") # 科创板 +# A-share exchange prefixes (CHINEXT/STAR from the canonical constants module) BSE_PREFIXES = ("8",) # 北交所 MAIN_BOARD_SH = ("600", "601", "603", "605") # 沪市主板 MAIN_BOARD_SZ = ("000", "001", "002", "003") # 深市主板 @@ -71,9 +76,9 @@ def __init__(self, config: dict[str, Any] | None = None) -> None: ) self._max_chase_pct = self._config.get("max_intraday_chase_pct", 5.0) self._min_daily_turnover_wan = self._config.get("min_daily_turnover_wan", 5000) - self._lot_size = self._config.get("lot_size", 100) - self._commission_rate = self._config.get("commission_rate", 0.0003) - self._stamp_duty_rate = self._config.get("stamp_duty_rate", 0.001) + self._lot_size = self._config.get("lot_size", LOT_SIZE) + self._commission_rate = self._config.get("commission_rate", COMMISSION_RATE) + self._stamp_duty_rate = self._config.get("stamp_duty_rate", STAMP_DUTY_RATE) self._min_commission = self._config.get("min_commission", 5.0) logger.info("TradingConstraintsEngine initialized") diff --git a/src/utils/ashare_constants.py b/src/utils/ashare_constants.py new file mode 100644 index 0000000..bbc5584 --- /dev/null +++ b/src/utils/ashare_constants.py @@ -0,0 +1,39 @@ +"""Canonical A-share market constants — single source of truth. + +The lot size, commission / stamp-duty rates, max-position cap, and board daily +price limits were previously re-declared as literal defaults in several modules +(``trading/constraints``, ``strategy/base``, ``backtest/engine``, +``risk/position_sizer``, ``agent_loop/ashare_constraints``). That duplication is +a drift hazard. Import these instead of re-hardcoding the values. + +Scope (#47): this consolidates the shared *constants*. Each module's board +*detection* logic and rule application still live locally — unifying those is a +deeper, behavior-sensitive follow-up tracked on the same issue. +""" + +from __future__ import annotations + +# Minimum tradable unit: 1 lot = 100 shares (1 手 = 100 股). +LOT_SIZE: int = 100 + +# Transaction costs. +COMMISSION_RATE: float = 0.0003 # brokerage commission (both sides) +STAMP_DUTY_RATE: float = 0.001 # stamp duty (sell side only) + +# Default max weight for a single position. +MAX_SINGLE_POSITION: float = 0.30 + +# Daily price-limit magnitude by board, as a decimal fraction. +# main board ±10% +# ChiNext / STAR ±20% +# BSE ±30% +PRICE_LIMITS: dict[str, float] = { + "main": 0.10, + "chinext": 0.20, + "star": 0.20, + "bse": 0.30, +} + +# Board code prefixes (6-digit A-share codes). +CHINEXT_PREFIXES: tuple[str, ...] = ("300", "301") +STAR_PREFIXES: tuple[str, ...] = ("688", "689")