qka 2.2.3.dev3__tar.gz → 2.2.4.dev2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/PKG-INFO +1 -1
  2. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/backtest.py +15 -2
  3. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/broker.py +34 -7
  4. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/sizing.py +24 -4
  5. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/.github/workflows/release.yml +0 -0
  6. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/.gitignore +0 -0
  7. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/.vscode/settings.json +0 -0
  8. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/CHANGELOG.md +0 -0
  9. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/LICENSE +0 -0
  10. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/README.md +0 -0
  11. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/pyproject.toml +0 -0
  12. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/__init__.py +0 -0
  13. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/cli.py +0 -0
  14. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/__init__.py +0 -0
  15. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/accessor.py +0 -0
  16. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/analysis.py +0 -0
  17. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/data.py +0 -0
  18. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/indicator.py +0 -0
  19. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/report.py +0 -0
  20. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/core/strategy.py +0 -0
  21. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/utils/__init__.py +0 -0
  22. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/qka/utils/logger.py +0 -0
  23. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/skills/qka/SKILL.md +0 -0
  24. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/skills/qka/references/analysis.md +0 -0
  25. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/skills/qka/references/backtest.md +0 -0
  26. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/skills/qka/references/broker.md +0 -0
  27. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/skills/qka/references/data.md +0 -0
  28. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/skills/qka/references/sizing.md +0 -0
  29. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/skills/qka/references/strategy.md +0 -0
  30. {qka-2.2.3.dev3 → qka-2.2.4.dev2}/tools/generate_api_ref.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: qka
3
- Version: 2.2.3.dev3
3
+ Version: 2.2.4.dev2
4
4
  Summary: QKA(快量化 / Quant Kit for A-shares)- 简洁易用的 A 股量化回测框架
5
5
  Project-URL: Home, https://github.com/zsrl/qka
6
6
  Project-URL: Repository, https://github.com/zsrl/qka
@@ -79,7 +79,10 @@ class Backtest:
79
79
 
80
80
  def run(self, cash: float = 100000.0,
81
81
  start_date: str = None, end_date: str = None,
82
- benchmark: Optional[str] = None):
82
+ benchmark: Optional[str] = None,
83
+ commission_rate: float = None,
84
+ stamp_duty_rate: float = None,
85
+ slippage: float = None):
83
86
  """
84
87
  执行回测
85
88
 
@@ -95,13 +98,23 @@ class Backtest:
95
98
  end_date: 回测截止日期 YYYY-MM-DD,None 表示数据最晚日期
96
99
  benchmark: 基准代码,如 '000300.SH'(沪深300)。
97
100
  如果提供,会下载基准数据用于对比。
101
+ commission_rate: 佣金费率,None 表示用 Broker 默认(万 2.5,最低 5 元)
102
+ stamp_duty_rate: 印花税率(仅卖出),None 表示用 Broker 默认(万 5)
103
+ slippage: 滑点比率,None 表示用 Broker 默认(0.1%)
98
104
 
99
105
  Returns:
100
106
  None。回测结果保存在 self.results、self.metrics、self.trade_history 中。
101
107
  """
102
108
  # 注入基础设施
103
109
  self.initial_cash = cash
104
- self.strategy.broker = Broker(initial_cash=cash)
110
+ broker_kwargs = {'initial_cash': cash}
111
+ if commission_rate is not None:
112
+ broker_kwargs['commission_rate'] = commission_rate
113
+ if stamp_duty_rate is not None:
114
+ broker_kwargs['stamp_duty_rate'] = stamp_duty_rate
115
+ if slippage is not None:
116
+ broker_kwargs['slippage'] = slippage
117
+ self.strategy.broker = Broker(**broker_kwargs)
105
118
  self.strategy.sizing = SizingAccessor(self.strategy.broker)
106
119
  self.strategy._data = DataAccessor()
107
120
 
@@ -99,6 +99,39 @@ class Broker:
99
99
  'trades': list(self.trade_history),
100
100
  }
101
101
 
