feat(trader): models 数据类 + AccountConfig 费率(Issue#3)
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
"""模拟盘数据模型(immutable DTOs)。
|
||||
|
||||
数据类承载配置与订单/成交/拒单状态,跨模块传递时保持不可变。
|
||||
费率默认值见 §6.4(Issue #3 费率参数化)。
|
||||
"""
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class MatchSession(str, Enum):
|
||||
"""撮合时点(spec §6.1)。
|
||||
|
||||
- NEXT_OPEN:下一根 bar 的 open(收盘型策略,安全)
|
||||
- CURRENT_CLOSE:当根 bar 的 close(尾盘抓涨停型,策略不得用当根 OHLC)
|
||||
- CALL_AUCTION:集合竞价(首版预留)
|
||||
"""
|
||||
|
||||
NEXT_OPEN = "next_open"
|
||||
CURRENT_CLOSE = "current_close"
|
||||
CALL_AUCTION = "call_auction"
|
||||
|
||||
|
||||
class OrderSide(str, Enum):
|
||||
BUY = "buy"
|
||||
SELL = "sell"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AccountConfig:
|
||||
"""账户费率/撮合参数(Issue #3 全字段可配)。"""
|
||||
|
||||
initial_capital: float
|
||||
rate: float = 0.0003 # 佣金率
|
||||
min_commission: float = 5.0 # 最低佣金 5 元
|
||||
stamp_duty_rate: float = 0.0005 # 印花税(仅卖,2023.8.28 起 0.05%)
|
||||
transfer_fee_rate: float = 0.00001 # 过户费率(沪深双向 ×2,由 Account 计算)
|
||||
slippage: float = 0.0
|
||||
pricetick: float = 0.01
|
||||
size: float = 1.0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PaperOrder:
|
||||
"""策略下单请求。match_session 决定撮合时点。"""
|
||||
|
||||
strategy_id: str
|
||||
symbol: str
|
||||
side: OrderSide
|
||||
price: float
|
||||
volume: int
|
||||
is_market: bool = True
|
||||
match_session: MatchSession = MatchSession.NEXT_OPEN
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PaperTrade:
|
||||
"""已成交记录(含费用拆分)。transfer_fee 为单边,Account 扣款时 ×2。"""
|
||||
|
||||
strategy_id: str
|
||||
symbol: str
|
||||
side: OrderSide
|
||||
price: float
|
||||
volume: int
|
||||
commission: float
|
||||
stamp_duty: float
|
||||
transfer_fee: float
|
||||
bar_date: str
|
||||
match_session: MatchSession
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PaperReject:
|
||||
"""拒单记录。reason 枚举:limit_up_locked / limit_down_locked /
|
||||
limit_not_touched / volume_below_min_lot / unsupported_match_session /
|
||||
insufficient_cash / blocked_by_strategy=<id>。"""
|
||||
|
||||
strategy_id: str
|
||||
symbol: str
|
||||
reason: str
|
||||
bar_date: str
|
||||
@@ -0,0 +1,89 @@
|
||||
"""PaperAccount/Order/Trade/Reject 数据类测试。"""
|
||||
from sanguo_trader.models import (
|
||||
AccountConfig,
|
||||
PaperOrder,
|
||||
PaperTrade,
|
||||
PaperReject,
|
||||
MatchSession,
|
||||
OrderSide,
|
||||
)
|
||||
|
||||
|
||||
def test_account_config_defaults():
|
||||
cfg = AccountConfig(initial_capital=1_000_000)
|
||||
assert cfg.initial_capital == 1_000_000
|
||||
assert cfg.rate == 0.0003
|
||||
assert cfg.min_commission == 5.0
|
||||
assert cfg.stamp_duty_rate == 0.0005
|
||||
assert cfg.transfer_fee_rate == 0.00001
|
||||
assert cfg.slippage == 0
|
||||
assert cfg.pricetick == 0.01
|
||||
|
||||
|
||||
def test_account_config_is_frozen():
|
||||
import dataclasses
|
||||
|
||||
cfg = AccountConfig(initial_capital=1_000_000)
|
||||
assert dataclasses.is_dataclass(cfg)
|
||||
# frozen=True → 不可变
|
||||
try:
|
||||
cfg.rate = 0.001 # type: ignore[misc]
|
||||
except dataclasses.FrozenInstanceError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("AccountConfig 应为 frozen dataclass")
|
||||
|
||||
|
||||
def test_paper_order_defaults_next_open():
|
||||
o = PaperOrder(
|
||||
strategy_id="s1",
|
||||
symbol="600000",
|
||||
side=OrderSide.BUY,
|
||||
price=10.0,
|
||||
volume=100,
|
||||
is_market=True,
|
||||
)
|
||||
assert o.match_session == MatchSession.NEXT_OPEN
|
||||
|
||||
|
||||
def test_paper_order_explicit_current_close():
|
||||
o = PaperOrder(
|
||||
strategy_id="s1",
|
||||
symbol="600000",
|
||||
side=OrderSide.SELL,
|
||||
price=10.0,
|
||||
volume=100,
|
||||
is_market=False,
|
||||
match_session=MatchSession.CURRENT_CLOSE,
|
||||
)
|
||||
assert o.match_session == MatchSession.CURRENT_CLOSE
|
||||
assert o.is_market is False
|
||||
|
||||
|
||||
def test_paper_trade_fields():
|
||||
t = PaperTrade(
|
||||
strategy_id="s1",
|
||||
symbol="600000",
|
||||
side=OrderSide.BUY,
|
||||
price=10.0,
|
||||
volume=100,
|
||||
commission=5.0,
|
||||
stamp_duty=0.0,
|
||||
transfer_fee=0.01,
|
||||
bar_date="2024-01-02",
|
||||
match_session=MatchSession.NEXT_OPEN,
|
||||
)
|
||||
assert t.volume == 100
|
||||
assert t.commission == 5.0
|
||||
assert t.bar_date == "2024-01-02"
|
||||
|
||||
|
||||
def test_paperReject_records_reason():
|
||||
r = PaperReject(
|
||||
strategy_id="s1",
|
||||
symbol="600000",
|
||||
reason="limit_up_locked",
|
||||
bar_date="2024-01-02",
|
||||
)
|
||||
assert r.reason == "limit_up_locked"
|
||||
assert r.symbol == "600000"
|
||||
Reference in New Issue
Block a user