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.
- py_niffler-0.0.1/PKG-INFO +17 -0
- py_niffler-0.0.1/README.md +3 -0
- py_niffler-0.0.1/pyproject.toml +47 -0
- py_niffler-0.0.1/src/niffler/__about__.py +1 -0
- py_niffler-0.0.1/src/niffler/__cli__.py +177 -0
- py_niffler-0.0.1/src/niffler/__default__.py +32 -0
- py_niffler-0.0.1/src/niffler/__error__.py +13 -0
- py_niffler-0.0.1/src/niffler/__init__.py +1 -0
- py_niffler-0.0.1/src/niffler/agent.py +1170 -0
- py_niffler-0.0.1/src/niffler/db.py +740 -0
- py_niffler-0.0.1/src/niffler/model.py +7312 -0
- py_niffler-0.0.1/src/niffler/pipeline/__init__.py +37 -0
- py_niffler-0.0.1/src/niffler/pipeline/submit.py +146 -0
- py_niffler-0.0.1/src/niffler/py.typed +0 -0
- py_niffler-0.0.1/src/niffler/strategy/__init__.py +201 -0
- py_niffler-0.0.1/src/niffler/strategy/naive.py +75 -0
- py_niffler-0.0.1/src/niffler/utils.py +210 -0
|
@@ -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,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__
|