py-niffler 0.0.1__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.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.3
2
+ Name: py-niffler
3
+ Version: 0.0.1
4
+ Summary: Add your description here
5
+ Author: AshSwing
6
+ Author-email: AshSwing <ashswing@email.cn>
7
+ Requires-Dist: click>=8.2.1
8
+ Requires-Dist: httpx>=0.28.1
9
+ Requires-Dist: kaitian>=0.0.6
10
+ Requires-Dist: psycopg2-binary>=2.9.10
11
+ Requires-Dist: sqlmodel>=0.0.24
12
+ Requires-Python: >=3.13
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Niffler
16
+
17
+ > Long-snouted, burrowing creatures native to Britain with a penchant for anything shiny.
@@ -0,0 +1,3 @@
1
+ # Niffler
2
+
3
+ > Long-snouted, burrowing creatures native to Britain with a penchant for anything shiny.
@@ -0,0 +1,47 @@
1
+ [project]
2
+ name = "py-niffler"
3
+ version = "0.0.1"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [{ name = "AshSwing", email = "ashswing@email.cn" }]
7
+ requires-python = ">=3.13"
8
+ dependencies = [
9
+ "click>=8.2.1",
10
+ "httpx>=0.28.1",
11
+ "kaitian>=0.0.6",
12
+ "psycopg2-binary>=2.9.10",
13
+ "sqlmodel>=0.0.24",
14
+ ]
15
+
16
+ [project.scripts]
17
+ niffler = "niffler.__cli__:niffler_cli"
18
+
19
+ [build-system]
20
+ requires = ["uv_build>=0.8.0,<0.9"]
21
+ build-backend = "uv_build"
22
+
23
+ [tool.uv.build-backend]
24
+ module-name = "niffler"
25
+ module-root = "src"
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "ipykernel>=6.30.0",
30
+ "kaitian[app,pretty]>=0.0.6",
31
+ "pandas>=2.3.1",
32
+ "psutil>=7.0.0",
33
+ "pytest>=8.4.1",
34
+ ]
35
+
36
+ [tool.pytest.ini_options]
37
+ python_files = "*.py"
38
+ testpaths = ["src", "test"]
39
+ addopts = "--doctest-modules"
40
+ doctest_optionflags = [
41
+ "NORMALIZE_WHITESPACE",
42
+ "IGNORE_EXCEPTION_DETAIL",
43
+ "ELLIPSIS",
44
+ ]
45
+
46
+ [tool.ruff.lint.per-file-ignores]
47
+ "__init__.py" = ["F401"]
@@ -0,0 +1 @@
1
+ __version__ = "0.0.1"
@@ -0,0 +1,177 @@
1
+ import asyncio
2
+ import importlib.util
3
+ import inspect
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Any, Dict, get_type_hints
7
+
8
+ import click
9
+ from kaitian.utils.logger import add_file_logger, setup_logger
10
+
11
+ from .agent import NifflerAgent
12
+ from .strategy import Strategy
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ def _str_to_bool(value: str) -> bool:
18
+ """
19
+ 将字符串 'true'/'false'/'yes'/'no'/'1'/'0' 等转为布尔值。
20
+ 大小写不敏感,失败时抛出 ValueError。
21
+ """
22
+ v = value.lower()
23
+ if v in {"true", "1", "yes", "y", "on"}:
24
+ return True
25
+ if v in {"false", "0", "no", "n", "off"}:
26
+ return False
27
+ raise ValueError(f"Invalid boolean value: {value!r}")
28
+
29
+
30
+ def _load_strategy_class(path: Path) -> type[Strategy[Any]]:
31
+ """动态加载文件并返回第一个 Strategy 子类。"""
32
+ spec = importlib.util.spec_from_file_location(path.stem, path)
33
+ if spec is None or spec.loader is None:
34
+ raise click.ClickException(f"无法加载模块 {path}")
35
+ mod = importlib.util.module_from_spec(spec)
36
+ spec.loader.exec_module(mod)
37
+
38
+ for _, obj in inspect.getmembers(mod, inspect.isclass):
39
+ if (
40
+ issubclass(obj, Strategy)
41
+ and obj is not Strategy
42
+ and not inspect.isabstract(obj)
43
+ ):
44
+ return obj
45
+ raise click.ClickException(f"找不到具体 Strategy 子类: {path}")
46
+
47
+
48
+ def _convert_kwargs(cls: type[Strategy[Any]], raw: Dict[str, str]) -> Dict[str, Any]:
49
+ """根据 Strategy.__init__ 的类型注解自动转换字符串值。"""
50
+ hints = get_type_hints(cls.__init__)
51
+ converted: Dict[str, Any] = {}
52
+ for k, v in raw.items():
53
+ typ = hints.get(k)
54
+ if typ is None:
55
+ # 没有注解保持字符串
56
+ converted[k] = v
57
+ continue
58
+
59
+ # 支持 bool、int、float,其余保持 str
60
+ if typ is bool:
61
+ converted[k] = _str_to_bool(v)
62
+ elif typ is int:
63
+ converted[k] = int(v)
64
+ elif typ is float:
65
+ converted[k] = float(v)
66
+ else:
67
+ converted[k] = v
68
+ return converted
69
+
70
+
71
+ @click.group()
72
+ def niffler_cli() -> None:
73
+ """Niffler: WorldQuant Brain Platform Assistant."""
74
+ pass
75
+
76
+
77
+ @niffler_cli.command(context_settings=dict(ignore_unknown_options=True))
78
+ @click.argument("strategy", type=click.Path(exists=True, path_type=Path))
79
+ @click.option("--region", required=True, help="Region")
80
+ @click.option("--universe", required=True, help="Universe")
81
+ @click.option("--delay", type=int, required=True, help="Delay")
82
+ @click.option("--slots", type=int, required=True, help="占用槽位")
83
+ @click.option("--batch_size", type=int, required=True, help="批大小")
84
+ @click.option("--name", type=str, default=None, help="任务名")
85
+ @click.option("--db", type=str, default=None, help="因子库")
86
+ @click.option("--logfile", type=click.Path(path_type=Path), default=None)
87
+ @click.option("--loglevel", type=int, default=10, help="日志级别")
88
+ @click.option("--username", type=str, default=None, help="用户名")
89
+ @click.option("--password", type=str, default=None, help="密码")
90
+ @click.option("--retry_delay", type=int, default=10, help="重试休眠间隔")
91
+ @click.option("--max_retry", type=int, default=10, help="最大重试次数")
92
+ @click.option("--concurrency", type=int, default=10, help="并发数")
93
+ @click.argument("extra_args", nargs=-1, type=click.UNPROCESSED)
94
+ def simulate(
95
+ strategy: Path,
96
+ region: str,
97
+ universe: str,
98
+ delay: int,
99
+ slots: int,
100
+ batch_size: int,
101
+ db: str | None,
102
+ name: str | None,
103
+ logfile: Path | None,
104
+ loglevel: int,
105
+ username: str | None,
106
+ password: str | None,
107
+ retry_delay: float,
108
+ max_retry: int,
109
+ concurrency: int,
110
+ extra_args: tuple[str, ...],
111
+ ) -> None:
112
+ """
113
+ Niffler 因子回测器
114
+
115
+ STRATEGY: 包含 Strategy 子类的 .py 文件路径
116
+ 其余形如 --key value 的参数都会透传给 Strategy 构造函数
117
+ """
118
+ cls = _load_strategy_class(strategy)
119
+
120
+ # 先把固定参数和额外参数统一收集到 raw
121
+ raw: Dict[str, str] = {
122
+ "region": region,
123
+ "universe": universe,
124
+ "delay": str(delay),
125
+ "slots": str(slots),
126
+ "batch_size": str(batch_size),
127
+ }
128
+ if name is not None:
129
+ raw["name"] = name
130
+
131
+ # 解析 extra_args
132
+ it = iter(extra_args)
133
+ for arg in it:
134
+ if not arg.startswith("--"):
135
+ raise click.ClickException(f"额外参数必须以 -- 开头,得到 {arg}")
136
+ key = arg[2:].replace("-", "_")
137
+ try:
138
+ value = next(it)
139
+ except StopIteration:
140
+ raise click.ClickException(f"{arg} 缺少对应值")
141
+ raw[key] = value
142
+
143
+ # 统一类型转换
144
+ kwargs = _convert_kwargs(cls, raw)
145
+ if db is not None:
146
+ if Path(db).parent.exists():
147
+ db = f"sqlite:///{Path(db).absolute()}"
148
+
149
+ logger = setup_logger(
150
+ "niffler",
151
+ loglevel,
152
+ fmt="[{levelname:.04}] {asctime} {message} [{filename}:{lineno}]",
153
+ )
154
+ if logfile is not None:
155
+ _ = add_file_logger(
156
+ logger,
157
+ logfile,
158
+ loglevel,
159
+ formatter=logging.Formatter(
160
+ fmt="[{levelname:.04}] {asctime} {message} [{filename}:{lineno}]",
161
+ datefmt="%m-%d %H:%M:%S",
162
+ style="{",
163
+ ),
164
+ )
165
+
166
+ agent = NifflerAgent(
167
+ dbengine=db,
168
+ username=username,
169
+ password=password,
170
+ retry_delay=retry_delay,
171
+ max_retry=max_retry,
172
+ request_concurrency=concurrency,
173
+ )
174
+
175
+ # logger.debug(f"增补参数: {kwargs}")
176
+ strategy_obj = cls(agent=agent, **kwargs)
177
+ asyncio.run(strategy_obj.run())
@@ -0,0 +1,32 @@
1
+ REGIONS: dict[str, list[str]] = {
2
+ "USA": ["TOP3000", "TOP1000", "TOP500", "TOP200", "ILLIQUID_MINVOL1M", "TOPSP500"],
3
+ "GLB": ["TOP3000", "MINVOL1M", "TOPDIV3000"],
4
+ "EUR": ["TOP2500", "TOP1200", "TOP800", "TOP400", "ILLIQUID_MINVOL1M"],
5
+ "ASI": ["MINVOL1M", "ILLIQUID_MINVOL1M"],
6
+ "CHN": ["TOP2000U"],
7
+ }
8
+ ALPHA_CATEGORIES: list[str] = [
9
+ "PRICE_REVERSION",
10
+ "PRICE_MOMENTUM",
11
+ "VOLUME",
12
+ "FUNDAMENTAL",
13
+ "ANALYST",
14
+ "PRICE_VOLUME",
15
+ "RELATION",
16
+ "SENTIMENT",
17
+ ]
18
+ ALPHA_STATUS: list[str] = ["ACTIVE", "UNSUBMITTED", "DECOMMISSIONED"]
19
+ ALPHA_LANGUAGE: list[str] = ["FASTEXPR", "EXPR", "PYTHON"]
20
+ ALPHA_INSTRUMENT: list[str] = ["CRYPTO", "EQUITY"]
21
+
22
+ START_DATE = "2013-01-20"
23
+ END_DATE = "2023-01-20"
24
+
25
+ POWER_POOL_ALPHA_DESCRIPTION_PLACEHOLDER = (
26
+ "Idea: This alpha aims to identify stocks that have recently experienced "
27
+ "significant [factor] relative to [reference point]. "
28
+ "By focusing on standardized, outlier-adjusted [factor] over a short-term horizon, "
29
+ "the alpha seeks to capture [market behavior], which may indicate [fundamental/expectation changes].\n"
30
+ "Rationale for data used: -\n"
31
+ "Rationale for operators used: -"
32
+ )
@@ -0,0 +1,13 @@
1
+ class SimulationInProgress(Exception):
2
+ def __init__(
3
+ self,
4
+ message: str | None = None,
5
+ simid: str | None = None,
6
+ progress: float | None = None,
7
+ ) -> None:
8
+ super().__init__(message)
9
+ self.simid = simid
10
+ self.progress = progress
11
+
12
+
13
+ class APIError(Exception): ...
@@ -0,0 +1 @@
1
+ from .__about__ import __version__