quantification-cn 1.0.0__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.
- quantification_cn-1.0.0/PKG-INFO +10 -0
- quantification_cn-1.0.0/README.md +0 -0
- quantification_cn-1.0.0/pyproject.toml +30 -0
- quantification_cn-1.0.0/quantification_cn/__init__.py +7 -0
- quantification_cn-1.0.0/quantification_cn/asset/__init__.py +4 -0
- quantification_cn-1.0.0/quantification_cn/asset/cash.py +48 -0
- quantification_cn-1.0.0/quantification_cn/asset/etf.py +93 -0
- quantification_cn-1.0.0/quantification_cn/asset/exchange.py +10 -0
- quantification_cn-1.0.0/quantification_cn/asset/stock.py +97 -0
- quantification_cn-1.0.0/quantification_cn/benckmark.py +18 -0
- quantification_cn-1.0.0/quantification_cn/broker/__init__.py +2 -0
- quantification_cn-1.0.0/quantification_cn/broker/etf.py +204 -0
- quantification_cn-1.0.0/quantification_cn/broker/stock.py +217 -0
- quantification_cn-1.0.0/quantification_cn/data/__init__.py +3 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/__init__.py +1 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/akshare.py +17 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/delegate/__init__.py +6 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/delegate/macro_china_fdi.py +47 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/delegate/macro_china_lpr.py +44 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/delegate/macro_china_qyspjg.py +52 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/delegate/macro_china_shrzgm.py +48 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/delegate/macro_cnbs.py +48 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/delegate/stock_zh_a_hist.py +77 -0
- quantification_cn-1.0.0/quantification_cn/data/akshare/setting.py +5 -0
- quantification_cn-1.0.0/quantification_cn/data/api.py +7 -0
- quantification_cn-1.0.0/quantification_cn/data/api.pyi +24 -0
- quantification_cn-1.0.0/quantification_cn/data/field.py +292 -0
- quantification_cn-1.0.0/quantification_cn/data/spider/__init__.py +1 -0
- quantification_cn-1.0.0/quantification_cn/data/spider/delegate/__init__.py +1 -0
- quantification_cn-1.0.0/quantification_cn/data/spider/delegate/baidu_index.py +238 -0
- quantification_cn-1.0.0/quantification_cn/data/spider/setting.py +5 -0
- quantification_cn-1.0.0/quantification_cn/data/spider/spider.py +11 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/__init__.py +1 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/delegate/__init__.py +5 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/delegate/base.py +44 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/delegate/daily_basic.py +75 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/delegate/etf_share_size.py +61 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/delegate/fina_report.py +202 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/delegate/fund_daily.py +72 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/delegate/index_daily.py +63 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/delegate/stock_daily.py +73 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/setting.py +5 -0
- quantification_cn-1.0.0/quantification_cn/data/tushare/tushare.py +18 -0
- quantification_cn-1.0.0/quantification_cn/trader/__init__.py +2 -0
- quantification_cn-1.0.0/quantification_cn/trader/film.py +9 -0
- quantification_cn-1.0.0/quantification_cn/trader/order.py +76 -0
- quantification_cn-1.0.0/quantification_cn/trigger/__init__.py +10 -0
- quantification_cn-1.0.0/quantification_cn/trigger/cn_trade_day.py +21 -0
- quantification_cn-1.0.0/quantification_cn/utils.py +31 -0
- quantification_cn-1.0.0/quantification_cn.egg-info/PKG-INFO +10 -0
- quantification_cn-1.0.0/quantification_cn.egg-info/SOURCES.txt +53 -0
- quantification_cn-1.0.0/quantification_cn.egg-info/dependency_links.txt +1 -0
- quantification_cn-1.0.0/quantification_cn.egg-info/requires.txt +4 -0
- quantification_cn-1.0.0/quantification_cn.egg-info/top_level.txt +2 -0
- quantification_cn-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: quantification-cn
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: quantification中国股市交易模块
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: akshare>=1.18.21
|
|
8
|
+
Requires-Dist: pycryptodome>=3.23.0
|
|
9
|
+
Requires-Dist: quantification>=1.0.0
|
|
10
|
+
Requires-Dist: tushare>=1.4.24
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "quantification-cn"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "quantification中国股市交易模块"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"akshare>=1.18.21",
|
|
9
|
+
"pycryptodome>=3.23.0",
|
|
10
|
+
"quantification>=1.0.0",
|
|
11
|
+
"tushare>=1.4.24",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[dependency-groups]
|
|
15
|
+
dev = [
|
|
16
|
+
"ipykernel>=7.1.0",
|
|
17
|
+
"scipy>=1.17.0",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[build-system]
|
|
21
|
+
requires = ["setuptools", "wheel"]
|
|
22
|
+
build-backend = "setuptools.build_meta"
|
|
23
|
+
|
|
24
|
+
[tool.setuptools]
|
|
25
|
+
packages.find.where = ["."]
|
|
26
|
+
packages.find.exclude = ["temp*", "ouput*"]
|
|
27
|
+
|
|
28
|
+
[[tool.uv.index]]
|
|
29
|
+
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
|
30
|
+
default = true
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from datetime import date, time
|
|
2
|
+
|
|
3
|
+
from quantification import Cash
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RMB(Cash):
|
|
7
|
+
symbol = "RMB"
|
|
8
|
+
|
|
9
|
+
@classmethod
|
|
10
|
+
def class_name(cls) -> str:
|
|
11
|
+
return "人民币"
|
|
12
|
+
|
|
13
|
+
@property
|
|
14
|
+
def amount(self) -> float:
|
|
15
|
+
return self.value
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def extra(self) -> dict:
|
|
19
|
+
return {}
|
|
20
|
+
|
|
21
|
+
def available(self, *args, **kwargs) -> "RMB":
|
|
22
|
+
return self.copy
|
|
23
|
+
|
|
24
|
+
def liquidate(self, day: date, moment: time) -> float:
|
|
25
|
+
return self.value
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def copy(self) -> "RMB":
|
|
29
|
+
return self.__class__(self.value)
|
|
30
|
+
|
|
31
|
+
def __add__(self, other: "RMB") -> "RMB":
|
|
32
|
+
assert self == other, f"{type(other)}不为BaseCash[RMB]的实例,无法相加"
|
|
33
|
+
|
|
34
|
+
return self.__class__(self.value + other.value)
|
|
35
|
+
|
|
36
|
+
def __sub__(self, other: "RMB") -> "RMB":
|
|
37
|
+
assert self == other, f"{type(other)}不为BaseCash[RMB]的实例,无法相减"
|
|
38
|
+
|
|
39
|
+
return self.__class__(self.value - other.value)
|
|
40
|
+
|
|
41
|
+
def __eq__(self, other: "RMB") -> bool:
|
|
42
|
+
return isinstance(other, RMB)
|
|
43
|
+
|
|
44
|
+
def __init__(self, value: float):
|
|
45
|
+
self.value = value
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
__all__ = ["RMB"]
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import overload
|
|
2
|
+
from datetime import date
|
|
3
|
+
|
|
4
|
+
from quantification import ETF, Exchange
|
|
5
|
+
|
|
6
|
+
from .exchange import CnExchange
|
|
7
|
+
from ..utils import SharePosition, add_shares, sub_shares
|
|
8
|
+
|
|
9
|
+
etf_family: dict[str, type["CnETF"]] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CnETF(ETF):
|
|
13
|
+
"""
|
|
14
|
+
T+1结算规则
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def class_name(cls, *args, **kwargs) -> str:
|
|
19
|
+
return f"ETF{cls.code()}" if cls.symbol else "ETF"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def amount(self) -> float:
|
|
23
|
+
return sum(self.share_position.values())
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def extra(self) -> dict:
|
|
27
|
+
return {"position": {k.isoformat(): int(v) for k, v in self.share_position.items()}}
|
|
28
|
+
|
|
29
|
+
def available(self, day: date) -> "CnETF":
|
|
30
|
+
return self.__class__[self.code()]({d: s for d, s in self.share_position.items() if d.date() < day})
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def copy(self) -> "CnETF":
|
|
34
|
+
return self.__class__[self.code()](self.share_position.copy())
|
|
35
|
+
|
|
36
|
+
def __add__(self, other: "CnETF") -> "CnETF":
|
|
37
|
+
assert self == other, f"只有同种ETF可以相加减, {self.__class__} != {other.__class__}"
|
|
38
|
+
|
|
39
|
+
return self.__class__[self.code()](add_shares(self.share_position, other.share_position))
|
|
40
|
+
|
|
41
|
+
def __sub__(self, other: "CnETF") -> "CnETF":
|
|
42
|
+
assert self == other, f"只有同种ETF可以相加减, {self.__class__} != {other.__class__}"
|
|
43
|
+
|
|
44
|
+
return self.__class__[self.code()](sub_shares(self.share_position, other.amount))
|
|
45
|
+
|
|
46
|
+
def __eq__(self, other: "CnETF") -> bool:
|
|
47
|
+
return isinstance(other, CnETF) and self.code() == other.code()
|
|
48
|
+
|
|
49
|
+
def __init__(self, share: SharePosition = None):
|
|
50
|
+
assert self.code() is not None, "未指定ETF代码, 无法实例化"
|
|
51
|
+
self.share_position = share or {}
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def __class_getitem__(cls, symbol: str) -> "type[CnETF]":
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@overload
|
|
58
|
+
def __class_getitem__(cls, symbol_and_exchange: tuple[str, Exchange]) -> "type[CnETF]":
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def __class_getitem__(cls, arg: str | tuple[str, Exchange]) -> "type[CnETF]":
|
|
62
|
+
if isinstance(arg, str):
|
|
63
|
+
assert len(arg.split(".")) == 2, "格式: ETF['000001.SZ']"
|
|
64
|
+
symbol, exchange_str = arg.split(".")
|
|
65
|
+
|
|
66
|
+
exchange: Exchange | None = None
|
|
67
|
+
match exchange_str.upper():
|
|
68
|
+
case "SH":
|
|
69
|
+
exchange = CnExchange.SSE
|
|
70
|
+
case "SZ":
|
|
71
|
+
exchange = CnExchange.SZSE
|
|
72
|
+
|
|
73
|
+
if exchange is None:
|
|
74
|
+
raise ValueError(f"不支持的股票交易所后缀: {exchange_str}")
|
|
75
|
+
|
|
76
|
+
return cls[symbol, exchange]
|
|
77
|
+
|
|
78
|
+
symbol, exchange = arg
|
|
79
|
+
|
|
80
|
+
code = f"{symbol}.{exchange.code}"
|
|
81
|
+
|
|
82
|
+
if not etf_family.get(code):
|
|
83
|
+
# 动态创建新的股票类
|
|
84
|
+
etf_family[code] = type[CnETF](
|
|
85
|
+
f"ETF{symbol}",
|
|
86
|
+
(CnETF,),
|
|
87
|
+
{"symbol": symbol, "exchange": exchange},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return etf_family[code]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
__all__ = ["CnETF"]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from typing import overload
|
|
2
|
+
from datetime import date
|
|
3
|
+
|
|
4
|
+
from quantification import Stock, Exchange
|
|
5
|
+
|
|
6
|
+
from .exchange import CnExchange
|
|
7
|
+
from ..utils import SharePosition, add_shares, sub_shares
|
|
8
|
+
|
|
9
|
+
stock_family: dict[str, type["CnStock"]] = {}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CnStock(Stock):
|
|
13
|
+
"""
|
|
14
|
+
T+1结算规则
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@classmethod
|
|
18
|
+
def class_name(cls, *args, **kwargs) -> str:
|
|
19
|
+
return f"股票{cls.code()}" if cls.symbol else "股票"
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def amount(self) -> float:
|
|
23
|
+
return sum(self.share_position.values())
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def extra(self) -> dict:
|
|
27
|
+
return {"position": {k.isoformat(): int(v) for k, v in self.share_position.items()}}
|
|
28
|
+
|
|
29
|
+
def available(self, day: date) -> "CnStock":
|
|
30
|
+
return self.__class__[self.code()]({d: s for d, s in self.share_position.items() if d.date() < day})
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def copy(self) -> "CnStock":
|
|
34
|
+
return self.__class__[self.code()](self.share_position.copy())
|
|
35
|
+
|
|
36
|
+
def __add__(self, other: "CnStock") -> "CnStock":
|
|
37
|
+
assert self == other, f"只有同种股票可以相加减, {self.__class__} != {other.__class__}"
|
|
38
|
+
|
|
39
|
+
return self.__class__[self.code()](add_shares(self.share_position, other.share_position))
|
|
40
|
+
|
|
41
|
+
def __sub__(self, other: "CnStock") -> "CnStock":
|
|
42
|
+
assert self == other, f"只有同种股票可以相加减, {self.__class__} != {other.__class__}"
|
|
43
|
+
|
|
44
|
+
return self.__class__[self.code()](sub_shares(self.share_position, other.amount))
|
|
45
|
+
|
|
46
|
+
def __eq__(self, other: "CnStock") -> bool:
|
|
47
|
+
return isinstance(other, CnStock) and self.code() == other.code()
|
|
48
|
+
|
|
49
|
+
def __init__(self, share: SharePosition = None):
|
|
50
|
+
assert self.code() is not None, "未指定股票代码, 无法实例化"
|
|
51
|
+
self.share_position = share or {}
|
|
52
|
+
|
|
53
|
+
@overload
|
|
54
|
+
def __class_getitem__(cls, symbol: str) -> "type[CnStock]":
|
|
55
|
+
"""通过股票代码创建股票类,自动推断交易所"""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
@overload
|
|
59
|
+
def __class_getitem__(cls, symbol_and_exchange: tuple[str, Exchange]) -> "type[CnStock]":
|
|
60
|
+
"""通过股票代码和交易所创建股票类"""
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
def __class_getitem__(cls, arg: str | tuple[str, Exchange]) -> "type[CnStock]":
|
|
64
|
+
if isinstance(arg, str):
|
|
65
|
+
assert len(arg.split(".")) == 2, "格式: Stock['000001.SZ']"
|
|
66
|
+
symbol, exchange_str = arg.split(".")
|
|
67
|
+
|
|
68
|
+
exchange: Exchange | None = None
|
|
69
|
+
match exchange_str.upper():
|
|
70
|
+
case "SH":
|
|
71
|
+
exchange = CnExchange.SSE
|
|
72
|
+
case "SZ":
|
|
73
|
+
exchange = CnExchange.SZSE
|
|
74
|
+
case "BJ":
|
|
75
|
+
exchange = CnExchange.BSE
|
|
76
|
+
|
|
77
|
+
if exchange is None:
|
|
78
|
+
raise ValueError(f"不支持的股票交易所后缀: {exchange_str}")
|
|
79
|
+
|
|
80
|
+
return cls[symbol, exchange]
|
|
81
|
+
|
|
82
|
+
symbol, exchange = arg
|
|
83
|
+
|
|
84
|
+
code = f"{symbol}.{exchange.code}"
|
|
85
|
+
|
|
86
|
+
if not stock_family.get(code):
|
|
87
|
+
# 动态创建新的股票类
|
|
88
|
+
stock_family[code] = type[CnStock](
|
|
89
|
+
f"Stock{symbol}",
|
|
90
|
+
(CnStock,),
|
|
91
|
+
{"symbol": symbol, "exchange": exchange},
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return stock_family[code]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["CnStock"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from datetime import datetime, date
|
|
2
|
+
|
|
3
|
+
from .data.api import CnDataAPI
|
|
4
|
+
from .data.field import Field
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def benchmark(api: CnDataAPI, index_code: str, start: date, end: date) -> dict[datetime, float]:
|
|
8
|
+
res = api.query(
|
|
9
|
+
start_date=start,
|
|
10
|
+
end_date=end,
|
|
11
|
+
fields=[Field.IN_收盘点位],
|
|
12
|
+
index=index_code
|
|
13
|
+
)[Field.IN_收盘点位]
|
|
14
|
+
|
|
15
|
+
return {k.to_pydatetime(): v for k, v in res.items()}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
__all__ = ['benchmark']
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
from inspect import isclass
|
|
2
|
+
from datetime import date, time, datetime
|
|
3
|
+
|
|
4
|
+
from pandas import to_datetime
|
|
5
|
+
|
|
6
|
+
from quantification import (
|
|
7
|
+
Field,
|
|
8
|
+
BaseBroker,
|
|
9
|
+
Portfolio,
|
|
10
|
+
Result,
|
|
11
|
+
logger
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from ..asset import CnETF, RMB, CnExchange
|
|
15
|
+
from ..trader import CnETFOrder
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class CnETFBroker(BaseBroker[CnETF, RMB]):
|
|
19
|
+
def matchable(self, asset: type[CnETF]) -> bool:
|
|
20
|
+
assert isclass(asset), "asset必须为class"
|
|
21
|
+
|
|
22
|
+
if not issubclass(asset, CnETF):
|
|
23
|
+
logger.trace(f"{self}: {asset}不是ETF, 无法处理, 交给下一个Broker")
|
|
24
|
+
return False
|
|
25
|
+
|
|
26
|
+
if not asset.exchange in [CnExchange.SSE, CnExchange.SZSE]:
|
|
27
|
+
logger.trace(f"{self}: {asset}不属于上交所|深交所, 无法处理, 交给下一个Broker")
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
return True
|
|
31
|
+
|
|
32
|
+
def calculate_commission(self, trade_value: float, is_open: bool) -> float:
|
|
33
|
+
"""计算佣金"""
|
|
34
|
+
if is_open:
|
|
35
|
+
commission_rate = self.open_commission
|
|
36
|
+
else:
|
|
37
|
+
commission_rate = self.close_commission
|
|
38
|
+
|
|
39
|
+
commission = trade_value * commission_rate
|
|
40
|
+
return max(commission, self.min_commission)
|
|
41
|
+
|
|
42
|
+
def calculate_transfer_fee(self, trade_value: float) -> float:
|
|
43
|
+
"""计算过户费"""
|
|
44
|
+
return trade_value * self.transfer_fee_rate
|
|
45
|
+
|
|
46
|
+
def execute_order(self, order: CnETFOrder, portfolio: Portfolio) -> Result | None:
|
|
47
|
+
assert self.env is not None, f"{self}: 无法获取env"
|
|
48
|
+
|
|
49
|
+
# 根据交易时间确定使用的价格字段
|
|
50
|
+
field = None
|
|
51
|
+
match self.env.time:
|
|
52
|
+
case time(hour=9, minute=30):
|
|
53
|
+
field = Field.ETF_开盘价 # 开盘时使用开盘价
|
|
54
|
+
case time(hour=15, minute=0):
|
|
55
|
+
field = Field.ETF_收盘价 # 收盘时使用收盘价
|
|
56
|
+
|
|
57
|
+
if field is None:
|
|
58
|
+
raise ValueError(f"{self}: 只支持在9:30和15:00撮合交易")
|
|
59
|
+
|
|
60
|
+
# 查询股票价格数据
|
|
61
|
+
df = self.api.query(
|
|
62
|
+
self.start_date,
|
|
63
|
+
self.end_date,
|
|
64
|
+
[Field.ETF_开盘价, Field.ETF_收盘价],
|
|
65
|
+
etf=order.asset
|
|
66
|
+
)
|
|
67
|
+
try:
|
|
68
|
+
price = df.at[to_datetime(self.env.date), field]
|
|
69
|
+
except KeyError:
|
|
70
|
+
logger.warning(f"{self}: {order.asset}在 {self.env.date} {self.env.time} 无价格信息")
|
|
71
|
+
return None
|
|
72
|
+
|
|
73
|
+
logger.trace(f"{self}: 撮合交易{order}: 日期{self.env.date} 时间{self.env.time} 价格{price}")
|
|
74
|
+
|
|
75
|
+
match order.category:
|
|
76
|
+
case "buy":
|
|
77
|
+
# 买入交易处理
|
|
78
|
+
available_rmb = portfolio[RMB][0].amount
|
|
79
|
+
cost_factor = 1 + self.open_commission + self.transfer_fee_rate
|
|
80
|
+
|
|
81
|
+
# 计算实际交易股数
|
|
82
|
+
if order.share is not None:
|
|
83
|
+
share = order.share # 指定股数
|
|
84
|
+
elif order.value is not None:
|
|
85
|
+
share = order.value / price / cost_factor # 指定金额
|
|
86
|
+
else:
|
|
87
|
+
share = available_rmb / price / cost_factor # 全仓买入
|
|
88
|
+
|
|
89
|
+
share = int(share)
|
|
90
|
+
|
|
91
|
+
if share == 0:
|
|
92
|
+
logger.warning(f"{self}: 购买股数为0, 跳过买入指令")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
trade_value = price * share
|
|
96
|
+
|
|
97
|
+
# 计算手续费(ETF买入通常只有佣金,没有印花税,可能有过户费)
|
|
98
|
+
commission = self.calculate_commission(trade_value, is_open=True)
|
|
99
|
+
transfer_fee = self.calculate_transfer_fee(trade_value)
|
|
100
|
+
|
|
101
|
+
total_cost = trade_value + commission + transfer_fee
|
|
102
|
+
|
|
103
|
+
# 资金检查
|
|
104
|
+
if not self.allow_debt and total_cost > available_rmb:
|
|
105
|
+
logger.warning(f"{self}: 可用RMB不足以购买{share}股({available_rmb}<{total_cost}), 跳过买入指令")
|
|
106
|
+
return None
|
|
107
|
+
|
|
108
|
+
logger.success(
|
|
109
|
+
f"{self}: 成功购买{share}股{order.asset}, "
|
|
110
|
+
f"单价{price}, 交易金额{trade_value}, "
|
|
111
|
+
f"佣金{commission}, 过户费{transfer_fee}, 总成本{total_cost}"
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return Result(
|
|
115
|
+
order=order,
|
|
116
|
+
sold=[RMB(total_cost)], # 支出总成本(包括手续费)
|
|
117
|
+
brought=[order.asset({datetime.combine(self.env.date, self.env.time): share})], # 获得ETF
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
case "sell":
|
|
121
|
+
# 卖出交易处理
|
|
122
|
+
if etfs := portfolio[order.asset]:
|
|
123
|
+
available_share = etfs[0].available(self.env.date).amount
|
|
124
|
+
else:
|
|
125
|
+
available_share = 0
|
|
126
|
+
|
|
127
|
+
# 计算实际交易股数
|
|
128
|
+
if order.share is not None:
|
|
129
|
+
share = order.share # 指定股数
|
|
130
|
+
elif order.value is not None:
|
|
131
|
+
share = order.value / price # 指定金额
|
|
132
|
+
else:
|
|
133
|
+
share = available_share # 全仓卖出
|
|
134
|
+
|
|
135
|
+
share = int(share)
|
|
136
|
+
|
|
137
|
+
# 做空检查
|
|
138
|
+
if not self.allow_short and share > available_share:
|
|
139
|
+
logger.warning(f"{self}: 没有足够的股数卖出({available_share}>{order.share}), 跳过卖出指令")
|
|
140
|
+
return None
|
|
141
|
+
|
|
142
|
+
trade_value = price * share
|
|
143
|
+
|
|
144
|
+
# 计算手续费(ETF卖出通常有佣金、印花税和过户费)
|
|
145
|
+
commission = self.calculate_commission(trade_value, is_open=False)
|
|
146
|
+
transfer_fee = self.calculate_transfer_fee(trade_value)
|
|
147
|
+
stamp_tax = trade_value * self.stamp_tax_rate
|
|
148
|
+
|
|
149
|
+
net_proceeds = trade_value - commission - transfer_fee - stamp_tax
|
|
150
|
+
|
|
151
|
+
# 确保净收入不为负数
|
|
152
|
+
net_proceeds = max(net_proceeds, 0)
|
|
153
|
+
|
|
154
|
+
logger.success(
|
|
155
|
+
f"{self}: 成功出售{share}股{order.asset}, "
|
|
156
|
+
f"单价{price}, 交易金额{trade_value}, "
|
|
157
|
+
f"佣金{commission}, 过户费{transfer_fee}, 印花税{stamp_tax}, 净收入{net_proceeds}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
return Result(
|
|
161
|
+
order=order,
|
|
162
|
+
sold=[order.asset({datetime.combine(self.env.date, self.env.time): share})], # 支出ETF
|
|
163
|
+
brought=[RMB(net_proceeds)], # 获得净现金(扣除手续费后)
|
|
164
|
+
)
|
|
165
|
+
case _:
|
|
166
|
+
raise ValueError(f"{self}: 无效的订单类型: {order.category}")
|
|
167
|
+
|
|
168
|
+
def liquidate_asset(self, asset: CnETF, day: date, moment: time) -> RMB:
|
|
169
|
+
df = self.api.query(
|
|
170
|
+
self.start_date,
|
|
171
|
+
self.end_date,
|
|
172
|
+
[Field.ETF_收盘价],
|
|
173
|
+
etf=asset.__class__,
|
|
174
|
+
no_mask=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
close_price = df.iloc[df.index.searchsorted(to_datetime(day), side="left")][Field.ETF_收盘价]
|
|
178
|
+
|
|
179
|
+
return RMB(close_price * asset.amount)
|
|
180
|
+
|
|
181
|
+
def __init__(
|
|
182
|
+
self,
|
|
183
|
+
allow_debt: bool = False,
|
|
184
|
+
allow_short: bool = False,
|
|
185
|
+
open_commission: float = 0.0003, # 开仓佣金比例,默认万分之三
|
|
186
|
+
close_commission: float = 0.0003, # 平仓佣金比例,默认万分之三
|
|
187
|
+
min_commission: float = 0.1, # 最低佣金,默认0.1元(部分券商ETF交易无最低佣金或更低)
|
|
188
|
+
stamp_tax_rate: float = 0.001, # 印花税率,默认千分之一(卖出时收取)
|
|
189
|
+
transfer_fee_rate: float = 0.00002 # 过户费率,默认十万分之二(双边收取)
|
|
190
|
+
):
|
|
191
|
+
super().__init__()
|
|
192
|
+
|
|
193
|
+
self.allow_debt = allow_debt # 允许负头寸(融资)
|
|
194
|
+
self.allow_short = allow_short # 允许做空(融券)
|
|
195
|
+
|
|
196
|
+
# ETF交易费率参数(与股票有所不同)
|
|
197
|
+
self.open_commission = open_commission # 开仓佣金比例
|
|
198
|
+
self.close_commission = close_commission # 平仓佣金比例
|
|
199
|
+
self.min_commission = min_commission # 最低佣金金额
|
|
200
|
+
self.stamp_tax_rate = stamp_tax_rate # 印花税率(仅卖出时收取)
|
|
201
|
+
self.transfer_fee_rate = transfer_fee_rate # 过户费率(双边收取)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
__all__ = ["CnETFBroker"]
|