tokoyo-sdk 0.10.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.
Files changed (53) hide show
  1. tokoyo_sdk-0.10.0/MANIFEST.in +4 -0
  2. tokoyo_sdk-0.10.0/PKG-INFO +114 -0
  3. tokoyo_sdk-0.10.0/PROTOCOL.md +50 -0
  4. tokoyo_sdk-0.10.0/README.md +90 -0
  5. tokoyo_sdk-0.10.0/example_allocation.py +35 -0
  6. tokoyo_sdk-0.10.0/example_backtest.py +44 -0
  7. tokoyo_sdk-0.10.0/example_classic_live.py +91 -0
  8. tokoyo_sdk-0.10.0/example_classic_strategies.py +274 -0
  9. tokoyo_sdk-0.10.0/example_first_tiantian.py +172 -0
  10. tokoyo_sdk-0.10.0/example_gym.py +71 -0
  11. tokoyo_sdk-0.10.0/example_live.py +173 -0
  12. tokoyo_sdk-0.10.0/example_manual.py +70 -0
  13. tokoyo_sdk-0.10.0/example_risk.py +57 -0
  14. tokoyo_sdk-0.10.0/publish.py +72 -0
  15. tokoyo_sdk-0.10.0/pyproject.toml +25 -0
  16. tokoyo_sdk-0.10.0/setup.cfg +4 -0
  17. tokoyo_sdk-0.10.0/tests/test_allocation.py +108 -0
  18. tokoyo_sdk-0.10.0/tests/test_backtest.py +179 -0
  19. tokoyo_sdk-0.10.0/tests/test_classic_examples.py +33 -0
  20. tokoyo_sdk-0.10.0/tests/test_gym_market.py +133 -0
  21. tokoyo_sdk-0.10.0/tests/test_macro.py +44 -0
  22. tokoyo_sdk-0.10.0/tests/test_manual.py +57 -0
  23. tokoyo_sdk-0.10.0/tests/test_market.py +56 -0
  24. tokoyo_sdk-0.10.0/tests/test_risk.py +113 -0
  25. tokoyo_sdk-0.10.0/tokoyo/__init__.py +22 -0
  26. tokoyo_sdk-0.10.0/tokoyo/_fund_ledger.py +385 -0
  27. tokoyo_sdk-0.10.0/tokoyo/backtest.py +381 -0
  28. tokoyo_sdk-0.10.0/tokoyo/gateways.py +417 -0
  29. tokoyo_sdk-0.10.0/tokoyo/gym.py +208 -0
  30. tokoyo_sdk-0.10.0/tokoyo/gym_market.py +233 -0
  31. tokoyo_sdk-0.10.0/tokoyo/login.py +44 -0
  32. tokoyo_sdk-0.10.0/tokoyo/macro.py +130 -0
  33. tokoyo_sdk-0.10.0/tokoyo/manual.py +171 -0
  34. tokoyo_sdk-0.10.0/tokoyo/manual_web.py +200 -0
  35. tokoyo_sdk-0.10.0/tokoyo/market.py +218 -0
  36. tokoyo_sdk-0.10.0/tokoyo/models.py +132 -0
  37. tokoyo_sdk-0.10.0/tokoyo/payment.py +125 -0
  38. tokoyo_sdk-0.10.0/tokoyo/risk/__init__.py +18 -0
  39. tokoyo_sdk-0.10.0/tokoyo/risk/_inputs.py +41 -0
  40. tokoyo_sdk-0.10.0/tokoyo/risk/allocation.py +127 -0
  41. tokoyo_sdk-0.10.0/tokoyo/risk/exposure.py +171 -0
  42. tokoyo_sdk-0.10.0/tokoyo/risk/factor.py +155 -0
  43. tokoyo_sdk-0.10.0/tokoyo/risk/lppls.py +153 -0
  44. tokoyo_sdk-0.10.0/tokoyo/risk/metrics.py +46 -0
  45. tokoyo_sdk-0.10.0/tokoyo/rl.py +61 -0
  46. tokoyo_sdk-0.10.0/tokoyo/session.py +202 -0
  47. tokoyo_sdk-0.10.0/tokoyo/strategy.py +152 -0
  48. tokoyo_sdk-0.10.0/tokoyo/trader.py +275 -0
  49. tokoyo_sdk-0.10.0/tokoyo_sdk.egg-info/PKG-INFO +114 -0
  50. tokoyo_sdk-0.10.0/tokoyo_sdk.egg-info/SOURCES.txt +51 -0
  51. tokoyo_sdk-0.10.0/tokoyo_sdk.egg-info/dependency_links.txt +1 -0
  52. tokoyo_sdk-0.10.0/tokoyo_sdk.egg-info/requires.txt +20 -0
  53. tokoyo_sdk-0.10.0/tokoyo_sdk.egg-info/top_level.txt +1 -0