102
+ def _buy_cost(self, price: float, size: int):
103
+ """
104
+ 计算买入成本明细(滑点后执行价、成交额、佣金、总成本)。
105
+
106
+ 作为买入费用的唯一来源,供 buy() 与仓位计算 estimate_buy_cost() 复用,
107
+ 保证"预留费用"与"实际扣费"口径一致。
108
+
109
+ Returns:
110
+ tuple: (exec_price, amount, commission, total_cost)
111
+ """
112
+ exec_price = price * (1 + self.slippage)
113
+ amount = exec_price * size
114
+ if self.commission_rate > 0:
115
+ commission = max(amount * self.commission_rate, MIN_COMMISSION)
116
+ else:
117
+ commission = 0.0
118
+ return exec_price, amount, commission, amount + commission
119
+
120
+ def estimate_buy_cost(self, price: float, size: int) -> float:
121
+ """
122
+ 预估买入总成本(含滑点与佣金)。
123
+
124
+ 供仓位计算预留费用使用,口径与 buy() 完全一致。
125
+
126
+ Args:
127
+ price (float): 市价
128
+ size (int): 拟买入数量
129
+
130
+ Returns:
131
+ float: 总成本(成交额 + 佣金)
132
+ """
133
+ return self._buy_cost(price, size)[3]
134
+
102
135
  def buy(self, symbol: str, price: float, size: int) -> bool:
103
136
  """
104
137
  买入操作
@@ -121,13 +154,7 @@ class Broker:
121
154
  logger.warning(f"价格 {price:.2f} 不合法(前复权可能导致早期价格为负),跳过买入 {symbol}")
122
155
  return False
123
156
 
124
- exec_price = price * (1 + self.slippage)
125
- amount = exec_price * size
126
- if self.commission_rate > 0:
127
- commission = max(amount * self.commission_rate, MIN_COMMISSION)
128
- else:
129
- commission = 0.0
130
- total_cost = amount + commission
157
+ exec_price, amount, commission, total_cost = self._buy_cost(price, size)
131
158
 
132
159
  if self.cash < total_cost:
133
160
  logger.debug(f"资金不足!需要 {total_cost:.2f}(佣金 {commission:.2f}),当前可用 {self.cash:.2f}")
@@ -63,10 +63,14 @@ class SizingAccessor:
63
63
  """
64
64
  固定金额。
65
65
 
66
- 计算 amount 能买多少股,向下按手取整。
66
+ 计算 amount 能买多少股(预留滑点与佣金,保证买入总成本不超过 amount),
67
+ 向下按手取整。
68
+
69
+ 注意:amount 为「总成本」上限(成交额 + 佣金),非纯「成交额」;
70
+ 与 percent() 同口径,避免满仓时因费用超支被 Broker 拒单。
67
71
 
68
72
  Args:
69
- amount: 投入金额
73
+ amount: 投入金额上限
70
74
  price: 每股价格
71
75
 
72
76
  Returns:
@@ -74,7 +78,13 @@ class SizingAccessor:
74
78
  """
75
79
  self._validate_positive(amount, 'amount')
76
80
  self._validate_positive(price, 'price')
77
- return self._round_lot(amount / price)
81
+ # 以含滑点的成交价估算可买股数,再逐手下调,
82
+ # 确保「成交额 + 佣金」不超过 amount(与 Broker.buy 口径一致)。
83
+ exec_price = price * (1 + self._broker.slippage)
84
+ size = self._round_lot(amount / exec_price)
85
+ while size > 0 and self._broker.estimate_buy_cost(price, size) > amount:
86
+ size -= self.MIN_LOT
87
+ return size
78
88
 
79
89
  def percent(self, ratio: float, price: float) -> int:
80
90
  """
@@ -82,6 +92,9 @@ class SizingAccessor:
82
92
 
83
93
  使用可用现金的 ratio 比例买入,按手取整。
84
94
 
95
+ 已预留滑点与佣金:返回的股数保证买入总成本(含费用)不超过
96
+ cash * ratio,避免满仓时因费用超支被 Broker 拒单。
97
+
85
98
  Args:
86
99
  ratio: 0~1 之间的比例,如 0.1 表示 10%
87
100
  price: 每股价格
@@ -93,7 +106,14 @@ class SizingAccessor:
93
106
  self._validate_positive(price, 'price')
94
107
  if self._broker.cash <= 0:
95
108
  return 0
96
- return self._round_lot(self._broker.cash * ratio / price)
109
+ budget = self._broker.cash * ratio
110
+ # 以含滑点的成交价估算可买股数,再逐手下调,
111
+ # 确保「成交额 + 佣金」不超过预算(与 Broker.buy 口径一致)。
112
+ exec_price = price * (1 + self._broker.slippage)
113
+ size = self._round_lot(budget / exec_price)
114
+ while size > 0 and self._broker.estimate_buy_cost(price, size) > budget:
115
+ size -= self.MIN_LOT
116
+ return size
97
117
 
98
118
  def atr_risk(self, risk_ratio: float, price: float, atr_value: float,
99
119
  multiplier: float = 2.0) -> int:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes