mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
feat(backtest): 成本模型拆分为佣金+印花税(仅卖出)+滑点 (#46)
* feat(backtest): 成本模型拆分为佣金+印花税(仅卖出)+滑点 MatcherConfig 新增 commission_pct/stamp_tax_pct 及 buy_cost_pct()/ sell_cost_pct() 方法, 撮合三处现场统一改用方法计算。 印花税单边(仅卖出)是 A 股与美股的本质差异: 旧的 fees_pct 双边模型 把印花税摊到买卖两腿, 会高估买入成本、低估卖出成本。拆分后买入腿= 佣金+滑点, 卖出腿=佣金+印花税+滑点。 commission_pct 未设时回退 fees_pct, 印花税未设时为 0, 完全向后兼容, 既有 12 个 portfolio 测试不变。strategy/api(含 SSE + 缓存键)全链透传。 新增 tests/backtest/test_cost_model.py 覆盖向后兼容/拆分/印花税仅卖出/ 优先级/撮合传导 5 个用例。 * fix(backtest): 打通成本模型前端链路并修复 cancel 缓存键 子代理审查发现 PR3 半成品缺陷, 本次补全: C1 (正确性): /strategy/cancel 的 _make_job_key 漏传 commission_pct/ stamp_tax_pct, 一旦用户设成本参数, cancel 算出的 job_key 与 stream 失配 导致取消静默失败。补 None-aware 解析对齐两侧口径。 C2 (完整性): 前端 SSE/sync 链未透传新字段, 新成本模型从 UI 够不到。 - backtestTask.ts / api.ts: 类型 + query 透传 commission_pct/stamp_tax_pct - StrategyBacktest.tsx: 佣金映射到 commission_pct, 新增印花税(千分之)输入, 默认 1 (A股千1), 映射 stamp_tax_pct; 状态持久化 - storage.ts: stampTax 类型声明 测试加固: - test_stamp_tax_only_deducts_on_sell_leg: 去掉硬编码 shares=9900, 改为断言两次运行 shares 相等 (守护买入腿 sizing 不受卖出成本污染) + 从结果反推卖出市值校验差额 - 新增 commission_pct=0.0 非 None 边界 (防 falsy 回退) - 新增 job_key 区分成本参数的回归测试 (守护 C1) 后端 26 测试全绿; 前端 tsc 仅剩既有 baseUrl deprecation (非本次引入)。
This commit is contained in:
@@ -191,6 +191,8 @@ class StrategyBacktestRequest(BaseModel):
|
||||
entry_fill: Literal["close_t", "open_t+1"] | None = None
|
||||
exit_fill: Literal["close_t", "open_t+1"] | None = None
|
||||
fees_pct: float = 0.0002
|
||||
commission_pct: float | None = None
|
||||
stamp_tax_pct: float | None = None
|
||||
slippage_bps: float = 5.0
|
||||
max_positions: int = 10
|
||||
max_exposure_pct: float = 1.0
|
||||
@@ -224,6 +226,8 @@ def strategy_run(req: StrategyBacktestRequest, request: Request):
|
||||
entry_fill=req.entry_fill,
|
||||
exit_fill=req.exit_fill,
|
||||
fees_pct=req.fees_pct,
|
||||
commission_pct=req.commission_pct,
|
||||
stamp_tax_pct=req.stamp_tax_pct,
|
||||
slippage_bps=req.slippage_bps,
|
||||
max_positions=req.max_positions,
|
||||
max_exposure_pct=req.max_exposure_pct,
|
||||
@@ -277,8 +281,9 @@ def _make_job_key(
|
||||
max_positions: int, max_exposure_pct: float, initial_capital: float, position_sizing: str,
|
||||
params: str | None, overrides: str | None,
|
||||
mode: str = "position", holding_days: int = 5,
|
||||
commission_pct: float | None = None, stamp_tax_pct: float | None = None,
|
||||
) -> str:
|
||||
raw = f"{strategy_id}|{symbols}|{start}|{end}|{matching}|{entry_fill}|{exit_fill}|{fees_pct}|{slippage_bps}|{max_positions}|{max_exposure_pct}|{initial_capital}|{position_sizing}|{params}|{overrides}|{mode}|{holding_days}"
|
||||
raw = f"{strategy_id}|{symbols}|{start}|{end}|{matching}|{entry_fill}|{exit_fill}|{fees_pct}|{slippage_bps}|{max_positions}|{max_exposure_pct}|{initial_capital}|{position_sizing}|{params}|{overrides}|{mode}|{holding_days}|{commission_pct}|{stamp_tax_pct}"
|
||||
return hashlib.md5(raw.encode()).hexdigest()[:12]
|
||||
|
||||
|
||||
@@ -293,6 +298,8 @@ async def strategy_stream(
|
||||
entry_fill: str | None = None,
|
||||
exit_fill: str | None = None,
|
||||
fees_pct: float = 0.0002,
|
||||
commission_pct: float | None = None,
|
||||
stamp_tax_pct: float | None = None,
|
||||
slippage_bps: float = 5.0,
|
||||
max_positions: int = 10,
|
||||
max_exposure_pct: float = 1.0,
|
||||
@@ -341,6 +348,7 @@ async def strategy_stream(
|
||||
fees_pct, slippage_bps, max_positions, max_exposure_pct, initial_capital, position_sizing,
|
||||
params, overrides,
|
||||
mode, holding_days,
|
||||
commission_pct, stamp_tax_pct,
|
||||
)
|
||||
|
||||
_cleanup_stale_jobs()
|
||||
@@ -374,6 +382,8 @@ async def strategy_stream(
|
||||
entry_fill=entry_fill,
|
||||
exit_fill=exit_fill,
|
||||
fees_pct=fees_pct,
|
||||
commission_pct=commission_pct,
|
||||
stamp_tax_pct=stamp_tax_pct,
|
||||
slippage_bps=slippage_bps,
|
||||
max_positions=int(max_positions),
|
||||
max_exposure_pct=float(max_exposure_pct),
|
||||
@@ -447,6 +457,10 @@ async def strategy_cancel(request: Request):
|
||||
p = parse_qs(qs)
|
||||
def _get(key: str, default: str = "") -> str:
|
||||
return p.get(key, [default])[0]
|
||||
def _get_opt_float(key: str) -> float | None:
|
||||
# 可选成本参数: 缺省或空串 → None (与 stream 侧 float | None 口径一致, 保证 job_key 对齐)。
|
||||
v = _get(key)
|
||||
return float(v) if v else None
|
||||
job_key = _make_job_key(
|
||||
_get("strategy_id"),
|
||||
_get("symbols") or None,
|
||||
@@ -465,6 +479,8 @@ async def strategy_cancel(request: Request):
|
||||
_get("overrides") or None,
|
||||
_get("mode", "position"),
|
||||
int(_get("holding_days", "5")),
|
||||
commission_pct=_get_opt_float("commission_pct"),
|
||||
stamp_tax_pct=_get_opt_float("stamp_tax_pct"),
|
||||
)
|
||||
job = _running_jobs.get(job_key)
|
||||
if job and not job.done:
|
||||
|
||||
@@ -34,7 +34,11 @@ class MatcherConfig:
|
||||
matching: Literal["close_t", "open_t+1"] = "close_t"
|
||||
entry_fill: Literal["close_t", "open_t+1"] | None = None
|
||||
exit_fill: Literal["close_t", "open_t+1"] | None = None
|
||||
# 成本模型: 优先使用拆分口径 (佣金双边 + 印花税仅卖出 + 滑点双边)。
|
||||
# 未设 commission_pct 时回退到 fees_pct 作为双边佣金 (向后兼容, 无印花税)。
|
||||
fees_pct: float = 0.0002
|
||||
commission_pct: float | None = None
|
||||
stamp_tax_pct: float | None = None
|
||||
slippage_bps: float = 5.0
|
||||
stop_loss_pct: float | None = None
|
||||
take_profit_pct: float | None = None
|
||||
@@ -56,6 +60,19 @@ class MatcherConfig:
|
||||
if self.exit_fill is None:
|
||||
self.exit_fill = self.matching
|
||||
|
||||
def _commission_pct(self) -> float:
|
||||
# commission_pct 显式给出时优先, 否则回退 fees_pct (向后兼容双边佣金)。
|
||||
return self.commission_pct if self.commission_pct is not None else self.fees_pct
|
||||
|
||||
def buy_cost_pct(self) -> float:
|
||||
# 买入腿: 佣金 + 滑点。
|
||||
return self._commission_pct() + self.slippage_bps / 10000.0
|
||||
|
||||
def sell_cost_pct(self) -> float:
|
||||
# 卖出腿: 佣金 + 印花税 + 滑点。印花税未设时为 0 (向后兼容)。
|
||||
stamp = self.stamp_tax_pct if self.stamp_tax_pct is not None else 0.0
|
||||
return self._commission_pct() + stamp + self.slippage_bps / 10000.0
|
||||
|
||||
|
||||
@dataclass
|
||||
class TradeRecord:
|
||||
@@ -340,7 +357,7 @@ class BacktestEngine:
|
||||
if exit_triggered:
|
||||
exit_price = float(sym_exit_prices[i])
|
||||
pnl_pct = (exit_price - entry_price) / entry_price if entry_price > 0 else 0.0
|
||||
fee_cost = config.fees_pct * 2 + config.slippage_bps / 10000.0 * 2
|
||||
fee_cost = config.buy_cost_pct() + config.sell_cost_pct()
|
||||
pnl_pct -= fee_cost
|
||||
|
||||
e_date = sym_dates[entry_idx]
|
||||
@@ -462,8 +479,8 @@ class BacktestEngine:
|
||||
row_pos_in_symbol[i] = len(rows)
|
||||
rows.append(i)
|
||||
|
||||
buy_cost_pct = config.fees_pct + config.slippage_bps / 10000.0
|
||||
sell_cost_pct = config.fees_pct + config.slippage_bps / 10000.0
|
||||
buy_cost_pct = config.buy_cost_pct()
|
||||
sell_cost_pct = config.sell_cost_pct()
|
||||
score_min = getattr(config, "score_min", None)
|
||||
score_max = getattr(config, "score_max", None)
|
||||
trades: list[TradeRecord] = []
|
||||
@@ -808,8 +825,8 @@ class BacktestEngine:
|
||||
if not all_dates:
|
||||
return self._empty_result()
|
||||
|
||||
buy_cost_pct = config.fees_pct + config.slippage_bps / 10000.0
|
||||
sell_cost_pct = config.fees_pct + config.slippage_bps / 10000.0
|
||||
buy_cost_pct = config.buy_cost_pct()
|
||||
sell_cost_pct = config.sell_cost_pct()
|
||||
cash = float(config.initial_capital)
|
||||
peak = cash
|
||||
max_positions = max(int(config.max_positions), 0)
|
||||
|
||||
@@ -35,6 +35,8 @@ class StrategyBacktestConfig:
|
||||
entry_fill: Literal["close_t", "open_t+1"] | None = None
|
||||
exit_fill: Literal["close_t", "open_t+1"] | None = None
|
||||
fees_pct: float = 0.0002
|
||||
commission_pct: float | None = None
|
||||
stamp_tax_pct: float | None = None
|
||||
slippage_bps: float = 5.0
|
||||
max_positions: int = 10
|
||||
max_exposure_pct: float = 1.0
|
||||
@@ -198,6 +200,8 @@ class StrategyBacktestService:
|
||||
entry_fill=config.entry_fill,
|
||||
exit_fill=config.exit_fill,
|
||||
fees_pct=config.fees_pct,
|
||||
commission_pct=config.commission_pct,
|
||||
stamp_tax_pct=config.stamp_tax_pct,
|
||||
slippage_bps=config.slippage_bps,
|
||||
stop_loss_pct=stop_loss,
|
||||
take_profit_pct=take_profit,
|
||||
@@ -653,6 +657,8 @@ class StrategyBacktestService:
|
||||
"entry_fill": c.entry_fill,
|
||||
"exit_fill": c.exit_fill,
|
||||
"fees_pct": c.fees_pct,
|
||||
"commission_pct": c.commission_pct,
|
||||
"stamp_tax_pct": c.stamp_tax_pct,
|
||||
"slippage_bps": c.slippage_bps,
|
||||
"max_positions": c.max_positions,
|
||||
"max_exposure_pct": c.max_exposure_pct,
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
"""成本模型拆分测试 — 佣金(双边) + 印花税(仅卖出) + 滑点(双边)。
|
||||
|
||||
覆盖:
|
||||
1. 向后兼容: 仅传 fees_pct 时, 买卖成本与旧行为完全一致 (无印花税)。
|
||||
2. 拆分模型: commission_pct / stamp_tax_pct / slippage_bps 各自参与, 印花税只在卖出侧。
|
||||
3. 优先级: 显式 commission_pct 覆盖 fees_pct。
|
||||
4. 撮合传导: 印花税只影响卖出腿, 且精度进入 TradeRecord。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
|
||||
import polars as pl
|
||||
|
||||
from app.backtest.engine import BacktestEngine, MatcherConfig
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 复用 portfolio 测试的最小面板/掩码构造
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
def _panel(symbols: list[str], days: int = 4, price: float = 10.0, overrides: dict | None = None) -> pl.DataFrame:
|
||||
overrides = overrides or {}
|
||||
start = date(2024, 1, 1)
|
||||
rows = []
|
||||
for sym in symbols:
|
||||
for i in range(days):
|
||||
patch = overrides.get((sym, i), {})
|
||||
rows.append({
|
||||
"symbol": sym,
|
||||
"name": sym,
|
||||
"date": start + timedelta(days=i),
|
||||
"open": patch.get("open", price),
|
||||
"high": patch.get("high", price),
|
||||
"low": patch.get("low", price),
|
||||
"close": patch.get("close", price),
|
||||
"volume": patch.get("volume", 100_000),
|
||||
"score": patch.get("score", 1),
|
||||
"signal_limit_up": patch.get("signal_limit_up", False),
|
||||
"signal_limit_down": patch.get("signal_limit_down", False),
|
||||
})
|
||||
return pl.DataFrame(rows).sort(["symbol", "date"])
|
||||
|
||||
|
||||
def _mask(panel: pl.DataFrame, marks: set[tuple[str, int]]) -> pl.Series:
|
||||
base = date(2024, 1, 1)
|
||||
values = []
|
||||
for row in panel.select(["symbol", "date"]).iter_rows(named=True):
|
||||
day = (row["date"] - base).days
|
||||
values.append((row["symbol"], day) in marks)
|
||||
return pl.Series(values, dtype=pl.Boolean)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 1. 单元测试: buy_cost_pct / sell_cost_pct
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
def test_legacy_fees_pct_keeps_symmetric_cost_without_stamp():
|
||||
"""仅传 fees_pct: 买卖成本相等, 均为 fees + slippage, 不含印花税 (旧行为)。"""
|
||||
cfg = MatcherConfig(fees_pct=0.0002, slippage_bps=5.0)
|
||||
assert cfg.buy_cost_pct() == 0.0002 + 0.0005
|
||||
assert cfg.sell_cost_pct() == 0.0002 + 0.0005 # 无印花税, 与买入对称
|
||||
|
||||
|
||||
def test_decomposed_cost_applies_stamp_only_on_sell():
|
||||
"""拆分模型: 佣金双边, 印花税仅卖出, 滑点双边。"""
|
||||
cfg = MatcherConfig(commission_pct=0.0003, stamp_tax_pct=0.001, slippage_bps=5.0)
|
||||
assert cfg.buy_cost_pct() == 0.0003 + 0.0005
|
||||
assert cfg.sell_cost_pct() == 0.0003 + 0.001 + 0.0005
|
||||
|
||||
|
||||
def test_commission_pct_overrides_fees_pct():
|
||||
"""同时给 fees_pct 与 commission_pct 时, 以 commission_pct 为准。"""
|
||||
cfg = MatcherConfig(fees_pct=0.0002, commission_pct=0.0009, slippage_bps=0)
|
||||
assert cfg.buy_cost_pct() == 0.0009
|
||||
assert cfg.sell_cost_pct() == 0.0009 # stamp 未设 → 0
|
||||
|
||||
|
||||
def test_commission_pct_zero_is_not_treated_as_unset():
|
||||
"""commission_pct=0.0 是有效值, 不应因 falsy 而回退到 fees_pct。"""
|
||||
cfg = MatcherConfig(fees_pct=0.0002, commission_pct=0.0, slippage_bps=0)
|
||||
assert cfg.buy_cost_pct() == 0.0
|
||||
assert cfg.sell_cost_pct() == 0.0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 2. 撮合传导: 印花税只影响卖出腿
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
def _round_trip_trade(cfg_kwargs: dict):
|
||||
"""价格恒定的一次买卖来回, 返回唯一成交的 TradeRecord。"""
|
||||
panel = _panel(
|
||||
["A"],
|
||||
days=3,
|
||||
overrides={
|
||||
("A", 1): {"open": 10, "high": 10, "low": 10, "close": 10},
|
||||
("A", 2): {"open": 10, "high": 10, "low": 10, "close": 10},
|
||||
},
|
||||
)
|
||||
entries = _mask(panel, {("A", 0)})
|
||||
exits = _mask(panel, set())
|
||||
result = BacktestEngine(repo=None).simulate_portfolio(
|
||||
panel,
|
||||
entries,
|
||||
exits,
|
||||
MatcherConfig(
|
||||
matching="open_t+1",
|
||||
max_positions=1,
|
||||
max_hold_days=1,
|
||||
initial_capital=100_000,
|
||||
**cfg_kwargs,
|
||||
),
|
||||
)
|
||||
assert len(result.trades) == 1
|
||||
return result.trades[0]
|
||||
|
||||
|
||||
def test_stamp_tax_only_deducts_on_sell_leg():
|
||||
"""价格不变时, 加印花税只影响卖出腿, 不改变买入腿的持仓股数。
|
||||
|
||||
本 PR 最需守护的不变量: 卖出成本不得反向污染买入腿的 sizing。
|
||||
因此断言 (a) 两次运行 shares 相等, (b) 亏损差额恰为 卖出市值 乘以 印花税率,
|
||||
卖出市值用反推的 shares 计算, 不硬编码 (避免 sizing 逻辑变动导致误报/漏报)。
|
||||
"""
|
||||
stamp = 0.001
|
||||
t_no_stamp = _round_trip_trade(dict(commission_pct=0.0003, stamp_tax_pct=0.0, slippage_bps=0))
|
||||
t_with_stamp = _round_trip_trade(dict(commission_pct=0.0003, stamp_tax_pct=stamp, slippage_bps=0))
|
||||
|
||||
# (a) 核心不变量: 买入腿由 buy_cost_pct 决定, 与印花税无关 → 股数必须一致。
|
||||
assert t_no_stamp.shares == t_with_stamp.shares
|
||||
shares = t_no_stamp.shares
|
||||
|
||||
# (b) 差额恰等于 卖出市值(shares * 卖出价 10) 乘以 印花税率。
|
||||
delta = t_no_stamp.pnl_amount - t_with_stamp.pnl_amount
|
||||
assert delta > 0
|
||||
assert abs(delta - shares * 10 * stamp) < 1e-6
|
||||
|
||||
|
||||
def test_independent_candidate_pnl_pct_includes_decomposed_costs():
|
||||
"""独立候选模式 (close_t): 价格不变时 pnl_pct == -(buy_cost + sell_cost)。"""
|
||||
panel = _panel(
|
||||
["A"],
|
||||
days=3,
|
||||
overrides={("A", 0): {"close": 10}, ("A", 1): {"close": 10}},
|
||||
)
|
||||
entries = _mask(panel, {("A", 0)})
|
||||
exits = _mask(panel, set())
|
||||
result = BacktestEngine(repo=None).simulate_independent_candidates(
|
||||
panel,
|
||||
entries,
|
||||
exits,
|
||||
MatcherConfig(matching="close_t", commission_pct=0.0003, stamp_tax_pct=0.001, slippage_bps=0, max_hold_days=1),
|
||||
)
|
||||
assert len(result.trades) == 1
|
||||
# buy_cost=0.0003, sell_cost=0.0003+0.001=0.0013 → 合计 -0.0016
|
||||
assert abs(result.trades[0].pnl_pct - (-(0.0003 + 0.0013))) < 1e-9
|
||||
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# 3. SSE 任务缓存键: 成本参数必须参与, 否则不同成本命中同一缓存 / cancel 失配
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
def test_job_key_distinguishes_commission_and_stamp():
|
||||
"""成本参数不同的两次回测必须得到不同 job_key (避免缓存碰撞与 cancel 失配)。"""
|
||||
from app.api.backtest import _make_job_key
|
||||
|
||||
base_args = ("s", None, None, None, "open_t+1", None, None, 0.0002, 5.0, 10, 1.0, 1e6, "equal", None, None, "position", 5)
|
||||
k_none = _make_job_key(*base_args)
|
||||
k_comm = _make_job_key(*base_args, commission_pct=0.0009)
|
||||
k_stamp = _make_job_key(*base_args, stamp_tax_pct=0.001)
|
||||
|
||||
assert k_none != k_comm
|
||||
assert k_none != k_stamp
|
||||
assert k_comm != k_stamp
|
||||
@@ -1174,6 +1174,8 @@ export const api = {
|
||||
entry_fill?: 'close_t' | 'open_t+1' | null
|
||||
exit_fill?: 'close_t' | 'open_t+1' | null
|
||||
fees_pct?: number
|
||||
commission_pct?: number
|
||||
stamp_tax_pct?: number
|
||||
slippage_bps?: number
|
||||
max_positions?: number
|
||||
initial_capital?: number
|
||||
|
||||
@@ -127,6 +127,8 @@ export function startBacktest(params: {
|
||||
entry_fill?: string
|
||||
exit_fill?: string
|
||||
fees_pct?: number
|
||||
commission_pct?: number
|
||||
stamp_tax_pct?: number
|
||||
slippage_bps?: number
|
||||
max_positions?: number
|
||||
max_exposure_pct?: number
|
||||
@@ -156,6 +158,8 @@ export function startBacktest(params: {
|
||||
entry_fill: params.entry_fill,
|
||||
exit_fill: params.exit_fill,
|
||||
fees_pct: params.fees_pct,
|
||||
commission_pct: params.commission_pct,
|
||||
stamp_tax_pct: params.stamp_tax_pct,
|
||||
slippage_bps: params.slippage_bps,
|
||||
max_positions: params.max_positions,
|
||||
max_exposure_pct: params.max_exposure_pct,
|
||||
|
||||
@@ -91,6 +91,7 @@ export const storage = {
|
||||
entryFill: 'close_t' | 'open_t+1'
|
||||
exitFill: 'close_t' | 'open_t+1'
|
||||
fees: string
|
||||
stampTax?: string
|
||||
slippage: string
|
||||
maxPositions: string
|
||||
maxExposure: string
|
||||
|
||||
@@ -662,6 +662,7 @@ export function StrategyBacktest() {
|
||||
const [entryFill, setEntryFill] = useState<'close_t' | 'open_t+1'>(saved?.entryFill ?? saved?.matching ?? 'open_t+1')
|
||||
const [exitFill, setExitFill] = useState<'close_t' | 'open_t+1'>(saved?.exitFill ?? saved?.matching ?? 'close_t')
|
||||
const [fees, setFees] = useState(saved?.fees ?? '2')
|
||||
const [stampTax, setStampTax] = useState(saved?.stampTax ?? '1')
|
||||
const [slippage, setSlippage] = useState(saved?.slippage ?? '5')
|
||||
const [maxPositions, setMaxPositions] = useState(saved?.maxPositions ?? '10')
|
||||
const [maxExposure, setMaxExposure] = useState(saved?.maxExposure ?? '100')
|
||||
@@ -768,6 +769,7 @@ export function StrategyBacktest() {
|
||||
entryFill,
|
||||
exitFill,
|
||||
fees,
|
||||
stampTax,
|
||||
slippage,
|
||||
maxPositions,
|
||||
maxExposure,
|
||||
@@ -792,7 +794,8 @@ export function StrategyBacktest() {
|
||||
matching,
|
||||
entry_fill: entryFill,
|
||||
exit_fill: exitFill,
|
||||
fees_pct: Number(fees) / 10000,
|
||||
commission_pct: Number(fees) / 10000,
|
||||
stamp_tax_pct: Number(stampTax) / 1000,
|
||||
slippage_bps: Number(slippage),
|
||||
max_positions: Number(maxPositions),
|
||||
max_exposure_pct: Number(maxExposure) / 100,
|
||||
@@ -1298,7 +1301,11 @@ export function StrategyBacktest() {
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-secondary block mb-1.5">佣金(万分之)</label>
|
||||
<input type="number" value={fees} onChange={e => setFees(e.target.value)} className={INPUT_CLS} />
|
||||
<input type="number" min={0} value={fees} onChange={e => setFees(e.target.value)} className={INPUT_CLS} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-secondary block mb-1.5">印花税(千分之)</label>
|
||||
<input type="number" min={0} value={stampTax} onChange={e => setStampTax(e.target.value)} className={INPUT_CLS} />
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs font-medium text-secondary block mb-1.5">滑点(万分之)</label>
|
||||
|
||||
Reference in New Issue
Block a user