@@ -0,0 +1,4 @@
1
+ include README.md PROTOCOL.md
2
+ include example_*.py
3
+ include publish.py
4
+ recursive-include tests *.py
@@ -0,0 +1,114 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokoyo-sdk
3
+ Version: 0.10.0
4
+ Summary: AI时代的场外基金量化投研SDK
5
+ Project-URL: Homepage, https://tokoyo-sdk.pages.dev/
6
+ Project-URL: Documentation, https://tokoyo-sdk.pages.dev/guide.html
7
+ Requires-Python: >=3.10
8
+ Description-Content-Type: text/markdown
9
+ Requires-Dist: pycryptodome>=3.20
10
+ Provides-Extra: risk
11
+ Requires-Dist: numpy>=1.24; extra == "risk"
12
+ Requires-Dist: scipy>=1.10; extra == "risk"
13
+ Provides-Extra: rl
14
+ Requires-Dist: numpy>=1.24; extra == "rl"
15
+ Requires-Dist: pandas>=2.0; extra == "rl"
16
+ Requires-Dist: gymnasium<2,>=1.0; extra == "rl"
17
+ Provides-Extra: market
18
+ Requires-Dist: akshare>=1.18; extra == "market"
19
+ Requires-Dist: pandas>=2.0; extra == "market"
20
+ Provides-Extra: research
21
+ Requires-Dist: matplotlib>=3.7; extra == "research"
22
+ Provides-Extra: dev
23
+ Requires-Dist: build>=1.2; extra == "dev"
24
+
25
+ # TOKOYO SDK(常世 SDK)0.10.0
26
+
27
+ AI时代的场外基金量化投研SDK
28
+
29
+ 这是独立的 Python SDK 项目。官网、文档、AI Skill 和 Great Prompt 广场由另一个 `web` 项目维护,不参与本项目安装或构建。
30
+
31
+ 在本目录从源码安装:
32
+
33
+ ```powershell
34
+ python -m pip install ".[market,risk]"
35
+ ```
36
+
37
+ 开发与打包:
38
+
39
+ ```powershell
40
+ python -m pip install -e ".[dev,market,risk,rl,research]"
41
+ python -m unittest discover -s tests -v
42
+ python -m build
43
+ ```
44
+
45
+ wheel 与源码发行包生成到本项目的 `dist/`,不包含官网或文档工程。以下已有 wheel 的安装示例也可以继续使用。
46
+
47
+ ## 发布到 PyPI
48
+
49
+ 构建并检查安装包后,运行 `python publish.py --dry-run` 检查待发布文件,再运行 `python publish.py`。后者在本地隐藏输入 PyPI API Token;令牌不写入项目文件或命令参数。也可由发布环境提供 `UV_PUBLISH_TOKEN`。脚本只上传当前版本的 wheel 和源码发行包,并用 PyPI 返回的 SHA-256 校验发布结果。
50
+
51
+ Python 3.10+。示例使用 Windows 控制台。SDK 的天天基金适配器使用真实生产主机和已授权的 Android 交易会话。当前版本依据客户端代码实现,尚待使用授权账户做端到端验收。
52
+
53
+ ## 安装与启动
54
+
55
+ 在 wheel 与 `example_live.py` 所在目录运行:
56
+
57
+ ```powershell
58
+ python -m pip install .\tokoyo_sdk-0.10.0-py3-none-any.whl
59
+ python .\example_live.py --account my-account --check
60
+ python .\example_live.py --account my-account --db .\live-orders.sqlite3
61
+ ```
62
+
63
+ `--check` 只验证会话。需要从本人已授权的 Android 登录取得 `CustomerNo`、`CToken`、`UToken`、`MobileKey` 和正确分区。也可以用 `--provider my_provider` 调用一次本地 `login() -> TianTianSession`。本包未实现手机验证码、新设备验证或自动提取官方 App 会话。银行卡参数使用平台已有的 `BankAccountNo` 标识。
64
+
65
+ 在自己的程序中调用 `from tokoyo import login`,然后用 `login(account)` 在本地终端导入已有的 Android 交易会话。也可以传入 `login(account, login_flow)`,由授权适配器返回 `TianTianSession`。两种方式都在返回交易连接前发起只读账户验证。
66
+
67
+ 启动后可在同一进程用 `assets` 验证会话,`refresh` 尝试续期,`orders` 查看本地账本,`query <本地订单ID>` 查询一笔订单。运行 `--help` 或 `<命令> --help` 查看交互命令参数。以下是参数格式,把尖括号替换为本人真实订单、持仓和目标信息:
68
+
69
+ ```text
70
+ subscribe <基金代码> <金额> --id <业务唯一ID>
71
+ redeem <基金代码> <份额> --share-id <持仓ID> --sub-account <子账户> --id <业务唯一ID>
72
+ redeem <基金代码> <份额> --share-id <持仓ID> --sub-account <子账户> --cashbag <活期宝基金代码> --id <业务唯一ID>
73
+ transfer_all <基金代码> <该持仓全部可用份额> --share-id <持仓ID> --sub-account <子账户> --cashbag <活期宝基金代码> --id <业务唯一ID>
74
+ withdraw <活期宝基金代码> <金额> --id <业务唯一ID>
75
+ cancel <本地订单ID> --business-id <交易记录ID> --business-type <业务类型> --display-code <显示业务代码> --refund-original
76
+ ```
77
+
78
+ `redeem-terms <基金代码>` 读取赎回规则。普通赎回默认到持仓对应的绑定银行卡,也可指定活期宝。划转支持将单个持仓的全部可用份额转入活期宝。取现支持活期宝普通取现到绑定银行卡。撤单需要匹配到本地流水,并由天天基金交易详情返回可撤,支持原路退回及平台明确提供时指定 `--refund-cashbag <代码>`。查看活期宝分类的交易详情时可加 `--cashbag-record`。组合和批量撤单尚未接入。
79
+
80
+ 每笔提交都有本地记录。用同一业务唯一 ID 重复调用会返回已有本地订单。同一个 ID 参数发生变化会报冲突。请求发出后若回执不明,查询官方交易记录核对,SDK 不会自动补发。`submitted` 表示已提交并拿到流水,`cancel_pending` 表示撤单待核对。银行要求二次支付时可通过 SDK 读取支付挑战,银行端的交互和挑战结果由官方渠道完成。
81
+
82
+ 六位交易密码在 Windows 进程内由 `CryptProtectMemory` 加密保存。构造时清除输入缓冲区,签发授权时短暂解密,随后清除 SDK 持有的明文缓冲区,关闭时清除密文。Python 和加密库内部短暂生成的副本无法保证全部清除。SDK 不把密码写入账本。
83
+
84
+ 个人开源版和机构企业版均可使用天天基金网 API 自动下单。配套的 `example_first_tiantian.py` 将 12 周动量规则用于新增资金申购:默认读取真实净值并输出信号,传入 `--submit --account <账户别名>` 后尝试提交实际交易。需提供已授权的 Android Session、六位支付密码和绑定支付账户标识。这个例子不自动赎回原有仓位,参数说明见本地文档的“天天基金网 API 自动下单”。密码保护的详细说明与架构图见“我们如何保护您的支付密码”。
85
+
86
+ 普通申购、赎回、划转、取现和撤单的已接入路径见 [PROTOCOL.md](PROTOCOL.md)。本地验证范围为包导入、命令解析及 Windows 加密与 RSA 往返,无资金操作联调结果。
87
+
88
+ ## 风险分析
89
+
90
+ 用 `python -m pip install ".\tokoyo_sdk-0.10.0-py3-none-any.whl[risk]"` 安装风险依赖。可从 `tokoyo.risk` 调用收益率回归、持仓穿透、Barra 风格截面因子模型、历史 VaR/ES 和 LPPLS。源码包内附 `example_risk.py`,用于对日期对齐的 CSV 运行收益率归因和风险诊断。
91
+
92
+ ## 行情与 Agent Skill
93
+
94
+ 安装 `[market]` 后,从 `tokoyo.market` 调用 `daily_nav`、`nav_bars`、`fund_profile` 和 `fund_holdings`。AI Skill 从配套网站的 Skill 页面下载,供 Agent 使用 TOKOYO 开发策略、看板和投研报告。
95
+
96
+ 从 `tokoyo.strategy` 导入 `Strategy` 和 `run_backtest`。同一目标权重回调可使用单基金历史和截面面板。回测支持日、周、月,按估值、确认、份额可用和到账事件分账。 `example_backtest.py` 可自动下载公开净值并导出账本。 `StrategyRunner` 可为同一回调切换执行模式,实盘需要自备已授权适配器。
97
+
98
+ ## 组合配置与自定义 RL
99
+
100
+ 组合配置接口 `tokoyo.risk.allocate_portfolio`,支持 MV、MVSK、ERC 及自定义风险预算。安装 `[risk]` 扩展即可使用。
101
+
102
+ `tokoyo.rl.RLAllocationPolicy(model, observation_fn, symbols)` 用于接入用户自己的模型,返回统一的目标权重。模型与训练框架由用户管理。可选安装 `[rl]`,使用 `tokoyo.gym.FundAllocationEnv`;环境复用 T+n、多资金账户和份额批次账本,遵循 Gymnasium 接口。
103
+
104
+ 运行 `example_allocation.py --start 2024-01-01 --end 2024-12-31 --method erc` 可自动获取真实历史净值并回测;使用自己的 RL 模块时改为 `--rl-provider my_policy`,模块提供 `model` 和 `observation_fn`。
105
+
106
+ ## Gym 行情抽样
107
+
108
+ 可通过 `MarketSettings`、`FundEpisodeSampler`、`MarketEpisode` 和 `RandomFundAllocationEnv` 指定基金、类型和日期范围,随机抽取真实历史窗口,按共同日期或相对步数组成截面。支持公开行情 CSV 缓存、固定种子与快照回放。相对步数模式会明确标记合成时钟,不保留真实跨基金同期关系。详见文档第 7 章,配套脚本为 `example_gym.py`。
109
+
110
+ ## 宏观数据与经典策略
111
+
112
+ 0.10.0 提供 `tokoyo.macro.macro_catalog`、`macro_series`、`macro_asof` 以及 `cpi`、`ppi`、`pmi`、`lpr` 简写,覆盖九个宏观序列。统计期与可用日期分开,缺少真实公布日时明确标记可用时间假设;返回的是当前历史序列,不是逐次发布的历史版本库。
113
+
114
+ 文档第六章逐步实现永久投资组合、MPT永久投资组合和简易全天候,附 2021—2025 五年真实净值回测图,夏普按 rf=0。`example_classic_strategies.py` 导出权益、目标、申赎事件与多账本;使用 `[market,risk,research]` 扩展运行。`example_classic_live.py` 复用配置规则,为新增资金生成申购计划,加 `--submit` 后调用授权的天天基金网实际接口。它不包含完整账户自动再平衡,本次未做真实账户成交验收。
@@ -0,0 +1,50 @@
1
+ # 天天基金 Android 实盘申购协议笔记
2
+
3
+ 依据 Android 6.8.13 安装包里的登录、解锁和买入模块做静态追踪。包内买入入口 `pages/buy-fund/index.js` 调用同一模块的 `common-chunk.js`。以下路径来自客户端代码,真实账户服务端联调待完成。 [安装包来源](https://m.app.so.com/detail/index?id=191826&pname=com.eastmoney.android.fund)
4
+
5
+ ## 初始化和会话
6
+
7
+ 1. 登录页面调用 `POST /User/home/GetCToken`,业务字段为 `DeviceName`、`DeviceType` 和 `DeviceOS`。
8
+ 2. 密码路径调用 `POST /User/Account/LoginForMobileReturnContextId`。短信路径依次调用 `SendPhoneVerifyCodeForLogin`、`CheckVerifyCodeForLoginReturnContextId`,并携带验证码挑战和 `ContextId`。
9
+ 3. 返回 `NewDevice` 时,客户端需要用户完成平台选定的短信、实名、人脸或银行卡验证。完成后将 `CustomerNo`、`CToken`、`UToken` 和分区交给交易模块。
10
+ 4. `POST /User/home/MyAssetUserInfo` 可验证已授权的会话并读取账户信息。`POST /User/Account/RefreshToken` 使用同一账户及现有令牌续期,响应含 `CustomerNo`、`CToken`、`UToken`。
11
+
12
+ 交易模块默认配置 `tradeapilvs{zone}.1234567.com.cn`,使用分区或平台返回的交易主机。表单请求公共参数包括 `UserId`、`CustomerNo`、`CToken`、`UToken`、`MobileKey`、`PhoneType`、`Version`、`ServerVersion` 和 `AppType`。登录模块使用 JSON,并可能启用 `EncryptContentEM` 请求封装。SDK 使用已授权的会话输入,登录挑战和平台要求的传输封装由授权实现提供。
13
+
14
+ ## 申购前检查
15
+
16
+ | POST 路径 | 业务字段和用途 |
17
+ | --- | --- |
18
+ | `/Business/home/GetFundInfo` | `fundCode`。读取基金信息和 `EnableSg` |
19
+ | `/Business/home/RateV3` | `fundCode`、`tradeType=2`、`payType=3`。读取默认申购费率及 `MinBusinLimit`、`MaxBusinLimit` |
20
+ | `/Business/home/GetBussTips` | `fundCode`、`pageName=申购`、`trigWorkDayDebit=0`。App 用于展示申购提示 |
21
+ | `/User/home/GetAndValidateTaxpayer` | `fundCode`。检查 `TaxpayerType` 和 `IsNeedTaxpayerValid` |
22
+ | `/Business/home/UserRiskMatchFund` | `FundCode`、`BType=1`。客户端将 `MatchResult=6` 视为可通过 |
23
+ | `/User/home/GetPayChannels` | `bankAccountNo`。读取选定账户的支付渠道 |
24
+
25
+ App 在提交前显示交易协议并调用交易验证。`VerifyPasswordL1ReturnContextId` 和 `VerifyPasswordL2ReturnContextId` 是交易验证模块里的两个路径,普通买入代码将验证返回值映射到 `L2Password` 或 `EncryptStr`,并携带 `SecuId`。SDK 要求通过授权回调取得本次交易的材料。 [天天基金交易密码说明](https://help.1234567.com.cn/question_331.html)
26
+
27
+ ## 提交和支付
28
+
29
+ 1. `POST /Business/home/NoticeStayTrace` 获取 `TraceID`。SDK 在发送订单前写入本地账本。
30
+ 2. `POST /Trade/FundTrade/CommitOrderL2` 提交。业务表单包含 `TradeType`、`FundAppsJson`、`BankAccountNo`、`TraceID`、`L2Password`、`EncryptStr`、`SecuId` 和支付选项。`FundAppsJson` 是字符串,例如 `[{"fundCode":"025070","amount":"10.00"}]`。默认申购代码为 `AsyC022`,活期宝为 `AsyJCJY022`,App 另有 `JP022-3`、`JP022-4`、`SJP022`。
31
+ 3. 如果响应含 `NeedNextStep=true`,App 将 `ContextId`、流水号和 `BankAppParam` 交给银行支付流程。`NeedSwitchToOrderPage=true` 时 App 跳过第二阶段提交。需要二段确认时,客户端使用 `POST /Trade/FundTrade/CompleteTrade`,传 `fundCode`、`contextId`、`bankAppPayResult`、`traceID`、`tradeFlow`。
32
+ 4. `POST /Trade/FundTrade/OrderResult` 使用 `appSerialNo`、`parentAppSerialNo`、`businType`、`tradeModeType` 查询。App 将 `PayState=1` 映射为支付成功,`2` 为等待,`4` 为暂无数据。SDK 保留 `SUBMITTED` 状态,份额确认回执还需单独核对。
33
+
34
+ App 的响应适配器会合并 `Data.SubscriptioResult`,并以 `AppSheetSerialNo` 作为备用流水号。SDK 同样保留这两种格式。缺少远端流水号的超时请求仍需用官方记录核对。`TraceID` 可用于追踪,当前证据未显示它具备服务端去重能力。
35
+
36
+ ## 赎回、活期宝及撤单
37
+
38
+ | 操作 | 客户端 POST 路径 | 要点 |
39
+ | --- | --- | --- |
40
+ | 查询赎回条款 | `/Business/Rate/GetShareAndRedeemRateList` | 基金代码 |
41
+ | 核对可用份额 | `/Trade/FundTrade/BankAvaliableShareBatch` | 基金代码、绑定支付账户和子账户 |
42
+ | 普通赎回 | `/Business/Hqb/MakeRedemptionL2` | 份额、份额 ID、子账户、支付账户、收款类型、请求号及当次交易授权 |
43
+ | 全额转活期宝 | `/Trade/FundTrade/SFTransferL2` | 转出基金、转入基金、份额 ID、全额标记、份额及当次交易授权 |
44
+ | 活期宝普通取现 | `/Business/Hqb/CashBagWithdrawL2` | 金额、绑定账户、基金代码和当次交易授权 |
45
+ | 查询交易详情 | `/api/mobile/Query/GetQueryDetailsQuickUse` | 独立生产交易查询主机,数据来自 `responseObjects` |
46
+ | 撤单 | `/Trade/FundTrade/RevokeOrderL2` | 交易记录 ID、业务类型、显示代码、退款去向及当次交易授权 |
47
+
48
+ 撤单细节来自天天基金公开分发的 [交易查询模块 4.6.0](https://j2.fund.eastmoney.com/resource/uploadzip/chunkFName_trial_6392757056457568161326923360.zip),与 [官方可撤业务说明](https://help.1234567.com.cn/question_371.html) 相互补充。查询详情读取 `isCanRevoke` 和 `appLst[].AppState`。撤单提交后以详情中的「已撤单」作为确认。组合撤单、活期宝快速取现、批量赎回和其他划转未接入。
49
+
50
+ Windows 示例使用 `TianTianPasswordAuthorizer` 请求交易加密公钥并生成每笔独立授权。客户端存在多种交易验证流程,六位 RSA 路径和银行支付渠道仍需用合法账户验收。
@@ -0,0 +1,90 @@
1
+ # TOKOYO SDK(常世 SDK)0.10.0
2
+
3
+ AI时代的场外基金量化投研SDK
4
+
5
+ 这是独立的 Python SDK 项目。官网、文档、AI Skill 和 Great Prompt 广场由另一个 `web` 项目维护,不参与本项目安装或构建。
6
+
7
+ 在本目录从源码安装:
8
+
9
+ ```powershell
10
+ python -m pip install ".[market,risk]"
11
+ ```
12
+
13
+ 开发与打包:
14
+
15
+ ```powershell
16
+ python -m pip install -e ".[dev,market,risk,rl,research]"
17
+ python -m unittest discover -s tests -v
18
+ python -m build
19
+ ```
20
+
21
+ wheel 与源码发行包生成到本项目的 `dist/`,不包含官网或文档工程。以下已有 wheel 的安装示例也可以继续使用。
22
+
23
+ ## 发布到 PyPI
24
+
25
+ 构建并检查安装包后,运行 `python publish.py --dry-run` 检查待发布文件,再运行 `python publish.py`。后者在本地隐藏输入 PyPI API Token;令牌不写入项目文件或命令参数。也可由发布环境提供 `UV_PUBLISH_TOKEN`。脚本只上传当前版本的 wheel 和源码发行包,并用 PyPI 返回的 SHA-256 校验发布结果。
26
+
27
+ Python 3.10+。示例使用 Windows 控制台。SDK 的天天基金适配器使用真实生产主机和已授权的 Android 交易会话。当前版本依据客户端代码实现,尚待使用授权账户做端到端验收。
28
+
29
+ ## 安装与启动
30
+
31
+ 在 wheel 与 `example_live.py` 所在目录运行:
32
+
33
+ ```powershell
34
+ python -m pip install .\tokoyo_sdk-0.10.0-py3-none-any.whl
35
+ python .\example_live.py --account my-account --check
36
+ python .\example_live.py --account my-account --db .\live-orders.sqlite3
37
+ ```
38
+
39
+ `--check` 只验证会话。需要从本人已授权的 Android 登录取得 `CustomerNo`、`CToken`、`UToken`、`MobileKey` 和正确分区。也可以用 `--provider my_provider` 调用一次本地 `login() -> TianTianSession`。本包未实现手机验证码、新设备验证或自动提取官方 App 会话。银行卡参数使用平台已有的 `BankAccountNo` 标识。
40
+
41
+ 在自己的程序中调用 `from tokoyo import login`,然后用 `login(account)` 在本地终端导入已有的 Android 交易会话。也可以传入 `login(account, login_flow)`,由授权适配器返回 `TianTianSession`。两种方式都在返回交易连接前发起只读账户验证。
42
+
43
+ 启动后可在同一进程用 `assets` 验证会话,`refresh` 尝试续期,`orders` 查看本地账本,`query <本地订单ID>` 查询一笔订单。运行 `--help` 或 `<命令> --help` 查看交互命令参数。以下是参数格式,把尖括号替换为本人真实订单、持仓和目标信息:
44
+
45
+ ```text
46
+ subscribe <基金代码> <金额> --id <业务唯一ID>
47
+ redeem <基金代码> <份额> --share-id <持仓ID> --sub-account <子账户> --id <业务唯一ID>
48
+ redeem <基金代码> <份额> --share-id <持仓ID> --sub-account <子账户> --cashbag <活期宝基金代码> --id <业务唯一ID>
49
+ transfer_all <基金代码> <该持仓全部可用份额> --share-id <持仓ID> --sub-account <子账户> --cashbag <活期宝基金代码> --id <业务唯一ID>
50
+ withdraw <活期宝基金代码> <金额> --id <业务唯一ID>
51
+ cancel <本地订单ID> --business-id <交易记录ID> --business-type <业务类型> --display-code <显示业务代码> --refund-original
52
+ ```
53
+
54
+ `redeem-terms <基金代码>` 读取赎回规则。普通赎回默认到持仓对应的绑定银行卡,也可指定活期宝。划转支持将单个持仓的全部可用份额转入活期宝。取现支持活期宝普通取现到绑定银行卡。撤单需要匹配到本地流水,并由天天基金交易详情返回可撤,支持原路退回及平台明确提供时指定 `--refund-cashbag <代码>`。查看活期宝分类的交易详情时可加 `--cashbag-record`。组合和批量撤单尚未接入。
55
+
56
+ 每笔提交都有本地记录。用同一业务唯一 ID 重复调用会返回已有本地订单。同一个 ID 参数发生变化会报冲突。请求发出后若回执不明,查询官方交易记录核对,SDK 不会自动补发。`submitted` 表示已提交并拿到流水,`cancel_pending` 表示撤单待核对。银行要求二次支付时可通过 SDK 读取支付挑战,银行端的交互和挑战结果由官方渠道完成。
57
+
58
+ 六位交易密码在 Windows 进程内由 `CryptProtectMemory` 加密保存。构造时清除输入缓冲区,签发授权时短暂解密,随后清除 SDK 持有的明文缓冲区,关闭时清除密文。Python 和加密库内部短暂生成的副本无法保证全部清除。SDK 不把密码写入账本。
59
+
60
+ 个人开源版和机构企业版均可使用天天基金网 API 自动下单。配套的 `example_first_tiantian.py` 将 12 周动量规则用于新增资金申购:默认读取真实净值并输出信号,传入 `--submit --account <账户别名>` 后尝试提交实际交易。需提供已授权的 Android Session、六位支付密码和绑定支付账户标识。这个例子不自动赎回原有仓位,参数说明见本地文档的“天天基金网 API 自动下单”。密码保护的详细说明与架构图见“我们如何保护您的支付密码”。
61
+
62
+ 普通申购、赎回、划转、取现和撤单的已接入路径见 [PROTOCOL.md](PROTOCOL.md)。本地验证范围为包导入、命令解析及 Windows 加密与 RSA 往返,无资金操作联调结果。
63
+
64
+ ## 风险分析
65
+
66
+ 用 `python -m pip install ".\tokoyo_sdk-0.10.0-py3-none-any.whl[risk]"` 安装风险依赖。可从 `tokoyo.risk` 调用收益率回归、持仓穿透、Barra 风格截面因子模型、历史 VaR/ES 和 LPPLS。源码包内附 `example_risk.py`,用于对日期对齐的 CSV 运行收益率归因和风险诊断。
67
+
68
+ ## 行情与 Agent Skill
69
+
70
+ 安装 `[market]` 后,从 `tokoyo.market` 调用 `daily_nav`、`nav_bars`、`fund_profile` 和 `fund_holdings`。AI Skill 从配套网站的 Skill 页面下载,供 Agent 使用 TOKOYO 开发策略、看板和投研报告。
71
+
72
+ 从 `tokoyo.strategy` 导入 `Strategy` 和 `run_backtest`。同一目标权重回调可使用单基金历史和截面面板。回测支持日、周、月,按估值、确认、份额可用和到账事件分账。 `example_backtest.py` 可自动下载公开净值并导出账本。 `StrategyRunner` 可为同一回调切换执行模式,实盘需要自备已授权适配器。
73
+
74
+ ## 组合配置与自定义 RL
75
+
76
+ 组合配置接口 `tokoyo.risk.allocate_portfolio`,支持 MV、MVSK、ERC 及自定义风险预算。安装 `[risk]` 扩展即可使用。
77
+
78
+ `tokoyo.rl.RLAllocationPolicy(model, observation_fn, symbols)` 用于接入用户自己的模型,返回统一的目标权重。模型与训练框架由用户管理。可选安装 `[rl]`,使用 `tokoyo.gym.FundAllocationEnv`;环境复用 T+n、多资金账户和份额批次账本,遵循 Gymnasium 接口。
79
+
80
+ 运行 `example_allocation.py --start 2024-01-01 --end 2024-12-31 --method erc` 可自动获取真实历史净值并回测;使用自己的 RL 模块时改为 `--rl-provider my_policy`,模块提供 `model` 和 `observation_fn`。
81
+
82
+ ## Gym 行情抽样
83
+
84
+ 可通过 `MarketSettings`、`FundEpisodeSampler`、`MarketEpisode` 和 `RandomFundAllocationEnv` 指定基金、类型和日期范围,随机抽取真实历史窗口,按共同日期或相对步数组成截面。支持公开行情 CSV 缓存、固定种子与快照回放。相对步数模式会明确标记合成时钟,不保留真实跨基金同期关系。详见文档第 7 章,配套脚本为 `example_gym.py`。
85
+
86
+ ## 宏观数据与经典策略
87
+
88
+ 0.10.0 提供 `tokoyo.macro.macro_catalog`、`macro_series`、`macro_asof` 以及 `cpi`、`ppi`、`pmi`、`lpr` 简写,覆盖九个宏观序列。统计期与可用日期分开,缺少真实公布日时明确标记可用时间假设;返回的是当前历史序列,不是逐次发布的历史版本库。
89
+
90
+ 文档第六章逐步实现永久投资组合、MPT永久投资组合和简易全天候,附 2021—2025 五年真实净值回测图,夏普按 rf=0。`example_classic_strategies.py` 导出权益、目标、申赎事件与多账本;使用 `[market,risk,research]` 扩展运行。`example_classic_live.py` 复用配置规则,为新增资金生成申购计划,加 `--submit` 后调用授权的天天基金网实际接口。它不包含完整账户自动再平衡,本次未做真实账户成交验收。
@@ -0,0 +1,35 @@
1
+ """Backtest ERC/MV/MVSK, or a user's own RL policy, on downloaded fund NAV."""
2
+ import argparse
3
+ from importlib import import_module
4
+ from tokoyo.strategy import Strategy, run_backtest
5
+ from tokoyo.risk import allocate_portfolio
6
+ from tokoyo.rl import RLAllocationPolicy
7
+
8
+
9
+ def main():
10
+ parser = argparse.ArgumentParser(description=__doc__)
11
+ parser.add_argument("--funds", nargs="+", default=["013835", "000001"])
12
+ parser.add_argument("--method", choices=["erc", "mv", "mvsk"], default="erc")
13
+ parser.add_argument("--rl-provider", help="Your module exposing model and observation_fn")
14
+ parser.add_argument("--start", required=True)
15
+ parser.add_argument("--end", required=True)
16
+ parser.add_argument("--cash", type=float, default=100_000)
17
+ args = parser.parse_args()
18
+ funds = tuple(args.funds)
19
+ if args.rl_provider:
20
+ provider = import_module(args.rl_provider)
21
+ policy = RLAllocationPolicy(provider.model, provider.observation_fn, funds)
22
+ else:
23
+ def policy(ctx):
24
+ recent = ctx.returns(lookback=60)
25
+ if len(recent) < 60:
26
+ return None
27
+ return allocate_portfolio(
28
+ recent.to_numpy(), asset_names=list(recent.columns), method=args.method,
29
+ ).weights
30
+ result = run_backtest(Strategy(funds, policy, warmup_days=150), args.cash, args.start, args.end)
31
+ print(result.summary)
32
+
33
+
34
+ if __name__ == "__main__":
35
+ main()
@@ -0,0 +1,44 @@
1
+ """Run a public-NAV strategy and export its event and account ledgers."""
2
+
3
+ import argparse
4
+ import json
5
+ from pathlib import Path
6
+
7
+ from tokoyo.strategy import BacktestConfig, Strategy, run_backtest
8
+
9
+
10
+ def main():
11
+ parser = argparse.ArgumentParser(description=__doc__)
12
+ parser.add_argument("--funds", nargs="+", default=["013835", "000001"])
13
+ parser.add_argument("--period", choices=("day", "week", "month"), default="week")
14
+ parser.add_argument("--capital", type=float, default=100_000)
15
+ parser.add_argument("--start", default="2024-01-01")
16
+ parser.add_argument("--end", default="2024-12-31")
17
+ parser.add_argument("--lookback", type=int, default=3)
18
+ parser.add_argument("--no-impact", action="store_true")
19
+ parser.add_argument("--output", type=Path)
20
+ args = parser.parse_args()
21
+ if args.lookback < 1:
22
+ parser.error("--lookback must be positive")
23
+
24
+ def signal(context):
25
+ changes = context.returns(lookback=args.lookback)
26
+ if len(changes) < args.lookback:
27
+ return None
28
+ momentum = (1 + changes).prod() - 1
29
+ if momentum.mean() <= 0:
30
+ return {}
31
+ return {momentum.idxmax(): 0.8}
32
+
33
+ strategy = Strategy(tuple(args.funds), signal, period=args.period, warmup_days=45 * args.lookback)
34
+ config = BacktestConfig(impact=None) if args.no_impact else BacktestConfig()
35
+ result = run_backtest(strategy, args.capital, args.start, args.end, config=config)
36
+ print(json.dumps(result.summary, indent=2, ensure_ascii=False))
37
+ if args.output is not None:
38
+ args.output.mkdir(parents=True, exist_ok=True)
39
+ for name in ("equity_curve", "trades", "events", "cash_ledger", "position_ledger"):
40
+ getattr(result, name).to_csv(args.output / f"{name}.csv", index=False, encoding="utf-8-sig")
41
+
42
+
43
+ if __name__ == "__main__":
44
+ main()
@@ -0,0 +1,91 @@
1
+ """Compute classic portfolio targets from published NAV; optionally invest NEW cash.
2
+
3
+ This is an initial-funding/additional-cash example, not a whole-account rebalancer.
4
+ Never pass credentials as arguments. Actual submissions require --submit.
5
+ """
6
+ import argparse
7
+ from datetime import date, datetime, timedelta, timezone
8
+ from decimal import Decimal, ROUND_DOWN
9
+ from getpass import getpass
10
+ import json
11
+ from pathlib import Path
12
+ from types import SimpleNamespace
13
+ import pandas as pd
14
+ from example_classic_strategies import GROUPS, PERMANENT, MonthlyTargets, history_data, macro_data
15
+ from example_first_tiantian import money, read_payment_password
16
+ from tokoyo import (FundAccount, FundTrader, OrderSide, OrderStatus, PaymentPassword,
17
+ TianTianGateway, TianTianPasswordAuthorizer, login)
18
+
19
+
20
+ def subscription_plan(weights, budget, reserve):
21
+ if not weights or any(not pd.notna(w) or w < 0 or w > 1 for w in weights.values()):
22
+ raise ValueError("Invalid target weights")
23
+ if sum(weights.values()) > 1 + 1e-9:
24
+ raise ValueError("Target weights exceed one")
25
+ spendable=budget*(Decimal(1)-reserve)
26
+ return [{"fund":s,"amount":str((spendable*Decimal(str(w))).quantize(Decimal('.01'),rounding=ROUND_DOWN))}
27
+ for s,w in sorted(weights.items()) if spendable*Decimal(str(w)) >= Decimal('.01')]
28
+
29
+
30
+ def main():
31
+ parser=argparse.ArgumentParser(description=__doc__)
32
+ parser.add_argument('--strategy',choices=('permanent','mpt','all_weather'),required=True)
33
+ parser.add_argument('--budget',type=money,required=True,help='NEW available CNY budget, not account equity')
34
+ parser.add_argument('--nav-through',type=date.fromisoformat,required=True,help='Verified published NAV cutoff')
35
+ parser.add_argument('--reserve',type=Decimal,default=Decimal('.01'),help='Cash reserve fraction for fees, default 1%%')
36
+ parser.add_argument('--cache',default='classic-live-cache')
37
+ parser.add_argument('--plan',default='classic-live-plan.json')
38
+ parser.add_argument('--strategy-id',help='Stable strategy identifier; defaults to classic-<strategy>-v1')
39
+ parser.add_argument('--account')
40
+ parser.add_argument('--db',default='classic-live.sqlite3')
41
+ parser.add_argument('--submit',action='store_true')
42
+ args=parser.parse_args()
43
+ today=datetime.now(timezone(timedelta(hours=8))).date()
44
+ if args.nav_through > today or (args.submit and today-args.nav_through > timedelta(days=7)):
45
+ parser.error('For real orders, verify a published NAV cutoff within the last seven days')
46
+ if not args.reserve.is_finite() or not Decimal(0) <= args.reserve < Decimal(1):
47
+ parser.error('--reserve must be a finite fraction in [0,1)')
48
+ if args.submit and not args.account:
49
+ parser.error('--submit requires --account')
50
+ strategy_id=args.strategy_id or f'classic-{args.strategy}-v1'
51
+ if not strategy_id.strip():parser.error('Strategy ID must not be empty')
52
+ cache=Path(args.cache);cache.mkdir(parents=True,exist_ok=True)
53
+ # Download at least 800 calendar days, retain only mutually observed NAV dates.
54
+ history=history_data(cache,str(args.nav_through),str(args.nav_through))
55
+ symbols=tuple(s for codes in GROUPS.values() for s in codes) if args.strategy=='all_weather' else PERMANENT
56
+ panels={s:history[s].set_index('date').daily_return for s in symbols}
57
+ returns=pd.DataFrame(panels).dropna().sort_index()
58
+ if returns.empty or (args.submit and today-returns.index[-1].date() > timedelta(days=7)):
59
+ raise ValueError('No sufficiently recent common NAV date for actual submission')
60
+ policy=MonthlyTargets(args.strategy,macro_data(cache,str(today),refresh=True) if args.strategy=='all_weather' else None)
61
+ ctx=SimpleNamespace(date=today,returns=lambda lookback:returns.tail(lookback))
62
+ weights=policy(ctx)
63
+ if weights is None:raise ValueError('Insufficient history for a target')
64
+ orders=subscription_plan(weights,args.budget,args.reserve)
65
+ plan={'strategy':args.strategy,'strategy_id':strategy_id,'decision_date':str(today),
66
+ 'nav_through':str(args.nav_through),'latest_common_nav':str(returns.index[-1].date()),
67
+ 'new_cash_budget':str(args.budget),'reserve':str(args.reserve),'weights':weights,
68
+ 'orders':orders,'scope':'new_cash_subscriptions_only'}
69
+ Path(args.plan).write_text(json.dumps(plan,ensure_ascii=False,indent=2),encoding='utf-8')
70
+ print(json.dumps(plan,ensure_ascii=False,indent=2))
71
+ if not args.submit:return
72
+ account=FundAccount(args.account)
73
+ transport=login(account)
74
+ bank=getpass('Bound BankAccountNo (hidden): ')
75
+ with PaymentPassword(read_payment_password()) as password:
76
+ gateway=TianTianGateway(transport,TianTianPasswordAuthorizer(transport,password,bank))
77
+ with FundTrader(gateway,db_path=args.db,enable_live=True) as trader:
78
+ for instruction in orders:
79
+ symbol=instruction['fund']
80
+ order_id=trader.order_fund(account,symbol,OrderSide.SUBSCRIBE,
81
+ amount=Decimal(instruction['amount']),
82
+ client_order_id=f'{strategy_id}:{today:%Y-%m}:{symbol}:subscribe',
83
+ strategy_name=strategy_id,order_remark='classic portfolio new cash')
84
+ order=trader.query_fund_order(account,order_id)
85
+ print(json.dumps({'fund':symbol,'order_id':order.order_id,'status':order.status.value,
86
+ 'broker_order_id':order.broker_order_id},ensure_ascii=False))
87
+ if order.status not in {OrderStatus.SUBMITTED,OrderStatus.CONFIRMED}:
88
+ raise RuntimeError('Reconcile this order or complete official bank payment before continuing')
89
+
90
+
91
+ if __name__=='__main__':main()