quantification 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl
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/__init__.py +3 -2
- quantification/api/__init__.py +3 -0
- quantification/api/akshare/__init__.py +1 -0
- quantification/api/akshare/akshare.py +17 -0
- quantification/api/akshare/delegate/__init__.py +6 -0
- quantification/api/akshare/delegate/macro_china_fdi.py +46 -0
- quantification/api/akshare/delegate/macro_china_lpr.py +43 -0
- quantification/api/akshare/delegate/macro_china_qyspjg.py +51 -0
- quantification/api/akshare/delegate/macro_china_shrzgm.py +47 -0
- quantification/api/akshare/delegate/macro_cnbs.py +47 -0
- quantification/api/akshare/delegate/stock_zh_a_hist.py +77 -0
- quantification/api/akshare/setting.py +5 -0
- quantification/api/api.py +11 -0
- quantification/api/api.pyi +21 -0
- quantification/api/tushare/__init__.py +1 -0
- quantification/api/tushare/delegate/__init__.py +7 -0
- quantification/api/tushare/delegate/balancesheet.py +66 -0
- quantification/api/tushare/delegate/cashflow.py +29 -0
- quantification/api/tushare/delegate/common.py +64 -0
- quantification/api/tushare/delegate/daily_basic.py +81 -0
- quantification/api/tushare/delegate/fina_indicator.py +20 -0
- quantification/api/tushare/delegate/income.py +34 -0
- quantification/api/tushare/delegate/index_daily.py +61 -0
- quantification/api/tushare/delegate/pro_bar.py +80 -0
- quantification/api/tushare/setting.py +5 -0
- quantification/api/tushare/tushare.py +17 -0
- quantification/core/__init__.py +9 -0
- quantification/core/asset/__init__.py +6 -0
- quantification/core/asset/base_asset.py +96 -0
- quantification/core/asset/base_broker.py +42 -0
- quantification/core/asset/broker.py +108 -0
- quantification/core/asset/cash.py +75 -0
- quantification/core/asset/stock.py +268 -0
- quantification/core/cache.py +93 -0
- quantification/core/configure.py +15 -0
- quantification/core/data/__init__.py +5 -0
- quantification/core/data/base_api.py +109 -0
- quantification/core/data/base_delegate.py +73 -0
- quantification/core/data/field.py +213 -0
- quantification/core/data/panel.py +42 -0
- quantification/core/env.py +25 -0
- quantification/core/logger.py +94 -0
- quantification/core/strategy/__init__.py +3 -0
- quantification/core/strategy/base_strategy.py +66 -0
- quantification/core/strategy/base_trigger.py +69 -0
- quantification/core/strategy/base_use.py +69 -0
- quantification/core/trader/__init__.py +7 -0
- quantification/core/trader/base_order.py +45 -0
- quantification/core/trader/base_stage.py +16 -0
- quantification/core/trader/base_trader.py +173 -0
- quantification/core/trader/collector.py +47 -0
- quantification/core/trader/order.py +23 -0
- quantification/core/trader/portfolio.py +72 -0
- quantification/core/trader/query.py +29 -0
- quantification/core/trader/report.py +76 -0
- quantification/core/util.py +181 -0
- quantification/default/__init__.py +5 -0
- quantification/default/stage/__init__.py +1 -0
- quantification/default/stage/cn_stock.py +23 -0
- quantification/default/strategy/__init__.py +1 -0
- quantification/default/strategy/simple/__init__.py +1 -0
- quantification/default/strategy/simple/strategy.py +8 -0
- quantification/default/trader/__init__.py +2 -0
- quantification/default/trader/a_factor/__init__.py +1 -0
- quantification/default/trader/a_factor/trader.py +27 -0
- quantification/default/trader/simple/__init__.py +1 -0
- quantification/default/trader/simple/trader.py +8 -0
- quantification/default/trigger/__init__.py +1 -0
- quantification/default/trigger/trigger.py +63 -0
- quantification/default/use/__init__.py +1 -0
- quantification/default/use/factors/__init__.py +2 -0
- quantification/default/use/factors/factor.py +205 -0
- quantification/default/use/factors/use.py +38 -0
- quantification-0.1.1.dist-info/METADATA +19 -0
- quantification-0.1.1.dist-info/RECORD +76 -0
- {quantification-0.1.0.dist-info → quantification-0.1.1.dist-info}/WHEEL +1 -1
- quantification-0.1.0.dist-info/METADATA +0 -13
- quantification-0.1.0.dist-info/RECORD +0 -4
@@ -0,0 +1,173 @@
|
|
1
|
+
from tqdm import tqdm
|
2
|
+
from typing import TypeVar, Generic, TYPE_CHECKING
|
3
|
+
from datetime import date, timedelta, datetime
|
4
|
+
|
5
|
+
from .query import Query
|
6
|
+
from .report import AssetData, OrderResultData, PeriodData, PointData, BenchmarkData, ReportData
|
7
|
+
from .portfolio import Portfolio
|
8
|
+
from .collector import Collector
|
9
|
+
from .base_order import Result, BaseOrder
|
10
|
+
from .base_stage import BaseStage
|
11
|
+
from ..asset.cash import RMB
|
12
|
+
from ..data.field import Field
|
13
|
+
|
14
|
+
from ..env import EnvGetter, Env
|
15
|
+
from ..logger import logger
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from ..data import BaseAPI
|
19
|
+
from ..asset import BaseBroker, BaseAsset
|
20
|
+
from ..strategy import BaseStrategy
|
21
|
+
|
22
|
+
StrategyType = TypeVar("StrategyType", bound="BaseStrategy")
|
23
|
+
|
24
|
+
|
25
|
+
class BaseTrader(Generic[StrategyType]):
|
26
|
+
def __init__(
|
27
|
+
self,
|
28
|
+
api: "BaseAPI",
|
29
|
+
init_portfolio: Portfolio,
|
30
|
+
start_date: date,
|
31
|
+
end_date: date,
|
32
|
+
padding: int,
|
33
|
+
stage: type[BaseStage],
|
34
|
+
strategy: StrategyType,
|
35
|
+
brokers: list[type["BaseBroker"]]
|
36
|
+
):
|
37
|
+
self.api = api
|
38
|
+
self.start_date = start_date
|
39
|
+
self.end_date = end_date
|
40
|
+
self.stage = stage
|
41
|
+
self.strategy = strategy
|
42
|
+
self.portfolio = init_portfolio
|
43
|
+
self.brokers = [broker(api, start_date - timedelta(days=padding), end_date, stage) for broker in brokers]
|
44
|
+
self.query = Query(api, start_date - timedelta(days=padding), end_date)
|
45
|
+
|
46
|
+
self.collector = Collector()
|
47
|
+
self.env: Env | None = None
|
48
|
+
EnvGetter.getter = lambda: self.env
|
49
|
+
|
50
|
+
def match_broker(self, asset: "BaseAsset") -> "BaseBroker|None":
|
51
|
+
for candidate_broker in self.brokers:
|
52
|
+
if candidate_broker.matchable(asset):
|
53
|
+
return candidate_broker
|
54
|
+
|
55
|
+
return None
|
56
|
+
|
57
|
+
@property
|
58
|
+
def timeline(self):
|
59
|
+
current_date = self.start_date
|
60
|
+
while current_date <= self.end_date:
|
61
|
+
for current_stage in self.stage:
|
62
|
+
self.env = Env(date=current_date, time=current_stage.time)
|
63
|
+
self.collector.commence(current_date, current_stage, self.portfolio)
|
64
|
+
yield current_date, current_stage
|
65
|
+
current_date += timedelta(days=1)
|
66
|
+
|
67
|
+
def run(self):
|
68
|
+
for day, stage in self.timeline:
|
69
|
+
logger.trace(f"==========日期:{day}===阶段:{stage}==========")
|
70
|
+
params = {
|
71
|
+
"day": day,
|
72
|
+
"stage": stage,
|
73
|
+
"portfolio": self.portfolio,
|
74
|
+
"context": self.strategy.context,
|
75
|
+
"query": self.query,
|
76
|
+
"trader": self,
|
77
|
+
"strategy": self.strategy
|
78
|
+
}
|
79
|
+
|
80
|
+
hooks = self.strategy.triggered(**params)
|
81
|
+
logger.trace(f"触发的全部hooks:{hooks}")
|
82
|
+
|
83
|
+
for hook in hooks:
|
84
|
+
logger.trace(f"开始运行{hook}")
|
85
|
+
gen = hook(**params)
|
86
|
+
order: BaseOrder | None = None
|
87
|
+
result: Result | None = None
|
88
|
+
|
89
|
+
while True:
|
90
|
+
try:
|
91
|
+
order = gen.send(result) if order else next(gen)
|
92
|
+
logger.trace(f"{hook}发出Order:{order}, 开始匹配Broker")
|
93
|
+
|
94
|
+
assert isinstance(order, BaseOrder), f"只能yield Order, 实际为{type(order)}"
|
95
|
+
|
96
|
+
broker = self.match_broker(order.asset)
|
97
|
+
|
98
|
+
if not broker:
|
99
|
+
logger.warning(f"{order}没有对应broker, 忽略该order")
|
100
|
+
result = None
|
101
|
+
continue
|
102
|
+
|
103
|
+
logger.trace(f"{broker}开始处理:{order}")
|
104
|
+
result = broker.execute_order(order)
|
105
|
+
logger.trace(f"{broker}处理完成:{result}")
|
106
|
+
self.handle_result(result)
|
107
|
+
except StopIteration:
|
108
|
+
logger.trace(f"运行结束{hook}")
|
109
|
+
break
|
110
|
+
|
111
|
+
def handle_result(self, result: Result):
|
112
|
+
self.portfolio += result.brought
|
113
|
+
self.portfolio -= result.sold
|
114
|
+
self.collector.collect(result, self.portfolio)
|
115
|
+
logger.trace(f"资产增加{result.brought}, 减少{result.sold}")
|
116
|
+
|
117
|
+
def liquidate(self, asset: "BaseAsset", day: date, stage: "BaseStage") -> int:
|
118
|
+
if isinstance(asset, RMB):
|
119
|
+
return asset.amount
|
120
|
+
|
121
|
+
if (broker := self.match_broker(asset)) is None:
|
122
|
+
return -1
|
123
|
+
|
124
|
+
return broker.liquidate_asset(asset, day, stage)
|
125
|
+
|
126
|
+
def report(self, title: str, description: str, benchmark: str) -> ReportData:
|
127
|
+
periods_data = []
|
128
|
+
|
129
|
+
for shard in tqdm(self.collector.shards, "生成报告"):
|
130
|
+
portfolios_data: list[AssetData] = []
|
131
|
+
total_liquidating_value = 0
|
132
|
+
for asset in shard.portfolio:
|
133
|
+
liquidating_value = self.liquidate(asset, shard.day, shard.stage)
|
134
|
+
total_liquidating_value += liquidating_value
|
135
|
+
portfolios_data.append(AssetData.from_asset(asset, liquidating_value=liquidating_value))
|
136
|
+
|
137
|
+
datetime_str = datetime.combine(shard.day, shard.stage.time).isoformat()
|
138
|
+
periods_data.append(PeriodData(
|
139
|
+
datetime=datetime_str,
|
140
|
+
liquidating_value=total_liquidating_value,
|
141
|
+
logs=logger.records.get(datetime_str, []),
|
142
|
+
portfolios=portfolios_data,
|
143
|
+
transactions=[OrderResultData.from_result(result) for result in shard.results]
|
144
|
+
))
|
145
|
+
|
146
|
+
benchmark_points = [
|
147
|
+
PointData(
|
148
|
+
datetime=index.to_pydatetime().isoformat(),
|
149
|
+
value=row[Field.IN_收盘点位]
|
150
|
+
) for index, row in self.api.query(
|
151
|
+
start_date=self.start_date,
|
152
|
+
end_date=self.end_date,
|
153
|
+
fields=[Field.IN_收盘点位],
|
154
|
+
index=benchmark,
|
155
|
+
).iterrows()
|
156
|
+
]
|
157
|
+
|
158
|
+
return ReportData(
|
159
|
+
title=title,
|
160
|
+
description=description,
|
161
|
+
start_date=self.start_date.isoformat(),
|
162
|
+
end_date=self.end_date.isoformat(),
|
163
|
+
init_value=periods_data[0].liquidating_value,
|
164
|
+
periods=periods_data,
|
165
|
+
benchmark=BenchmarkData(
|
166
|
+
name=benchmark,
|
167
|
+
init_value=benchmark_points[0].value,
|
168
|
+
points=benchmark_points
|
169
|
+
)
|
170
|
+
)
|
171
|
+
|
172
|
+
|
173
|
+
__all__ = ["BaseTrader"]
|
@@ -0,0 +1,47 @@
|
|
1
|
+
from datetime import date
|
2
|
+
|
3
|
+
from .portfolio import Portfolio
|
4
|
+
from .base_order import Result
|
5
|
+
from .base_stage import BaseStage
|
6
|
+
|
7
|
+
|
8
|
+
class Shard:
|
9
|
+
def __init__(
|
10
|
+
self,
|
11
|
+
day: date,
|
12
|
+
stage: BaseStage,
|
13
|
+
portfolio: Portfolio,
|
14
|
+
):
|
15
|
+
self.day = day
|
16
|
+
self.stage = stage
|
17
|
+
self.portfolio = portfolio
|
18
|
+
self.results: list[Result] = []
|
19
|
+
|
20
|
+
def collect(self, result: Result):
|
21
|
+
self.results.append(result)
|
22
|
+
|
23
|
+
def __repr__(self):
|
24
|
+
return (
|
25
|
+
f"================{self.day} {self.stage.time}================\n"
|
26
|
+
f"Portfolio: {self.portfolio}\n"
|
27
|
+
f"Results: {self.results}\n"
|
28
|
+
)
|
29
|
+
|
30
|
+
__str__ = __repr__
|
31
|
+
|
32
|
+
|
33
|
+
class Collector:
|
34
|
+
def __init__(self):
|
35
|
+
self.shards: list[Shard] = []
|
36
|
+
|
37
|
+
def commence(
|
38
|
+
self,
|
39
|
+
day: date,
|
40
|
+
stage: BaseStage,
|
41
|
+
portfolio: Portfolio,
|
42
|
+
):
|
43
|
+
self.shards.append(Shard(day, stage, portfolio.copy))
|
44
|
+
|
45
|
+
def collect(self, result: Result, portfolio: Portfolio):
|
46
|
+
self.shards[-1].collect(result)
|
47
|
+
self.shards[-1].portfolio = portfolio.copy
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from typing import TYPE_CHECKING, Literal
|
2
|
+
|
3
|
+
from .base_order import BaseOrder
|
4
|
+
|
5
|
+
if TYPE_CHECKING:
|
6
|
+
from ..asset import Stock
|
7
|
+
|
8
|
+
|
9
|
+
class StockOrder(BaseOrder["Stock"]):
|
10
|
+
def __init__(self, asset: "Stock", category: Literal["buy"] | Literal["sell"]):
|
11
|
+
super().__init__(asset, category)
|
12
|
+
|
13
|
+
@property
|
14
|
+
def extra(self):
|
15
|
+
return {}
|
16
|
+
|
17
|
+
def __repr__(self):
|
18
|
+
return f"<股票交易指令 {self.category} {self.asset}>"
|
19
|
+
|
20
|
+
__str__ = __repr__
|
21
|
+
|
22
|
+
|
23
|
+
__all__ = ["StockOrder"]
|
@@ -0,0 +1,72 @@
|
|
1
|
+
from typing import TypeVar
|
2
|
+
|
3
|
+
from ..asset.base_asset import BaseAsset
|
4
|
+
|
5
|
+
AssetType = TypeVar('AssetType', bound=BaseAsset)
|
6
|
+
|
7
|
+
|
8
|
+
class Portfolio:
|
9
|
+
def __init__(self, *args: BaseAsset):
|
10
|
+
self.assets: list[BaseAsset] = list(args)
|
11
|
+
|
12
|
+
def add(self, asset: BaseAsset):
|
13
|
+
for index in range(len(self.assets)):
|
14
|
+
if self.assets[index] != asset:
|
15
|
+
continue
|
16
|
+
|
17
|
+
self.assets[index] += asset
|
18
|
+
break
|
19
|
+
else:
|
20
|
+
self.assets.append(asset)
|
21
|
+
|
22
|
+
def sub(self, asset: BaseAsset):
|
23
|
+
for index in range(len(self.assets)):
|
24
|
+
if self.assets[index] != asset:
|
25
|
+
continue
|
26
|
+
|
27
|
+
self.assets[index] -= asset
|
28
|
+
break
|
29
|
+
else:
|
30
|
+
raise IndexError(f"{self}没有资产{asset}")
|
31
|
+
|
32
|
+
self.assets = [i for i in self.assets if not (i.is_empty and i.is_closeable)]
|
33
|
+
|
34
|
+
def __add__(self, other: BaseAsset | list[BaseAsset]):
|
35
|
+
if isinstance(other, BaseAsset):
|
36
|
+
self.add(other)
|
37
|
+
|
38
|
+
if isinstance(other, list):
|
39
|
+
for asset in other:
|
40
|
+
assert isinstance(asset, BaseAsset), f"只允许添加资产, 实际为{type(asset)}"
|
41
|
+
self.add(asset)
|
42
|
+
|
43
|
+
return self
|
44
|
+
|
45
|
+
def __sub__(self, other: BaseAsset | list[BaseAsset]):
|
46
|
+
if isinstance(other, BaseAsset):
|
47
|
+
self.sub(other)
|
48
|
+
|
49
|
+
if isinstance(other, list):
|
50
|
+
for asset in other:
|
51
|
+
assert isinstance(asset, BaseAsset), f"只允许添加资产, 实际为{type(asset)}"
|
52
|
+
self.sub(asset)
|
53
|
+
|
54
|
+
return self
|
55
|
+
|
56
|
+
def __getitem__(self, item: type[AssetType]) -> list[AssetType]:
|
57
|
+
return [i for i in self.copy.assets if isinstance(i, item)]
|
58
|
+
|
59
|
+
def __iter__(self):
|
60
|
+
return iter(self.copy.assets)
|
61
|
+
|
62
|
+
@property
|
63
|
+
def copy(self) -> "Portfolio":
|
64
|
+
return Portfolio(*[asset.copy for asset in self.assets])
|
65
|
+
|
66
|
+
def __repr__(self):
|
67
|
+
return f"资产: {self.assets}"
|
68
|
+
|
69
|
+
__str__ = __repr__
|
70
|
+
|
71
|
+
|
72
|
+
__all__ = ["Portfolio"]
|
@@ -0,0 +1,29 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
2
|
+
from datetime import date, datetime
|
3
|
+
|
4
|
+
from ..env import EnvGetter
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from ..data import BaseAPI
|
8
|
+
|
9
|
+
|
10
|
+
class Query(EnvGetter):
|
11
|
+
def __init__(self, api: "BaseAPI", start_date: date, end_date: date):
|
12
|
+
super().__init__()
|
13
|
+
|
14
|
+
self.api = api
|
15
|
+
self.start_date = start_date
|
16
|
+
self.end_date = end_date
|
17
|
+
|
18
|
+
def __call__(self, **kwargs):
|
19
|
+
assert self.env is not None, "无法获取env"
|
20
|
+
|
21
|
+
params = {
|
22
|
+
"start_date": self.start_date,
|
23
|
+
"end_date": self.end_date,
|
24
|
+
}
|
25
|
+
params.update(kwargs)
|
26
|
+
return self.api.query(**params).on(datetime.combine(self.env.date, self.env.time))
|
27
|
+
|
28
|
+
|
29
|
+
__all__ = ['Query']
|
@@ -0,0 +1,76 @@
|
|
1
|
+
from typing import TYPE_CHECKING
|
2
|
+
|
3
|
+
from pydantic import BaseModel
|
4
|
+
|
5
|
+
from ..logger import Record
|
6
|
+
|
7
|
+
if TYPE_CHECKING:
|
8
|
+
from .base_order import Result
|
9
|
+
from ..asset import BaseAsset
|
10
|
+
|
11
|
+
|
12
|
+
class AssetData(BaseModel):
|
13
|
+
type: str
|
14
|
+
name: str
|
15
|
+
amount: float
|
16
|
+
extra: dict
|
17
|
+
|
18
|
+
@classmethod
|
19
|
+
def from_asset(cls, asset: "BaseAsset", **kwargs) -> "AssetData":
|
20
|
+
return cls(
|
21
|
+
type=asset.type(),
|
22
|
+
name=asset.name(),
|
23
|
+
amount=asset.amount,
|
24
|
+
extra={**asset.extra, **kwargs}
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
class OrderResultData(BaseModel):
|
29
|
+
order_type: str
|
30
|
+
order_category: str
|
31
|
+
order_asset: AssetData
|
32
|
+
order_extra: dict
|
33
|
+
result_brought: list[AssetData]
|
34
|
+
result_sold: list[AssetData]
|
35
|
+
|
36
|
+
@classmethod
|
37
|
+
def from_result(cls, result: "Result", **kwargs) -> "OrderResultData":
|
38
|
+
return cls(
|
39
|
+
order_type=result.order.type(),
|
40
|
+
order_asset=AssetData.from_asset(result.order.asset),
|
41
|
+
order_category=result.order.category,
|
42
|
+
order_extra={**result.order.extra, **kwargs},
|
43
|
+
result_brought=[AssetData.from_asset(i) for i in result.brought],
|
44
|
+
result_sold=[AssetData.from_asset(i) for i in result.sold],
|
45
|
+
)
|
46
|
+
|
47
|
+
|
48
|
+
class PeriodData(BaseModel):
|
49
|
+
datetime: str
|
50
|
+
liquidating_value: float
|
51
|
+
|
52
|
+
logs: list[Record]
|
53
|
+
portfolios: list[AssetData]
|
54
|
+
transactions: list[OrderResultData]
|
55
|
+
|
56
|
+
|
57
|
+
class PointData(BaseModel):
|
58
|
+
datetime: str
|
59
|
+
value: float
|
60
|
+
|
61
|
+
|
62
|
+
class BenchmarkData(BaseModel):
|
63
|
+
name: str
|
64
|
+
init_value: float
|
65
|
+
points: list[PointData]
|
66
|
+
|
67
|
+
|
68
|
+
class ReportData(BaseModel):
|
69
|
+
title: str
|
70
|
+
description: str
|
71
|
+
start_date: str
|
72
|
+
end_date: str
|
73
|
+
init_value: float
|
74
|
+
|
75
|
+
benchmark: BenchmarkData
|
76
|
+
periods: list[PeriodData]
|
@@ -0,0 +1,181 @@
|
|
1
|
+
import types
|
2
|
+
import inspect
|
3
|
+
import functools
|
4
|
+
from typing import Callable, Any
|
5
|
+
|
6
|
+
from pydantic import ValidationError, create_model, ConfigDict
|
7
|
+
from pydantic_core import PydanticUndefined
|
8
|
+
|
9
|
+
|
10
|
+
def to_str(x: Callable[..., Any]) -> str:
|
11
|
+
"""Convert a callable object to a descriptive string representation.
|
12
|
+
|
13
|
+
Args:
|
14
|
+
x: Any callable object (function, lambda, partial, class, method, etc.)
|
15
|
+
|
16
|
+
Returns:
|
17
|
+
A string representation of the callable.
|
18
|
+
"""
|
19
|
+
# 1. Handle functools.partial objects
|
20
|
+
if isinstance(x, functools.partial):
|
21
|
+
func_str = to_str(x.func)
|
22
|
+
args = [repr(a) for a in x.args]
|
23
|
+
keywords = [f"{k}={repr(v)}" for k, v in x.keywords.items()]
|
24
|
+
all_args = ", ".join(args + keywords)
|
25
|
+
return f"functools.partial({func_str}, {all_args})"
|
26
|
+
|
27
|
+
# 2. Handle classes (since classes are callable)
|
28
|
+
if inspect.isclass(x):
|
29
|
+
return f"{x.__module__}.{x.__qualname__}"
|
30
|
+
|
31
|
+
# 3. Handle bound methods (instance methods and class methods)
|
32
|
+
if isinstance(x, (types.MethodType, types.BuiltinMethodType)):
|
33
|
+
method_name = x.__name__
|
34
|
+
owner = x.__self__
|
35
|
+
|
36
|
+
if inspect.isclass(owner):
|
37
|
+
owner_str = owner.__qualname__
|
38
|
+
return f"<bound method {owner_str}.{method_name}>"
|
39
|
+
|
40
|
+
class_name = owner.__class__.__qualname__
|
41
|
+
return f"<bound method {class_name}.{method_name} of {repr(owner)}>"
|
42
|
+
|
43
|
+
if isinstance(x, (types.FunctionType, types.BuiltinFunctionType)):
|
44
|
+
module = x.__module__
|
45
|
+
qualname = x.__qualname__
|
46
|
+
|
47
|
+
if qualname == '<lambda>':
|
48
|
+
try:
|
49
|
+
source = inspect.getsource(x).strip()
|
50
|
+
if '\n' in source:
|
51
|
+
return f"<lambda at {module}:{x.__code__.co_firstlineno}>"
|
52
|
+
return source
|
53
|
+
except (OSError, TypeError):
|
54
|
+
return "<lambda>"
|
55
|
+
|
56
|
+
return f"{module}.{qualname}"
|
57
|
+
|
58
|
+
# Fallback: Use standard string representation
|
59
|
+
return str(x)
|
60
|
+
|
61
|
+
|
62
|
+
def get_function_location(func) -> str:
|
63
|
+
"""获取函数的定义位置(文件路径和行号)"""
|
64
|
+
try:
|
65
|
+
# 尝试获取函数的源文件和行号
|
66
|
+
source_file = inspect.getsourcefile(func)
|
67
|
+
lines, start_line = inspect.getsourcelines(func)
|
68
|
+
end_line = start_line + len(lines) - 1
|
69
|
+
|
70
|
+
# 如果是lambda函数,使用特殊格式
|
71
|
+
if func.__name__ == '<lambda>':
|
72
|
+
return f"lambda at {source_file}:{start_line}"
|
73
|
+
|
74
|
+
return f"{source_file}:{start_line}-{end_line}"
|
75
|
+
except Exception:
|
76
|
+
# 如果无法获取位置信息,返回默认值
|
77
|
+
return "<unknown location>"
|
78
|
+
|
79
|
+
|
80
|
+
def format_arg_type(value) -> str:
|
81
|
+
"""格式化参数类型信息,特别处理类对象"""
|
82
|
+
if inspect.isclass(value):
|
83
|
+
# 对于类对象,显示类名而不是元类
|
84
|
+
return f"<class '{value.__module__}.{value.__qualname__}'>"
|
85
|
+
return str(type(value))
|
86
|
+
|
87
|
+
|
88
|
+
def format_annotations(params) -> str:
|
89
|
+
"""格式化参数注解信息"""
|
90
|
+
lines = []
|
91
|
+
for name, param in params.items():
|
92
|
+
# 跳过可变关键字参数
|
93
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
94
|
+
continue
|
95
|
+
|
96
|
+
# 获取类型注解
|
97
|
+
ann = param.annotation
|
98
|
+
ann_str = ann.__name__ if ann is not param.empty else "Any"
|
99
|
+
|
100
|
+
# 添加默认值信息
|
101
|
+
default = ""
|
102
|
+
if param.default is not param.empty:
|
103
|
+
default = f" (默认值: {repr(param.default)})"
|
104
|
+
|
105
|
+
# 添加参数类型标记
|
106
|
+
kind = ""
|
107
|
+
if param.kind == inspect.Parameter.KEYWORD_ONLY:
|
108
|
+
kind = " [keyword-only]"
|
109
|
+
elif param.kind == inspect.Parameter.POSITIONAL_ONLY:
|
110
|
+
kind = " [positional-only]"
|
111
|
+
|
112
|
+
lines.append(f" - {name}: {ann_str}{default}{kind}")
|
113
|
+
return "\n".join(lines)
|
114
|
+
|
115
|
+
|
116
|
+
def inject(func, **kwargs) -> Any:
|
117
|
+
sig = inspect.signature(func)
|
118
|
+
params = sig.parameters
|
119
|
+
|
120
|
+
model_fields = {}
|
121
|
+
for name, param in params.items():
|
122
|
+
if param.kind == inspect.Parameter.VAR_KEYWORD:
|
123
|
+
continue
|
124
|
+
|
125
|
+
annotation = param.annotation
|
126
|
+
if annotation is param.empty:
|
127
|
+
annotation = Any
|
128
|
+
|
129
|
+
default = PydanticUndefined
|
130
|
+
if param.default is not param.empty:
|
131
|
+
default = param.default
|
132
|
+
|
133
|
+
model_fields[name] = (annotation, default)
|
134
|
+
|
135
|
+
func_name = to_str(func)
|
136
|
+
|
137
|
+
model = create_model(
|
138
|
+
f"{func_name}_Params",
|
139
|
+
__config__=ConfigDict(
|
140
|
+
extra="allow",
|
141
|
+
arbitrary_types_allowed=True,
|
142
|
+
coerce_numbers_to_str=True
|
143
|
+
),
|
144
|
+
**model_fields
|
145
|
+
)
|
146
|
+
|
147
|
+
try:
|
148
|
+
validated = model(**kwargs)
|
149
|
+
except ValidationError as e:
|
150
|
+
errors = []
|
151
|
+
for error in e.errors():
|
152
|
+
loc = ".".join(map(str, error["loc"]))
|
153
|
+
expected_type = " | ".join(error.get("ctx", {}).get("expected", ["未知类型"]))
|
154
|
+
actual_type = type(kwargs.get(loc, "<missing>")).__name__
|
155
|
+
msg = f"无法将类型 {actual_type} 转换为函数定义的类型 {expected_type}"
|
156
|
+
input_value = error.get("input", "<missing>")
|
157
|
+
|
158
|
+
errors.append(f" - {loc}: {msg} (输入值: {input_value})")
|
159
|
+
|
160
|
+
arg_types = "\n".join([
|
161
|
+
f" - {k}: {format_arg_type(v)}"
|
162
|
+
for k, v in kwargs.items()
|
163
|
+
])
|
164
|
+
|
165
|
+
raise TypeError(
|
166
|
+
f"🚫 参数类型与可用参数不兼容,且强制转换失败,请检查 {func_name} 的定义:\n"
|
167
|
+
f"📋 定义的参数:\n{format_annotations(params)}\n"
|
168
|
+
f"📤 可用的全部参数(正确标准):\n{arg_types}\n"
|
169
|
+
f"❌ 参数类型不匹配且转换失败:\n{"\n".join(errors)}\n"
|
170
|
+
f"📍 函数定义位置: {get_function_location(func)}\n"
|
171
|
+
)
|
172
|
+
|
173
|
+
validated_data = validated.model_dump()
|
174
|
+
|
175
|
+
if any((name for name, param in params.items() if param.kind == inspect.Parameter.VAR_KEYWORD)):
|
176
|
+
return func(**validated_data)
|
177
|
+
|
178
|
+
return func(**{p.name: validated_data[p.name] for p in params.values()})
|
179
|
+
|
180
|
+
|
181
|
+
__all__ = ["inject"]
|
@@ -0,0 +1 @@
|
|
1
|
+
from .cn_stock import StockStageCN
|
@@ -0,0 +1,23 @@
|
|
1
|
+
from enum import auto
|
2
|
+
from datetime import time
|
3
|
+
|
4
|
+
from quantification import BaseStage
|
5
|
+
|
6
|
+
|
7
|
+
class StockStageCN(BaseStage):
|
8
|
+
盘前 = auto()
|
9
|
+
开盘 = auto()
|
10
|
+
收盘 = auto()
|
11
|
+
盘后 = auto()
|
12
|
+
|
13
|
+
@property
|
14
|
+
def time(self) -> time:
|
15
|
+
return {
|
16
|
+
StockStageCN.盘前: time(0, 0),
|
17
|
+
StockStageCN.开盘: time(9, 30),
|
18
|
+
StockStageCN.收盘: time(15, 0),
|
19
|
+
StockStageCN.盘后: time(23, 59),
|
20
|
+
}[self]
|
21
|
+
|
22
|
+
|
23
|
+
__all__ = ['StockStageCN']
|
@@ -0,0 +1 @@
|
|
1
|
+
from .simple import *
|
@@ -0,0 +1 @@
|
|
1
|
+
from .strategy import SimpleStrategy
|
@@ -0,0 +1 @@
|
|
1
|
+
from .trader import AFactorTrader
|
@@ -0,0 +1,27 @@
|
|
1
|
+
from quantification import (
|
2
|
+
RMB,
|
3
|
+
Stock,
|
4
|
+
Portfolio,
|
5
|
+
BaseTrader,
|
6
|
+
StockBrokerCN
|
7
|
+
)
|
8
|
+
|
9
|
+
|
10
|
+
class AFactorTrader(BaseTrader):
|
11
|
+
def __init__(
|
12
|
+
self,
|
13
|
+
api,
|
14
|
+
start_date,
|
15
|
+
end_date,
|
16
|
+
stage,
|
17
|
+
strategy,
|
18
|
+
stocks: list[type[Stock]],
|
19
|
+
init_cash: float = 100_000,
|
20
|
+
padding: int = 0
|
21
|
+
):
|
22
|
+
super().__init__(api, Portfolio(RMB(init_cash)), start_date, end_date, padding, stage, strategy,
|
23
|
+
[StockBrokerCN])
|
24
|
+
self.stocks = stocks
|
25
|
+
|
26
|
+
|
27
|
+
__all__ = ["AFactorTrader"]
|
@@ -0,0 +1 @@
|
|
1
|
+
from .trader import SimpleTrader
|