├── .gitignore ├── README.md ├── __init__.py ├── backtest.py ├── data.zip ├── example.ipynb ├── factor_analysis.py ├── group_calc.py ├── preprocess.py ├── requirements.txt └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | /__pycache__/* 2 | *.ipynb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | # 因子回测框架说明 7 | 本框架包含了常见的因子处理和回测函数,并给出了示例。 8 | 9 | 使用本框架请先解压data.zip到当前文件夹下,并通过 10 | ``` 11 | pip install -r requriements.txt 12 | ``` 13 | 下载相应的安装包。 14 | 具体代码使用可参考example.ipynb。 15 | 16 | ## 更新 17 | 2023-07-09: 修正了市值因子没提前一期的bug,增加了单分组回测结果可视化、序贯排序等函数 18 | 19 | ## 文件说明 20 | 1. `data`: 数据文件夹 21 | 2. `example.ipynb`:示例文件 22 | 3. `preprocess.py`: 数据预处理文件 23 | 4. `group_calc.py`: 分组回测文件 24 | 5. `factor_analysis.py`: 因子分析模块 25 | 6. `backtest.py`: 回测模块 26 | 7. `utils.py`: 工具性函数 27 | 28 | ## 函数文档说明 29 | ### `preprocess.py` 30 | 1. del_outlier 31 | ``` 32 | Description 33 | ---------- 34 | 对每期因子进行去极值 35 | 36 | Parameters 37 | ---------- 38 | factor_df: pandas.DataFrame. 因子数据,格式为trade_date,stock_code,factor 39 | factor_name: str. 因子名称 40 | method: str. 去极值方式,为'mad'或'sigma',默认为mad 41 | n: float.去极值的n值.默认取值为3 42 | 43 | Return 44 | ---------- 45 | pandas.DataFrame. 46 | 去极值后的因子数据, 格式为trade_date,stock_code,factor 47 | ``` 48 | 49 | 2. standardize 50 | ``` 51 | Description 52 | ---------- 53 | 标准化 54 | 55 | Parameters 56 | ---------- 57 | factor: pandas.DataFrame,因子值,格式为trade_date,stock_code,factor 58 | factor_name: str.因子名称 59 | method: str.中性化方式,可选为'rank'(排序标准化)或者'zscore'(Z-score标准化),默认为rank 60 | 61 | Return 62 | ---------- 63 | pandas.DataFrame. 64 | 标准化后的因子数据, 格式为trade_date,stock_code,factor 65 | ``` 66 | 67 | 3. neutralize 68 | ``` 69 | Description 70 | ---------- 71 | 中性化 72 | 73 | Parameters 74 | ---------- 75 | factor_df: pandas.DataFrame. 76 | 因子值, 格式为trade_date,stock_code,factor 77 | mktmv_df: pandas.DataFrame. 78 | 股票流通市值,格式为trade_date,stock_code,mktmv. 79 | 默认为None即不进行市值中性化 80 | industry_df: pandas.DataFrame, 股票所属行业, 格式为trade_date,stock_code,ind_code.默认为None即不进行行业中性化 81 | 82 | Return 83 | ---------- 84 | pandas.DataFrame. 85 | 中性化后的因子数据, 格式为trade_date,stock_code,factor 86 | ``` 87 | 88 | ### `group_calc.py` 89 | 1. get_stock_group 90 | ``` 91 | Description 92 | ---------- 93 | 通过因子值,对股票分成n_groups组 94 | 组名从小到大为group0到group{n_groups-1} 95 | 96 | Parameters 97 | ---------- 98 | factor_df: pandas.DataFrame. 99 | 因子数据,格式为trade_date, stock_code, factor_name 100 | factor_name: str. 101 | 因子名称 102 | n_groups: int. 103 | 分组数量 104 | 105 | Return 106 | ---------- 107 | pandas.DataFrame. 108 | 格式为trade_date, stock_code, factor_name, factor_name_group 109 | ``` 110 | 111 | 2. get_group_ret 112 | ``` 113 | Description 114 | ---------- 115 | 计算分组的收益率 116 | 117 | Parameters 118 | ---------- 119 | factor_df: pandas.DataFrame. 120 | 未提前的因子数据,格式为trade_date, stock_code, factor_name 121 | ret_df: pandas.DataFrame. 122 | 收益率数据,格式为trade_date, stock_code, ret 123 | factor_name: str. 124 | 因子名称 125 | n_groups: int. 126 | 分组数量. 127 | mktmv_df: pandas.DataFrame. 默认为None,即等权 128 | 市值数据.格式为trade_date, stock_code, mktmv 129 | 130 | Return 131 | ---------- 132 | pandas.DataFrame. 133 | 索引为trade_date, 列名为group0到group{n_group-1},和factor_ret 134 | ``` 135 | 136 | 3. get_group_ret_backtest 137 | ``` 138 | Description 139 | ---------- 140 | 计算各组收益率的回测指标 141 | 142 | Parameters 143 | ---------- 144 | group_ret: pandas.DataFrame. 145 | 各组收益率, 每列为各组收益率的时间序列 146 | rf: float. 147 | 无风险收益率, 默认为0 148 | benchmark: pandas. DataFrame. 149 | 基准收益率数据,格式为trade_date, ret 150 | period: str. 指定数据频率 151 | 有DAILY, WEEKLY, MONTHLY三种, 默认为DAILY 152 | 153 | Return 154 | ---------- 155 | pandas.DataFrame. 列名为分组名称 156 | 行名为年化收益率(%), 年化波动率(%), 夏普比率, 最大回撤(%) 157 | 若benchmark不为None,则会额外输出: 超额年化收益率(%), 158 | 超额年化波动率(%). 信息比率, 相对基准胜率(%), 超额收益最大回撤(%) 159 | ``` 160 | 161 | 4. analysis_group_ret 162 | ``` 163 | Description 164 | ---------- 165 | 单分组下的因子分析 166 | 167 | Parameters 168 | ---------- 169 | factor_df: pandas.DataFrame. 170 | 未提前的因子数据,格式为trade_date, stock_code, factor_name 171 | ret_df: pandas.DataFrame. 172 | 收益率数据,格式为trade_date, stock_code, ret 173 | factor_name: str. 174 | 因子名称 175 | n_groups: int. 176 | 分组数量. 177 | mktmv_df: pandas.DataFrame. 178 | 市值数据.格式为trade_date, stock_code, mktmv. 默认为None,即等权 179 | rf: float. 180 | 无风险收益率, 默认为0 181 | benchmark: pandas. DataFrame. 182 | 基准收益率数据,格式为trade_date, ret 183 | period: str. 指定数据频率 184 | 有DAILY, WEEKLY, MONTHLY三种, 默认为DAILY 185 | 186 | Return 187 | ---------- 188 | tuple. 第一个为分组回测指标,格式为pandas.DataFrame 189 | 第二个为分组净值曲线图,第三个为因子多空净值曲线图 190 | ``` 191 | 192 | 193 | 5. get_double_sort_group 194 | ``` 195 | Description 196 | ---------- 197 | 获取序贯排序双分组情况 198 | 199 | Parameters 200 | ---------- 201 | factor1_df: pandas.DataFrame. 202 | 未提前的因子数据,格式为trade_date, stock_code, factor1_name 203 | factor2_df: pandas.DataFrame. 204 | 未提前的因子数据,格式为trade_date, stock_code, factor2_name 205 | factor1_name: str. 206 | 因子1名称 207 | factor2_name: str. 208 | 因子2名称 209 | n_groups1: int. 210 | 对因子1分组数量. 211 | n_groups2: int. 212 | 对因子2分组数量. 213 | 214 | Return 215 | ---------- 216 | pandas.DataFrame. 217 | 列名为trade_date, stock_code, factor1_name, factor2_name, group1_name, group2_name, ret 218 | ``` 219 | 220 | 6. get_double_sort_group_ret 221 | ``` 222 | Description 223 | ---------- 224 | 计算序贯排序双分组的收益率 225 | 226 | Parameters 227 | ---------- 228 | factor1_df: pandas.DataFrame. 229 | 未提前的因子数据,格式为trade_date, stock_code, factor1_name 230 | factor2_df: pandas.DataFrame. 231 | 未提前的因子数据,格式为trade_date, stock_code, factor2_name 232 | ret_df: pandas.DataFrame. 233 | 收益率数据,格式为trade_date, stock_code, ret 234 | factor1_name: str. 235 | 因子1名称 236 | factor2_name: str. 237 | 因子2名称 238 | n_groups1: int. 239 | 对因子1分组数量. 240 | n_groups2: int. 241 | 对因子2分组数量. 242 | mktmv_df: pandas.DataFrame. 默认为None,即等权 243 | 未提前的市值数据.格式为trade_date, stock_code, mktmv 244 | 245 | Return 246 | ---------- 247 | pandas.DataFrame. 248 | 列名为trade_date, group1_name, group2_name, ret 249 | ``` 250 | 251 | 7. double_sort_mean 252 | ``` 253 | Description 254 | ---------- 255 | 计算序贯排序双分组收益率均值 256 | 257 | Parameters 258 | ---------- 259 | group_ret: pandas.DataFrame. 260 | 各组收益率, 格式为trade_date, group1_name, group2_name, ret 261 | factor1_name: str. 262 | 因子1名称 263 | factor2_name: str. 264 | 因子2名称 265 | 266 | Return 267 | ---------- 268 | pandas.DataFrame. 269 | 列名为Group0, Group1, …, Groupm, H-L 270 | 索引第一层为Group0,…,Groupn 271 | 第二层为ret_mean(%), tvalue 272 | ``` 273 | 274 | 8. double_sort_backtest 275 | ``` 276 | Description 277 | ---------- 278 | 计算序贯排序双分组的回测指标 279 | 280 | Parameters 281 | ---------- 282 | group_ret: pandas.DataFrame. 283 | 各组收益率, 格式为trade_date, group1_name, group2_name, ret 284 | factor1_name: str. 285 | 因子1名称 286 | factor2_name: str. 287 | 因子2名称 288 | rf: float. 289 | 无风险收益率, 默认为0 290 | benchmark: pandas. DataFrame. 291 | 基准收益率数据,格式为trade_date, ret 292 | period: str. 指定数据频率 293 | 有DAILY, WEEKLY, MONTHLY三种, 默认为DAILY 294 | 295 | Return 296 | ---------- 297 | pandas.DataFrame. 298 | 列名为Group0, …, Groupm, H-L 299 | 索引第一层为Group0, …, Groupn, 300 | 第二层为年化收益率(%), 年化波动率(%), 夏普比率, 最大回撤(%) 301 | 若benchmark不为None,则第二层会额外输出:超额年化收益率(%), 302 | 超额年化波动率(%). 信息比率, 相对基准胜率(%), 超额收益最大回撤(%) 303 | ``` 304 | 305 | ### `factor_analysis.py` 306 | 1. get_factor_ic 307 | ``` 308 | Description 309 | ---------- 310 | 计算因子IC序列 311 | 312 | Parameters 313 | ---------- 314 | factor_df: pandas.DataFrame. 未提前的因子数据. 315 | 316 | Return 317 | ---------- 318 | pandas.DataFrame. 319 | ``` 320 | 321 | 2. newy_west_test: 322 | ``` 323 | Description 324 | ---------- 325 | 计算收益率均值并输出Newywest-t统计量和p值 326 | 327 | Parameters 328 | ---------- 329 | arr: array_like. 收益率序列 330 | factor_name: str. 因子名称, 默认为'factor' 331 | max_lags: int. 滞后阶数. 默认为None, 即int(4*(T/100)**(2/9)) 332 | 333 | Return 334 | ---------- 335 | Dict. 336 | 输出示例为{"ret_mean(%)":10%, "t-value": 2.30, "p-value": 0.02, "p-star": **} 337 | ``` 338 | 339 | 3. analysis_factor_ic 340 | ``` 341 | Description 342 | ---------- 343 | 分析因子IC 344 | 345 | Parameters 346 | ---------- 347 | factor_df: pandas.DataFrame. 348 | 未提前的因子数据,格式为trade_date, stock_code, factor_name 349 | ret_df: pandas.DataFrame. 350 | 收益率数据,格式为trade_date, stock_code, ret 351 | factor_name: str. 352 | 因子名称, 默认为'factor' 353 | 354 | Return 355 | ---------- 356 | tuple. 第一个为Dict,格式为dct = { 357 | "因子名称": [factor_name], 358 | "IC均值": [ic_mean], 359 | "IC标准差": [ic_std], 360 | "IR比率": [ir_ratio], 361 | "IC>0的比例(%)": [ic0_ratio], 362 | "IC>0.02的比例(%)": [ic002_ratio], 363 | } 364 | 第二个为因子的IC时序图和累计图 365 | ``` 366 | 367 | 368 | 4. risk_adj_alpha 369 | ``` 370 | Description 371 | ---------- 372 | 计算因子收益率经风险调整后的Alpha和t值 373 | 374 | Parameters 375 | ---------- 376 | factor_ret: pandas.DataFrame. 待检测因子收益率序列 377 | risk_factor_ret: pandas.DataFrame. 风险因子收益率矩阵 378 | max_lags: int. 滞后阶数. 默认为None, 即int(4*(T/100)**(2/9)) 379 | 380 | Return 381 | ---------- 382 | tuple. 为(alpha, alphat) 383 | ``` 384 | 385 | 386 | 5. fama_macbeth_reg 387 | ``` 388 | Description 389 | ---------- 390 | Fama-Macbeth回归 391 | 返回平均观测值数量, 系数对应的Newey-West t值,估计参数和R-square 392 | 393 | Parameters 394 | ---------- 395 | ret: pandas.DataFrame. 396 | 股票收益率数据, 格式为trade_date, stock_code, ret 397 | factor_df: pandas.DataFrame. 398 | 因子数据, 格式为trade_date, stock_code, factor_name 399 | factor_name_lst: List[str]. 400 | 因子变量名列表。输入格式为列表[factor_name1, factor_name2, …, factor_namem] 401 | 402 | Return 403 | ---------- 404 | Dict.输出示例为: { 405 | "factor_name": factor_name_lst, 406 | "beta": list(fama_macbeth.params[1:]), 407 | "t-value": list(fama_macbeth.tstats[1:]), 408 | "R-square": fama_macbeth.rsquared, 409 | "Average-Obs": fama_macbeth.time_info[0], 410 | } 411 | ``` 412 | 413 | 414 | ### `backtest.py` 415 | 1. net_value 416 | ``` 417 | Description 418 | ---------- 419 | 计算净值曲线 420 | 421 | Parameters 422 | ---------- 423 | returns: array_like. 收益率序列 424 | 425 | Return 426 | ---------- 427 | numpy.ndarray. 净值曲线 428 | ``` 429 | 430 | 2. previous_peak 431 | ``` 432 | Description 433 | ---------- 434 | 计算历史最大净值 435 | 436 | Parameters 437 | ---------- 438 | returns: array_like. 收益率序列 439 | 440 | Return 441 | ---------- 442 | numpy.ndarray. 历史最大净值 443 | ``` 444 | 445 | 446 | 3. drawdown 447 | ``` 448 | Description 449 | ---------- 450 | 计算回撤序列,回撤=净值现值/历史最大净值-1 451 | 452 | Parameters 453 | ---------- 454 | returns: array_like. 收益率序列 455 | 456 | Return 457 | ---------- 458 | numpy.ndarray. 单位为% 459 | ``` 460 | 461 | 462 | 4. max_drawdown 463 | ``` 464 | Description 465 | ---------- 466 | 计算最大回撤 467 | 468 | Parameters 469 | ---------- 470 | returns: array_like. 收益率序列 471 | 472 | Return 473 | ---------- 474 | float. 最大回撤. 单位为% 475 | ``` 476 | 477 | 478 | 5. annualized_return 479 | ``` 480 | Description 481 | ---------- 482 | 计算年化收益率 483 | 484 | Parameters 485 | ---------- 486 | returns: array_like. 收益率序列 487 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 488 | 489 | Return 490 | ---------- 491 | float. 年化收益率.单位为% 492 | ``` 493 | 494 | 495 | 6. annualized_volatility 496 | ``` 497 | Description 498 | ---------- 499 | 计算年化波动率 500 | 501 | Parameters 502 | ---------- 503 | returns: array_like. 收益率序列 504 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 505 | 506 | Return 507 | ---------- 508 | float. 年化波动率.单位为% 509 | ``` 510 | 511 | 512 | 7. annualized_sharpe 513 | ``` 514 | Description 515 | ---------- 516 | 计算年化夏普比率.年化夏普=(年化收益率-无风险收益率)/年化波动率 517 | 518 | Parameters 519 | ---------- 520 | returns: array_like. 收益率序列 521 | rf: float. 无风险收益率, 单位为绝对值, 默认为0 522 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 523 | 524 | Return 525 | ---------- 526 | float. 年化夏普比率. 527 | ``` 528 | 529 | 530 | 8. er_annual_return 531 | ``` 532 | Description 533 | ---------- 534 | 计算年化超额收益率, 即(1+年化策略收益率)/(1+年化基准收益率)-1 535 | 536 | Parameters 537 | ---------- 538 | returns: array_like. 收益率序列 539 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 540 | 541 | Return 542 | ---------- 543 | float. 年化超额收益率.单位为% 544 | ``` 545 | 546 | 9. er_annual_volatility 547 | ``` 548 | Description 549 | ---------- 550 | 计算超额收益的年化波动率 551 | 552 | Parameters 553 | ---------- 554 | returns: array_like. 收益率序列 555 | benchmark_returns: array_like. 基准收益率序列 556 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 557 | 558 | Return 559 | ---------- 560 | float. 超额收益的年化波动率.单位为% 561 | ``` 562 | 563 | 10. information_ratio 564 | ``` 565 | Description 566 | ---------- 567 | 计算信息比率, 即超额年化收益率/超额年化夏普比率 568 | 569 | Parameters 570 | ---------- 571 | returns: array_like. 收益率序列 572 | benchmark_returns: array_like. 基准收益率序列 573 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 574 | 575 | Return 576 | ---------- 577 | float. 信息比率 578 | ``` 579 | 580 | 11. er_max_drawdown 581 | ``` 582 | Description 583 | ---------- 584 | 计算超额收益的最大回撤 585 | 586 | Parameters 587 | ---------- 588 | returns: array_like. 收益率序列 589 | benchmark_returns: array_like. 基准收益率序列 590 | 591 | Return 592 | ---------- 593 | float. 超额收益的最大回撤.单位为% 594 | ``` 595 | 596 | 12. winrate 597 | ``` 598 | Description 599 | ---------- 600 | 计算策略相对于基准的胜率 601 | 602 | Parameters 603 | ---------- 604 | returns: array_like. 收益率序列 605 | benchmark_returns: array_like. 基准收益率序列 606 | 607 | Return 608 | ---------- 609 | float. 策略相对于基准的胜率.单位为% 610 | ``` 611 | 612 | 13. get_backtest_result 613 | ``` 614 | Description 615 | ---------- 616 | 输出回测指标. 包含年化收益率、年化波动率、夏普比率、最大回撤等 617 | 618 | Parameters 619 | ---------- 620 | returns: array_like. 收益率序列 621 | rf: float. 无风险收益率, 单位为绝对值, 默认为0 622 | benchmark_returns: array_like. 基准收益率序列, 默认为None,即不计算相关指标 623 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 624 | 625 | Return 626 | ---------- 627 | Dict. 输出格式为{ 628 | '年化收益率(%)': [ann_ret], 629 | '年化波动率(%)': [ann_vol], 630 | '夏普比率': [ann_sp], 631 | '最大回撤(%)': [mdd], 632 | } 633 | 若benchmark不为None,则会额外输出:超额年化收益率(%), 634 | 超额年化波动率(%). 信息比率, 相对基准胜率(%), 超额收益最大回撤(%) 635 | ``` 636 | 637 | 638 | ### `utils.py` 639 | 1. get_previous_factor 640 | ``` 641 | Description 642 | ---------- 643 | 获取上期因子值 644 | 645 | Parameters 646 | ---------- 647 | factor_df: pandas.DataFrame. 输入因子数据,必须含有trade_date 648 | 649 | Return 650 | ---------- 651 | pandas.DataFrame. 上期因子值 652 | ``` 653 | 654 | 655 | 2. stackdf 656 | ``` 657 | Description 658 | ---------- 659 | 对输入数据进行堆栈,每行为截面数据,每列为时间序列数据 660 | 661 | Parameters 662 | ---------- 663 | df: pandas.DataFrame. 664 | 输出数据为堆栈后的数据 665 | date_name: str. 日期名称, 默认为trade_date 666 | code_name: str. 代码名称, 默认为stock_code 667 | 668 | Return 669 | ---------- 670 | pandas.DataFrame. 671 | 堆栈后的数据,列为trade_date, stock_code和var_name 672 | ``` 673 | 674 | 675 | 3. unstackdf 676 | ``` 677 | Description 678 | ---------- 679 | 反堆栈函数 680 | 681 | Parameters 682 | ---------- 683 | df: pandas.DataFrame. 684 | 输入列必须为三列且必须有date_name和code_name 685 | date_name: str. 日期名称, 默认为trade_date 686 | code_name: str. 代码名称, 默认为stock_code 687 | 688 | Return 689 | ---------- 690 | pandas.DataFrame. 反堆栈后的数据 691 | ``` 692 | 693 | 694 | 4. get_last_date 695 | ``` 696 | Description 697 | ---------- 698 | 获取交易日历中历史最近的日期 699 | 700 | Parameters 701 | ---------- 702 | date: str. 所选日期 703 | trade_date_lst: List[str]. 交易日历列表 704 | 705 | Return 706 | ---------- 707 | str. 交易日历中未来最近的日期 708 | ``` 709 | 710 | 711 | 5. get_next_date 712 | ``` 713 | Description 714 | ---------- 715 | 获取交易日历中未来最近的日期 716 | 717 | Parameters 718 | ---------- 719 | date: str. 所选日期 720 | trade_date_lst: List[str]. 交易日历列表 721 | 722 | Return 723 | ---------- 724 | str. 交易日历中未来最近的日期 725 | ``` 726 | 727 | 6. plot_bar_line 728 | ``` 729 | Description 730 | ---------- 731 | 绘制双坐标的柱状图和线形图 732 | 733 | Parameters 734 | ---------- 735 | x1: array_like, 柱状图横坐标 736 | y1: array_like, 柱状图纵坐标 737 | x2: array_like, 线形图横坐标 738 | y2: array_like, 线形图纵坐标 739 | xlabel: str. 横坐标标签 740 | ylabel1: str. 柱状图纵坐标标签 741 | ylabel2: str. 柱状图纵坐标标签 742 | fig_title: str. 图片标题 743 | 744 | Return 745 | ---------- 746 | figure. 747 | ``` 748 | 749 | 7. plot_multi_line 750 | ``` 751 | Description 752 | ---------- 753 | 绘制多根折线图 754 | 755 | Parameters 756 | ---------- 757 | x_lst: array_like, 折线图横坐标列表 758 | y_lst: array_like, 折线图纵坐标列表 759 | label_lst: array_like, 折线图标签列表 760 | xlabel: str. 折线图横坐标标签 761 | ylabel: str. 折线图纵坐标标签 762 | fig_title: str. 图片标题 763 | 764 | Return 765 | ---------- 766 | figure. 767 | ``` -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkl0707/factor_backtest/4f2b2dfdcf1c13e34f35caf1dba102c0ebd0a9d7/__init__.py -------------------------------------------------------------------------------- /backtest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: dkl 3 | Description: 4 | 回测模块,包含: 5 | * 净值曲线计算:net_value 6 | * 历史最大净值计算:previous_peak 7 | * 回撤序列计算:drawdown 8 | * 计算最大回撤:max_drawdown 9 | * 年化收益率计算:annualized_return 10 | * 年化波动率计算:annualized_volatility 11 | * 年化夏普比率计算:annualized_sharpe 12 | * 计算年化超额收益率: er_annual_return 13 | * 计算超额收益的年化波动率: er_annual_volatility 14 | * 计算信息比率: information_ratio 15 | * 计算超额收益的最大回撤: er_max_drawdown 16 | * 计算策略相对于基准的胜率: winrate 17 | * 输出回测指标: get_backtest_result 18 | Date: 2023-07-07 08:57:59 19 | """ 20 | import numpy as np 21 | 22 | 23 | # 根据数据频率映射到相应的年化因子 24 | mapping_dct = {"DAILY": 252, "WEEKLY": 52, "MONTHLY": 12} 25 | 26 | 27 | def _convert_returns_type(returns): 28 | try: 29 | returns = np.asarray(returns) 30 | except Exception: 31 | raise ValueError("returns is not array_like.") 32 | return returns 33 | 34 | 35 | def _check_period(period): 36 | period_lst = list(mapping_dct.keys()) 37 | if period not in period_lst: 38 | period_str = ",".join(period_lst) 39 | raise ValueError(f"period must be in {period_str}") 40 | return 41 | 42 | 43 | def net_value(returns): 44 | """ 45 | Description 46 | ---------- 47 | 计算净值曲线 48 | 49 | Parameters 50 | ---------- 51 | returns: array_like. 收益率序列 52 | 53 | Return 54 | ---------- 55 | numpy.ndarray. 净值曲线 56 | """ 57 | returns = _convert_returns_type(returns) 58 | return np.cumprod(1 + returns) 59 | 60 | 61 | def previous_peak(returns): 62 | """ 63 | Description 64 | ---------- 65 | 计算历史最大净值 66 | 67 | Parameters 68 | ---------- 69 | returns: array_like. 收益率序列 70 | 71 | Return 72 | ---------- 73 | numpy.ndarray. 历史最大净值 74 | """ 75 | returns = _convert_returns_type(returns) 76 | nv = net_value(returns) 77 | return np.maximum.accumulate(nv) 78 | 79 | 80 | def drawdown(returns): 81 | """ 82 | Description 83 | ---------- 84 | 计算回撤序列,回撤=净值现值/历史最大净值-1 85 | 86 | Parameters 87 | ---------- 88 | returns: array_like. 收益率序列 89 | 90 | Return 91 | ---------- 92 | numpy.ndarray. 回撤序列, 单位为% 93 | """ 94 | returns = _convert_returns_type(returns) 95 | nv = net_value(returns) 96 | previous_peaks = previous_peak(returns) 97 | dd = (nv / previous_peaks - 1) * 100 98 | return dd 99 | 100 | 101 | def max_drawdown(returns): 102 | """ 103 | Description 104 | ---------- 105 | 计算最大回撤 106 | 107 | Parameters 108 | ---------- 109 | returns: array_like. 收益率序列 110 | 111 | Return 112 | ---------- 113 | float. 最大回撤. 单位为% 114 | """ 115 | returns = _convert_returns_type(returns) 116 | dd = drawdown(returns) 117 | # 注意上述drawdown单位已为% 118 | mdd = np.min(dd) 119 | return mdd 120 | 121 | 122 | def annualized_return(returns, period="DAILY"): 123 | """ 124 | Description 125 | ---------- 126 | 计算年化收益率, 即净值**(1/(T/ann_factor))-1 127 | 128 | Parameters 129 | ---------- 130 | returns: array_like. 收益率序列 131 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 132 | 133 | Return 134 | ---------- 135 | float. 年化收益率.单位为% 136 | """ 137 | _check_period(period) 138 | returns = _convert_returns_type(returns) 139 | ann_factor = mapping_dct[period] 140 | # 交易日数量 141 | n = returns.shape[0] 142 | # 计算最后的净值 143 | nv = net_value(returns) 144 | final_value = nv[-1] 145 | # 年化收益率 146 | ann_ret = 100 * (final_value ** (ann_factor / n) - 1) 147 | return ann_ret 148 | 149 | 150 | def annualized_volatility(returns, period="DAILY"): 151 | """ 152 | Description 153 | ---------- 154 | 计算年化波动率,即标准差*sqrt(ann_factor) 155 | 156 | Parameters 157 | ---------- 158 | returns: array_like. 收益率序列 159 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 160 | 161 | Return 162 | ---------- 163 | float. 年化波动率.单位为% 164 | """ 165 | _check_period(period) 166 | returns = _convert_returns_type(returns) 167 | ann_factor = mapping_dct[period] 168 | ann_vol = 100 * np.std(returns) * np.sqrt(ann_factor) 169 | return ann_vol 170 | 171 | 172 | def annualized_sharpe(returns, rf=0, period="DAILY"): 173 | """ 174 | Description 175 | ---------- 176 | 计算年化夏普比率.年化夏普=(年化收益率-无风险收益率)/年化波动率 177 | 178 | Parameters 179 | ---------- 180 | returns: array_like. 收益率序列 181 | rf: float. 无风险收益率, 默认为0 182 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 183 | 184 | Return 185 | ---------- 186 | float. 年化夏普比率. 187 | """ 188 | _check_period(period) 189 | returns = _convert_returns_type(returns) 190 | ann_ret = annualized_return(returns, period) 191 | ann_vol = annualized_volatility(returns, period) 192 | if ann_vol < 1e-10: 193 | return 0 194 | return (ann_ret / 100 - rf) / (ann_vol / 100) 195 | 196 | 197 | # 和基准比较模块 198 | def _compare_length(returns, benchmark_returns): 199 | if len(returns) != len(benchmark_returns): 200 | message = "Length of returns must be equal to length of benchmark_returns." 201 | raise ValueError(message) 202 | 203 | 204 | def er_annual_return(returns, benchmark_returns, period="DAILY"): 205 | """ 206 | Description 207 | ---------- 208 | 计算年化超额收益率, 即(1+年化策略收益率)/(1+年化基准收益率)-1 209 | 210 | Parameters 211 | ---------- 212 | returns: array_like. 收益率序列 213 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 214 | 215 | Return 216 | ---------- 217 | float. 年化超额收益率.单位为% 218 | """ 219 | _check_period(period) 220 | returns = _convert_returns_type(returns) 221 | benchmark_returns = _convert_returns_type(benchmark_returns) 222 | _compare_length(returns, benchmark_returns) 223 | str_ann_ret = annualized_return(returns, period) / 100 224 | benchmark_ann_ret = annualized_return(benchmark_returns, period) / 100 225 | er_ann_ret = 100 * ((1 + str_ann_ret) / (1 + benchmark_ann_ret) - 1) 226 | return er_ann_ret 227 | 228 | 229 | def er_annual_volatility(returns, benchmark_returns, period="DAILY"): 230 | """ 231 | Description 232 | ---------- 233 | 计算超额收益的年化波动率 234 | 235 | Parameters 236 | ---------- 237 | returns: array_like. 收益率序列 238 | benchmark_returns: array_like. 基准收益率序列 239 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 240 | 241 | Return 242 | ---------- 243 | float. 超额收益的年化波动率.单位为% 244 | """ 245 | _check_period(period) 246 | returns = _convert_returns_type(returns) 247 | benchmark_returns = _convert_returns_type(benchmark_returns) 248 | _compare_length(returns, benchmark_returns) 249 | er_ret = returns - benchmark_returns 250 | er_ann_vol = annualized_volatility(er_ret, period) 251 | return er_ann_vol 252 | 253 | 254 | def information_ratio(returns, benchmark_returns, period="DAILY"): 255 | """ 256 | Description 257 | ---------- 258 | 计算信息比率, 即超额年化收益率/超额年化夏普比率 259 | 260 | Parameters 261 | ---------- 262 | returns: array_like. 收益率序列 263 | benchmark_returns: array_like. 基准收益率序列 264 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 265 | 266 | Return 267 | ---------- 268 | float. 信息比率 269 | """ 270 | _check_period(period) 271 | returns = _convert_returns_type(returns) 272 | benchmark_returns = _convert_returns_type(benchmark_returns) 273 | _compare_length(returns, benchmark_returns) 274 | er_ann_ret = er_annual_return(returns, benchmark_returns, period=period) 275 | er_ann_vol = er_annual_volatility(returns, benchmark_returns, period=period) 276 | if er_ann_vol < 1e-10: 277 | return 0 278 | return er_ann_ret / er_ann_vol 279 | 280 | 281 | def er_max_drawdown(returns, benchmark_returns): 282 | """ 283 | Description 284 | ---------- 285 | 计算超额收益的最大回撤 286 | 287 | Parameters 288 | ---------- 289 | returns: array_like. 收益率序列 290 | benchmark_returns: array_like. 基准收益率序列 291 | 292 | Return 293 | ---------- 294 | float. 超额收益的最大回撤.单位为% 295 | """ 296 | returns = _convert_returns_type(returns) 297 | benchmark_returns = _convert_returns_type(benchmark_returns) 298 | _compare_length(returns, benchmark_returns) 299 | # 计算策略和基准的净值,再计算超额收益的净值 300 | str_nv = np.cumprod(1 + returns) 301 | benchmark_nv = np.cumprod(1 + benchmark_returns) 302 | er_nv = str_nv / benchmark_nv 303 | # 历史最大净值 304 | er_prev_peaks = np.maximum.accumulate(er_nv) 305 | # 回撤 306 | er_dd = 100 * (er_nv / er_prev_peaks - 1) 307 | # 注意上述drawdown单位已为% 308 | er_mdd = np.min(er_dd) 309 | return er_mdd 310 | 311 | 312 | def winrate(returns, benchmark_returns): 313 | """ 314 | Description 315 | ---------- 316 | 计算策略相对于基准的胜率 317 | 318 | Parameters 319 | ---------- 320 | returns: array_like. 收益率序列 321 | benchmark_returns: array_like. 基准收益率序列 322 | 323 | Return 324 | ---------- 325 | float. 策略相对于基准的胜率.单位为% 326 | """ 327 | returns = _convert_returns_type(returns) 328 | benchmark_returns = _convert_returns_type(benchmark_returns) 329 | _compare_length(returns, benchmark_returns) 330 | er_ret = returns - benchmark_returns 331 | rate = 100 * np.sum(np.where(er_ret > 0, 1, 0)) / len(er_ret) 332 | return rate 333 | 334 | 335 | # 总结输出模块 336 | def get_backtest_result(returns, rf=0, benchmark_returns=None, period="DAILY"): 337 | """ 338 | Description 339 | ---------- 340 | 输出回测指标. 包含年化收益率、年化波动率、夏普比率、最大回撤等 341 | 342 | Parameters 343 | ---------- 344 | returns: array_like. 收益率序列 345 | rf: float. 无风险收益率, 单位为绝对值, 默认为0 346 | benchmark_returns: array_like. 基准收益率序列, 默认为None,即不计算相关指标 347 | period: str. 计算周期, 必须为DAILY, WEEKLY, MONTHLY中的一种。默认'DAILY' 348 | 349 | Return 350 | ---------- 351 | Dict. 输出格式为{ 352 | '年化收益率(%)': [ann_ret], 353 | '年化波动率(%)': [ann_vol], 354 | '夏普比率': [ann_sp], 355 | '最大回撤(%)': [mdd], 356 | } 357 | 若benchmark不为None,则会额外输出:超额年化收益率(%), 358 | 超额年化波动率(%). 信息比率, 相对基准胜率(%), 超额收益最大回撤(%) 359 | """ 360 | ann_ret = annualized_return(returns, period) 361 | ann_vol = annualized_volatility(returns, period) 362 | ann_sr = annualized_sharpe(returns, rf, period) 363 | mdd = max_drawdown(returns) 364 | dct = { 365 | "年化收益率(%)": [ann_ret], 366 | "年化波动率(%)": [ann_vol], 367 | "夏普比率": [ann_sr], 368 | "最大回撤(%)": [mdd], 369 | } 370 | er_dct = dict() 371 | if benchmark_returns is not None: 372 | er_ann_ret = er_annual_return(returns, benchmark_returns, period=period) 373 | er_ann_vol = er_annual_volatility(returns, benchmark_returns, period=period) 374 | str_ir = information_ratio(returns, benchmark_returns, period) 375 | str_winrate = winrate(returns, benchmark_returns) 376 | er_mdd = er_max_drawdown(returns, benchmark_returns) 377 | er_dct = { 378 | "超额年化收益率(%)": [er_ann_ret], 379 | "超额年化波动率(%)": [er_ann_vol], 380 | "信息比率": [str_ir], 381 | "相对基准胜率(%)": [str_winrate], 382 | "超额收益最大回撤(%)": [er_mdd], 383 | } 384 | dct.update(er_dct) 385 | return dct 386 | -------------------------------------------------------------------------------- /data.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dkl0707/factor_backtest/4f2b2dfdcf1c13e34f35caf1dba102c0ebd0a9d7/data.zip -------------------------------------------------------------------------------- /factor_analysis.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: dkl 3 | Description: 因子分析模块, 包含 4 | * 计算因子IC序列: get_factor_ic 5 | * Newywest-T计算: newy_west_test 6 | * 分析因子IC: analysis_factor_ic 7 | * 计算因子收益率经风险调整后的Alpha和t值: risk_adj_alpha 8 | * Fama-Macbeth回归: fama_macbeth_reg 9 | Date: 2023-07-05 19:12:16 10 | """ 11 | import numpy as np 12 | import pandas as pd 13 | import statsmodels.api as sm 14 | from linearmodels import FamaMacBeth 15 | import utils 16 | 17 | 18 | def get_factor_ic(factor_df, ret_df, factor_name): 19 | """ 20 | Description 21 | ---------- 22 | 计算因子IC序列 23 | 24 | Parameters 25 | ---------- 26 | factor_df: pandas.DataFrame. 未提前的因子数据. 27 | 28 | Return 29 | ---------- 30 | pandas.DataFrame. 31 | """ 32 | 33 | def calc_corr_func(df): 34 | return np.corrcoef(df[factor_name], df["ret"])[0, 1] 35 | 36 | prev_factor_df = utils.get_previous_factor(factor_df) 37 | df = pd.merge(prev_factor_df, ret_df, on=["trade_date", "stock_code"]) 38 | ic_df = df.groupby(["trade_date"], group_keys=False).apply(calc_corr_func) 39 | ic_df = ic_df.reset_index() 40 | ic_df.columns = ["trade_date", "IC"] 41 | return ic_df 42 | 43 | 44 | def newy_west_test(arr, factor_name="factor", max_lags=None): 45 | """ 46 | Description 47 | ---------- 48 | 计算收益率均值并输出Newywest-t统计量和p值 49 | 50 | Parameters 51 | ---------- 52 | arr: array_like. 收益率序列 53 | factor_name: str. 因子名称, 默认为'factor' 54 | max_lags: int. 滞后阶数. 默认为None, 即int(4*(T/100)**(2/9)) 55 | 56 | Return 57 | ---------- 58 | Dict. 59 | 输出示例为{"ret_mean(%)":10%, "t-value": 2.30, "p-value": 0.02, "p-star": **} 60 | """ 61 | arr = np.array(arr).reshape(-1) 62 | if max_lags is None: 63 | T = len(arr) 64 | max_lags = int(4 * (T / 100) ** (2 / 9)) 65 | mean_test = {"factor_name": [factor_name]} 66 | model = sm.OLS(arr, np.ones(len(arr))) 67 | result = model.fit(missing="drop", cov_type="HAC", cov_kwds={"maxlags": max_lags}) 68 | # 平均收益率 69 | ret_mean = list(result.params)[0] * 100 70 | # t值 71 | t_value = list(result.tvalues)[0] 72 | # p值 73 | p_value = list(result.pvalues)[0] 74 | # p值对应的星号 75 | p_star = _get_p_star(p_value) 76 | # 平均收益率 77 | mean_test["ret_mean(%)"] = [round(ret_mean, 3)] 78 | # t值 79 | mean_test["t-value"] = [round(t_value, 3)] 80 | # p值 81 | mean_test["p-value"] = [round(p_value, 3)] 82 | mean_test["p-star"] = [p_star] 83 | return mean_test 84 | 85 | 86 | def _get_p_star(x): 87 | dct = { 88 | "": [0.1, 1], 89 | "*": [0.05, 0.1], 90 | "**": [0.01, 0.05], 91 | "***": [-0.01, 0.01], 92 | } 93 | for key, value in dct.items(): 94 | lower, upper = value[0], value[1] 95 | if (lower < x) and (x <= upper): 96 | return key 97 | raise ValueError("x is invalid.") 98 | 99 | 100 | def analysis_factor_ic(factor_df, ret_df, factor_name): 101 | """ 102 | Description 103 | ---------- 104 | 分析因子IC 105 | 106 | Parameters 107 | ---------- 108 | factor_df: pandas.DataFrame. 109 | 未提前的因子数据,格式为trade_date, stock_code, factor_name 110 | ret_df: pandas.DataFrame. 111 | 收益率数据,格式为trade_date, stock_code, ret 112 | factor_name: str. 113 | 因子名称, 默认为'factor' 114 | 115 | Return 116 | ---------- 117 | tuple. 第一个为Dict,格式为dct = { 118 | "因子名称": [factor_name], 119 | "IC均值": [ic_mean], 120 | "IC标准差": [ic_std], 121 | "IR比率": [ir_ratio], 122 | "IC>0的比例(%)": [ic0_ratio], 123 | "IC>0.02的比例(%)": [ic002_ratio], 124 | } 125 | 第二个为因子的IC时序图和累计图 126 | """ 127 | ic_df = get_factor_ic(factor_df, ret_df, factor_name) 128 | ic_mean = ic_df["IC"].mean() 129 | ic_std = ic_df["IC"].std() 130 | ir_ratio = ic_mean / ic_std 131 | ic0_ratio = 100 * len(ic_df.loc[ic_df["IC"] > 0, :]) / len(ic_df) 132 | ic002_ratio = 100 * len(ic_df.loc[ic_df["IC"] > 0.02, :]) / len(ic_df) 133 | dct = { 134 | "因子名称": [factor_name], 135 | "IC均值": [ic_mean], 136 | "IC标准差": [ic_std], 137 | "IR比率": [ir_ratio], 138 | "IC>0的比例(%)": [ic0_ratio], 139 | "IC>0.02的比例(%)": [ic002_ratio], 140 | } 141 | ic_df["trade_date"] = pd.to_datetime(ic_df["trade_date"]) 142 | plot_params_dct = { 143 | "x1": ic_df["trade_date"], 144 | "y1": ic_df["IC"], 145 | "x2": ic_df["trade_date"], 146 | "y2": ic_df["IC"].cumsum(), 147 | "label1": "因子IC", 148 | "label2": "因子IC累计值", 149 | "xlabel": "日期", 150 | "ylabel1": "因子IC", 151 | "ylabel2": "因子IC累计值", 152 | "fig_title": f"因子{factor_name}的IC分析", 153 | } 154 | fig = utils.plot_bar_line(**plot_params_dct) 155 | return dct, fig 156 | 157 | 158 | def risk_adj_alpha(factor_ret, risk_factor_ret, max_lags=None): 159 | """ 160 | Description 161 | ---------- 162 | 计算因子收益率经风险调整后的Alpha和t值 163 | 164 | Parameters 165 | ---------- 166 | factor_ret: pandas.DataFrame. 待检测因子收益率序列 167 | risk_factor_ret: pandas.DataFrame. 风险因子收益率矩阵 168 | max_lags: int. 滞后阶数. 默认为None, 即int(4*(T/100)**(2/9)) 169 | 170 | Return 171 | ---------- 172 | tuple. 为(alpha, alphat) 173 | """ 174 | risk_factor_name_lst = risk_factor_ret.drop(columns=["trade_date"]).columns.tolist() 175 | factor_name = factor_ret.drop(columns=["trade_date"]).columns[0] 176 | df = pd.merge(factor_ret, risk_factor_ret, on="trade_date") 177 | risk_factor_ret = df[risk_factor_name_lst].values 178 | factor_ret = df[factor_name].values 179 | if max_lags is None: 180 | T = len(factor_ret) 181 | max_lags = int(4 * (T / 100) ** (2 / 9)) 182 | # 加入常数项,回归 183 | X = sm.add_constant(risk_factor_ret) 184 | model = sm.OLS(factor_ret, X) 185 | results = model.fit(missing="drop", cov_type="HAC", cov_kwds={"maxlags": max_lags}) 186 | # 获取系数 187 | coefficients = results.params 188 | # 风险调整的alpha 189 | alpha = coefficients[0] 190 | # 计算Newey-West调整的t值 191 | cov_matrix = results.cov_params() 192 | alphat = alpha / np.sqrt(np.diag(cov_matrix)[0]) 193 | return alpha, alphat 194 | 195 | 196 | def fama_macbeth_reg(ret, factor_df, factor_name_lst): 197 | """ 198 | Description 199 | ---------- 200 | Fama-Macbeth回归 201 | 返回平均观测值数量, 系数对应的Newey-West t值,估计参数和R-square 202 | 203 | Parameters 204 | ---------- 205 | ret: pandas.DataFrame. 206 | 股票收益率数据, 格式为trade_date, stock_code, ret 207 | factor_df: pandas.DataFrame. 208 | 因子数据, 格式为trade_date, stock_code, factor_name 209 | factor_name_lst: List[str]. 210 | 因子变量名列表。输入格式为列表[factor_name1, factor_name2, …, factor_namem] 211 | 212 | Return 213 | ---------- 214 | Dict. 输出示例为: { 215 | "factor_name": factor_name_lst, 216 | "beta": list(fama_macbeth.params[1:]), 217 | "t-value": list(fama_macbeth.tstats[1:]), 218 | "R-square": fama_macbeth.rsquared, 219 | "Average-Obs": fama_macbeth.time_info[0], 220 | } 221 | """ 222 | utils._check_columns(ret, ["trade_date", "stock_code", "ret"]) 223 | utils._check_columns(factor_df, ["trade_date", "stock_code"] + factor_name_lst) 224 | # 将因子数据提前一期 225 | prev_factor_df = utils.get_previous_factor(factor_df) 226 | # 合并数据 227 | regdf = pd.merge(ret, prev_factor_df, on=["trade_date", "stock_code"]) 228 | regdf["trade_date"] = pd.to_datetime(regdf["trade_date"]) 229 | T = len(list(set(regdf["trade_date"]))) 230 | regdf = regdf.sort_index(level=["stock_code", "trade_date"]) 231 | regdf = regdf.set_index(["stock_code", "trade_date"]) 232 | formula = "ret ~ 1 + " + " + ".join(factor_name_lst) 233 | model = FamaMacBeth.from_formula(formula, data=regdf) 234 | bandw = 4 * (T / 100) ** (2 / 9) 235 | fama_macbeth = model.fit(cov_type="kernel", debiased=False, bandwidth=bandw) 236 | res_dct = { 237 | "factor_name": factor_name_lst, 238 | "beta": list(fama_macbeth.params[1:]), 239 | "t-value": list(fama_macbeth.tstats[1:]), 240 | "R-square": fama_macbeth.rsquared, 241 | "Average-Obs": fama_macbeth.time_info[0], 242 | } 243 | return res_dct 244 | -------------------------------------------------------------------------------- /group_calc.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: dkl, ssj 3 | Description: 分组计算, 包含 4 | * 对股票分组: get_stock_group 5 | * 计算股票分组收益率: get_group_ret 6 | * 计算各组收益率的回测指标: get_group_ret_backtest 7 | * 单分组下的因子分析: analysis_group_ret 8 | * 获取序贯排序双分组情况: get_double_sort_group 9 | * 计算序贯排序双分组的收益率: get_doublesort_group_ret 10 | * 计算序贯排序双分组收益率均值: double_sort_mean 11 | * 计算序贯排序双分组的回测指标: double_sort_backtest 12 | Date: 2023-07-05 21:31:08 13 | """ 14 | import numpy as np 15 | import pandas as pd 16 | import factor_analysis 17 | import backtest 18 | import utils 19 | 20 | 21 | # ################################################ 22 | # 分组模块 23 | def get_stock_group(factor_df, factor_name, n_groups): 24 | """ 25 | Description 26 | ---------- 27 | 通过因子值,对股票分成n_groups组 28 | 组名从小到大为Group0到Group{n_groups-1} 29 | 30 | Parameters 31 | ---------- 32 | factor_df: pandas.DataFrame. 33 | 因子数据,格式为trade_date, stock_code, factor_name 34 | factor_name: str. 35 | 因子名称 36 | n_groups: int. 37 | 分组数量 38 | 39 | Return 40 | ---------- 41 | pandas.DataFrame. 42 | 格式为trade_date, stock_code, factor_name, factor_name_group 43 | """ 44 | col_lst = ["trade_date", "stock_code", factor_name] 45 | utils._check_sub_columns(factor_df, col_lst) 46 | factor_df = factor_df.copy() 47 | # 对factor_df中的factor_name进行分组,并进行标记 48 | g = factor_df.groupby("trade_date", group_keys=False) 49 | factor_df = g.apply( 50 | _get_single_period_group, factor_name=factor_name, n_groups=n_groups 51 | ) 52 | return factor_df 53 | 54 | 55 | def _get_single_period_group(df, factor_name, n_groups): 56 | # 将df中的factor_name进行分组 57 | # 组名从小到大为group0到group{n_groups-1} 58 | df = df.copy() 59 | group_name = factor_name + "_group" 60 | labels = ["Group" + str(i) for i in range(n_groups)] 61 | df[group_name] = pd.cut(df[factor_name].rank(), bins=n_groups, labels=labels) 62 | return df 63 | 64 | 65 | def get_group_ret(factor_df, ret_df, factor_name, n_groups, mktmv_df=None): 66 | """ 67 | Description 68 | ---------- 69 | 计算分组的收益率 70 | 71 | Parameters 72 | ---------- 73 | factor_df: pandas.DataFrame. 74 | 未提前的因子数据,格式为trade_date, stock_code, factor_name 75 | ret_df: pandas.DataFrame. 76 | 收益率数据,格式为trade_date, stock_code, ret 77 | factor_name: str. 78 | 因子名称 79 | n_groups: int. 80 | 分组数量. 81 | mktmv_df: pandas.DataFrame. 默认为None,即等权 82 | 市值数据.格式为trade_date, stock_code, mktmv 83 | 84 | Return 85 | ---------- 86 | pandas.DataFrame. 87 | 索引为trade_date, 列名为group0到group{n_group-1},和H-L 88 | """ 89 | factor_col_lst = ["trade_date", "stock_code", factor_name] 90 | ret_col_lst = ["trade_date", "stock_code", "ret"] 91 | utils._check_columns(factor_df, factor_col_lst) 92 | utils._check_columns(ret_df, ret_col_lst) 93 | # 对因子数据提前一期 94 | prev_factor_df = utils.get_previous_factor(factor_df) 95 | # 拼接 96 | df = pd.merge(prev_factor_df, ret_df, on=["trade_date", "stock_code"]) 97 | if mktmv_df is not None: 98 | utils._check_columns(mktmv_df, ["trade_date", "stock_code", "mktmv"]) 99 | mktmv_df = utils.get_previous_factor(mktmv_df) 100 | else: 101 | mktmv_df = df[["trade_date", "stock_code"]].copy() 102 | mktmv_df["mktmv"] = 1 103 | 104 | def get_group_weight_ret(df): 105 | df = df.copy() 106 | df["weight"] = df["mktmv"] / df["mktmv"].sum() 107 | return np.sum(df["weight"] * df["ret"]) 108 | 109 | # 计算分组 110 | # 此时, df格式为trade_date, stock_code, factor_name 111 | df = df.copy() 112 | df = get_stock_group(df, factor_name, n_groups) 113 | df = pd.merge(df, mktmv_df, on=["trade_date", "stock_code"]) 114 | # 此时, df格式为trade_date, stock_code, factor_name, mktmv 115 | # 分组计算收益率 116 | group_name = factor_name + "_group" 117 | g = df.groupby(["trade_date", group_name]) 118 | stacked_group_ret = g.apply(get_group_weight_ret) 119 | stacked_group_ret = stacked_group_ret.reset_index() 120 | stacked_group_ret.columns = ["trade_date", group_name, "ret"] 121 | # 反堆栈 122 | group_ret = utils.unstackdf(stacked_group_ret, code_name=group_name) 123 | # 计算多空收益率 124 | factor_ret = _get_factor_ret(group_ret, n_groups) 125 | group_ret["H-L"] = factor_ret 126 | return group_ret 127 | 128 | 129 | def _get_factor_ret(group_ret, n_groups): 130 | # 多空因子收益 131 | long_group_name = "Group" + str(n_groups - 1) 132 | short_group_name = "Group0" 133 | long_group_ret = group_ret[long_group_name] 134 | short_group_ret = group_ret[short_group_name] 135 | factor_ret = long_group_ret - short_group_ret 136 | return factor_ret 137 | 138 | 139 | def get_group_ret_backtest(group_ret, rf=0, benchmark=None, period="DAILY"): 140 | """ 141 | Description 142 | ---------- 143 | 计算各组收益率的回测指标 144 | 145 | Parameters 146 | ---------- 147 | group_ret: pandas.DataFrame. 148 | 各组收益率, 每列为各组收益率的时间序列 149 | rf: float. 150 | 无风险收益率, 默认为0 151 | benchmark: pandas. DataFrame. 152 | 基准收益率数据,格式为trade_date, ret 153 | period: str. 指定数据频率 154 | 有DAILY, WEEKLY, MONTHLY三种, 默认为DAILY 155 | 156 | Return 157 | ---------- 158 | pandas.DataFrame. 列名为分组名称 159 | 行名为年化收益率(%), 年化波动率(%), 夏普比率, 最大回撤(%) 160 | 若benchmark不为None,则会额外输出: 超额年化收益率(%), 161 | 超额年化波动率(%). 信息比率, 相对基准胜率(%), 超额收益最大回撤(%) 162 | """ 163 | if benchmark is not None: 164 | utils._check_columns(benchmark, ["trade_date", "ret"]) 165 | benchmark = benchmark.set_index("trade_date") 166 | s1 = set(benchmark.index.tolist()) 167 | s2 = set(group_ret.index.tolist()) 168 | common_time_lst = sorted(list(s1.intersection(s2))) 169 | group_ret = group_ret.loc[common_time_lst].copy() 170 | benchmark = benchmark.loc[common_time_lst].copy() 171 | benchmark_ret = benchmark["ret"] 172 | benchmark.columns = ["benchmark"] 173 | group_ret = pd.concat([group_ret, benchmark], axis=1) 174 | else: 175 | benchmark_ret = None 176 | backtest_df = pd.DataFrame() 177 | for col in group_ret.columns: 178 | res_dct = backtest.get_backtest_result( 179 | group_ret[col], rf=rf, benchmark_returns=benchmark_ret, period=period 180 | ) 181 | res_df = pd.DataFrame(res_dct).T 182 | backtest_df = pd.concat([backtest_df, res_df], axis=1) 183 | backtest_df.columns = group_ret.columns 184 | return backtest_df 185 | 186 | 187 | def analysis_group_ret( 188 | factor_df, 189 | ret_df, 190 | factor_name, 191 | n_groups, 192 | mktmv_df=None, 193 | rf=0, 194 | benchmark=None, 195 | period="DAILY", 196 | ): 197 | ''' 198 | Description 199 |    ---------- 200 | 单分组下的因子分析 201 | 202 | Parameters 203 |    ---------- 204 | factor_df: pandas.DataFrame. 205 | 未提前的因子数据,格式为trade_date, stock_code, factor_name 206 | ret_df: pandas.DataFrame. 207 | 收益率数据,格式为trade_date, stock_code, ret 208 | factor_name: str. 209 | 因子名称 210 | n_groups: int. 211 | 分组数量. 212 | mktmv_df: pandas.DataFrame. 213 | 市值数据.格式为trade_date, stock_code, mktmv. 默认为None,即等权 214 | rf: float. 215 | 无风险收益率, 默认为0 216 | benchmark: pandas. DataFrame. 217 | 基准收益率数据,格式为trade_date, ret 218 | period: str. 指定数据频率 219 | 有DAILY, WEEKLY, MONTHLY三种, 默认为DAILY 220 | 221 | Return 222 |    ---------- 223 | tuple. 第一个为分组回测指标,格式为pandas.DataFrame 224 | 第二个为分组净值曲线图,第三个为因子多空净值曲线图 225 | ''' 226 | group_ret = get_group_ret(factor_df, ret_df, factor_name, n_groups, mktmv_df) 227 | # 回测指标计算 228 | backtest_df = get_group_ret_backtest(group_ret, rf, benchmark, period) 229 | time_idx = pd.to_datetime(group_ret.index) 230 | factor_ret = group_ret["H-L"] 231 | weight_name = "等权" if mktmv_df is None else "市值加权" 232 | # 分组净值曲线 233 | group_ret = group_ret.drop(columns=["H-L"]) 234 | plot_params_dct1 = { 235 | "x_lst": [time_idx] * n_groups, 236 | "y_lst": [(group_ret[col] + 1).cumprod() for col in group_ret.columns], 237 | "label_lst": group_ret.columns.tolist(), 238 | "xlabel": "日期", 239 | "ylabel": "净值", 240 | "fig_title": f"因子{factor_name}的分组净值曲线({weight_name})", 241 | } 242 | fig1 = utils.plot_multi_line(**plot_params_dct1) 243 | 244 | # 因子多空净值曲线 245 | factor_cumret = (1 + factor_ret).cumprod() - 1 246 | plot_params_dct2 = { 247 | "x1": time_idx, 248 | "y1": 100 * np.array(factor_ret).reshape(-1), 249 | "x2": time_idx, 250 | "y2": 100 * np.array(factor_cumret).reshape(-1), 251 | "label1": "因子多空收益率(%)", 252 | "label2": "因子累计多空收益率(%)", 253 | "xlabel": "日期", 254 | "ylabel1": "因子多空收益率(%)", 255 | "ylabel2": "因子累计收益率(%)", 256 | "fig_title": f"因子{factor_name}的多空收益曲线图({weight_name})", 257 | } 258 | fig2 = utils.plot_bar_line(**plot_params_dct2) 259 | return backtest_df, fig1, fig2 260 | 261 | 262 | # 序贯排序双分组部分 263 | def get_double_sort_group( 264 | factor1_df, 265 | factor2_df, 266 | factor1_name, 267 | factor2_name, 268 | n_groups1, 269 | n_groups2, 270 | ): 271 | """ 272 | Description 273 | ---------- 274 | 获取序贯排序双分组情况 275 | 276 | Parameters 277 | ---------- 278 | factor1_df: pandas.DataFrame. 279 | 未提前的因子数据,格式为trade_date, stock_code, factor1_name 280 | factor2_df: pandas.DataFrame. 281 | 未提前的因子数据,格式为trade_date, stock_code, factor2_name 282 | factor1_name: str. 283 | 因子1名称 284 | factor2_name: str. 285 | 因子2名称 286 | n_groups1: int. 287 | 对因子1分组数量. 288 | n_groups2: int. 289 | 对因子2分组数量. 290 | 291 | Return 292 | ---------- 293 | pandas.DataFrame. 294 | 列名为trade_date, stock_code, factor1_name, factor2_name, group1_name, group2_name, ret 295 | """ 296 | utils._check_columns(factor1_df, ["trade_date", "stock_code", factor1_name]) 297 | utils._check_columns(factor2_df, ["trade_date", "stock_code", factor2_name]) 298 | factor_df = pd.merge(factor1_df, factor2_df, on=["trade_date", "stock_code"]) 299 | # 对factor_df中的factor_name进行分组,并进行标记 300 | g1 = factor_df.groupby("trade_date", group_keys=False) 301 | factor_df = g1.apply( 302 | _get_single_period_group, factor_name=factor1_name, n_groups=n_groups1 303 | ) 304 | # 再按照trade_date, factor1_group进行分组 305 | g2 = factor_df.groupby("trade_date", group_keys=False) 306 | factor_df = g2.apply( 307 | _get_single_period_group, factor_name=factor2_name, n_groups=n_groups2 308 | ) 309 | return factor_df 310 | 311 | 312 | def get_double_sort_group_ret( 313 | factor1_df, 314 | factor2_df, 315 | ret_df, 316 | factor1_name, 317 | factor2_name, 318 | n_groups1, 319 | n_groups2, 320 | mktmv_df=None, 321 | ): 322 | """ 323 | Description 324 | ---------- 325 | 计算序贯排序双分组的收益率 326 | 327 | Parameters 328 | ---------- 329 | factor1_df: pandas.DataFrame. 330 | 未提前的因子数据,格式为trade_date, stock_code, factor1_name 331 | factor2_df: pandas.DataFrame. 332 | 未提前的因子数据,格式为trade_date, stock_code, factor2_name 333 | ret_df: pandas.DataFrame. 334 | 收益率数据,格式为trade_date, stock_code, ret 335 | factor1_name: str. 336 | 因子1名称 337 | factor2_name: str. 338 | 因子2名称 339 | n_groups1: int. 340 | 对因子1分组数量. 341 | n_groups2: int. 342 | 对因子2分组数量. 343 | mktmv_df: pandas.DataFrame. 默认为None,即等权 344 | 未提前的市值数据.格式为trade_date, stock_code, mktmv 345 | 346 | Return 347 | ---------- 348 | pandas.DataFrame. 349 | 列名为trade_date, group1_name, group2_name, ret 350 | """ 351 | utils._check_columns(ret_df, ["trade_date", "stock_code", "ret"]) 352 | # 获取双分组结果,并提前一期 353 | factor_df = get_double_sort_group( 354 | factor1_df, 355 | factor2_df, 356 | factor1_name, 357 | factor2_name, 358 | n_groups1, 359 | n_groups2, 360 | ) 361 | factor_df = utils.get_previous_factor(factor_df) 362 | 363 | # 计算分组收益 364 | df = pd.merge(factor_df, ret_df, on=["trade_date", "stock_code"]) 365 | if mktmv_df is not None: 366 | utils._check_columns(mktmv_df, ["trade_date", "stock_code", "mktmv"]) 367 | mktmv_df = utils.get_previous_factor(mktmv_df) 368 | else: 369 | mktmv_df = df[["trade_date", "stock_code"]].copy() 370 | mktmv_df["mktmv"] = 1 371 | 372 | def get_group_weight_ret(df): 373 | df = df.copy() 374 | df["weight"] = df["mktmv"] / df["mktmv"].sum() 375 | return np.sum(df["weight"] * df["ret"]) 376 | 377 | # 计算分组 378 | # 此时, df格式为trade_date, stock_code, factor_name 379 | df = pd.merge(df, mktmv_df, on=["trade_date", "stock_code"]) 380 | # 此时, df格式为trade_date, stock_code, factor_name, mktmv 381 | 382 | # 分组计算收益率 383 | group1_name = factor1_name + "_group" 384 | group2_name = factor2_name + "_group" 385 | g = df.groupby(["trade_date", group1_name, group2_name]) 386 | group_ret = g.apply(get_group_weight_ret) 387 | group_ret = group_ret.reset_index() 388 | group_ret.columns = ["trade_date", group1_name, group2_name, "ret"] 389 | 390 | # 计算多空收益率 391 | for i in range(n_groups1): 392 | group1_idx = "Group" + str(i) 393 | factor_ret = pd.DataFrame() 394 | factor_ret["trade_date"] = sorted(list(set(group_ret["trade_date"]))) 395 | factor_ret[group1_name] = group1_idx 396 | factor_ret[group2_name] = "H-L" 397 | long_group2_idx = "Group" + str(n_groups2 - 1) 398 | short_group2_idx = "Group0" 399 | cond1 = group_ret[group1_name] == group1_idx 400 | cond2_long = group_ret[group2_name] == long_group2_idx 401 | cond2_short = group_ret[group2_name] == short_group2_idx 402 | long_ret = group_ret.loc[cond1 & cond2_long, "ret"].values 403 | short_ret = group_ret.loc[cond1 & cond2_short, "ret"].values 404 | factor_ret["ret"] = long_ret - short_ret 405 | group_ret = pd.concat([group_ret, factor_ret]) 406 | group_ret = group_ret.sort_values(["trade_date", group1_name, group2_name]) 407 | group_ret = group_ret.reset_index(drop=True) 408 | return group_ret 409 | 410 | 411 | def double_sort_mean(group_ret, factor1_name, factor2_name): 412 | """ 413 | Description 414 | ---------- 415 | 计算序贯排序双分组收益率均值 416 | 417 | Parameters 418 | ---------- 419 | group_ret: pandas.DataFrame. 420 | 各组收益率, 格式为trade_date, group1_name, group2_name, ret 421 | factor1_name: str. 422 | 因子1名称 423 | factor2_name: str. 424 | 因子2名称 425 | 426 | Return 427 | ---------- 428 | pandas.DataFrame. 429 | 列名为Group0, Group1, …, Groupm, H-L 430 | 索引第一层为Group0,…,Groupn 431 | 第二层为ret_mean(%), tvalue 432 | """ 433 | group1_name = factor1_name + "_group" 434 | group2_name = factor2_name + "_group" 435 | n_groups1 = len(group_ret.drop_duplicates([group1_name])) 436 | n_groups2 = len(group_ret.drop_duplicates([group2_name])) - 1 437 | ret_mean_arr = np.zeros((n_groups1, n_groups2 + 1)) 438 | ret_t_arr = np.zeros((n_groups1, n_groups2 + 1)) 439 | for i in range(n_groups1): 440 | for j in range(n_groups2 + 1): 441 | cond1 = group_ret[group1_name] == "Group" + str(i) 442 | if j < n_groups2: 443 | cond2 = group_ret[group2_name] == "Group" + str(j) 444 | else: 445 | cond2 = group_ret[group2_name] == "H-L" 446 | ret_arr = group_ret.loc[cond1 & cond2, "ret"].values 447 | test_dct = factor_analysis.newy_west_test(ret_arr) 448 | ret_mean_arr[i, j] = test_dct["ret_mean(%)"][0] 449 | ret_t_arr[i, j] = test_dct["t-value"][0] 450 | 451 | # 将t值放到均值下面调整输出结果 452 | res_arr = np.zeros((2 * n_groups1, n_groups2 + 1)) 453 | for i in range(n_groups1): 454 | mean_idx = 2 * i 455 | t_idx = 2 * i - 1 456 | res_arr[mean_idx, :] = ret_mean_arr[i, :] 457 | res_arr[t_idx, :] = ret_t_arr[i, :] 458 | # 设置列名 459 | col_lst = ["Group" + str(i) for i in range(n_groups2)] + ["H-L"] 460 | # 建立索引 461 | idx1_lst = ["Group" + str(i) for i in range(n_groups1)] 462 | idx2_lst = ["ret_mean(%)", "t-value"] 463 | res_idx = pd.MultiIndex.from_product([idx1_lst, idx2_lst]) 464 | res_df = pd.DataFrame(res_arr, columns=col_lst, index=res_idx) 465 | return res_df 466 | 467 | 468 | def double_sort_backtest( 469 | group_ret, factor1_name, factor2_name, rf=0, benchmark=None, period="DAILY" 470 | ): 471 | """ 472 | Description 473 | ---------- 474 | 计算序贯排序双分组的回测指标 475 | 476 | Parameters 477 | ---------- 478 | group_ret: pandas.DataFrame. 479 | 各组收益率, 格式为trade_date, group1_name, group2_name, ret 480 | factor1_name: str. 481 | 因子1名称 482 | factor2_name: str. 483 | 因子2名称 484 | rf: float. 485 | 无风险收益率, 默认为0 486 | benchmark: pandas. DataFrame. 487 | 基准收益率数据,格式为trade_date, ret 488 | period: str. 指定数据频率 489 | 有DAILY, WEEKLY, MONTHLY三种, 默认为DAILY 490 | 491 | Return 492 | ---------- 493 | pandas.DataFrame. 494 | 列名为Group0, …, Groupm, H-L 495 | 索引第一层为Group0, …, Groupn, 496 | 第二层为年化收益率(%), 年化波动率(%), 夏普比率, 最大回撤(%) 497 | 若benchmark不为None,则第二层会额外输出:超额年化收益率(%), 498 | 超额年化波动率(%). 信息比率, 相对基准胜率(%), 超额收益最大回撤(%) 499 | """ 500 | group1_name = factor1_name + "_group" 501 | group2_name = factor2_name + "_group" 502 | n_groups1 = len(group_ret.drop_duplicates([group1_name])) 503 | n_groups2 = len(group_ret.drop_duplicates([group2_name])) - 1 504 | if benchmark is not None: 505 | utils._check_columns(benchmark, ["trade_date", "ret"]) 506 | # 选出共同的trade_date 507 | s1 = set(benchmark["trade_date"].tolist()) 508 | s2 = set(group_ret["trade_date"].tolist()) 509 | common_time_lst = sorted(list(s1.intersection(s2))) 510 | cond1 = group_ret["trade_date"].isin(common_time_lst) 511 | cond2 = benchmark["trade_date"].isin(common_time_lst) 512 | group_ret = group_ret.loc[cond1, :].copy() 513 | benchmark = benchmark.loc[cond2, :].copy() 514 | # 排序重设索引 515 | group_ret = group_ret.sort_values(["trade_date", group1_name, group2_name]) 516 | benchmark = benchmark.sort_values("trade_date") 517 | group_ret = group_ret.reset_index(drop=True) 518 | benchmark = benchmark.reset_index(drop=True) 519 | benchmark_ret = benchmark["ret"] 520 | ind_n = 9 521 | else: 522 | benchmark_ret = None 523 | ind_n = 4 524 | 525 | res_arr = np.zeros((n_groups1 * ind_n, n_groups2 + 1)) 526 | for i in range(n_groups1): 527 | for j in range(n_groups2 + 1): 528 | group1_idx = "Group" + str(i) 529 | if j == n_groups2: 530 | group2_idx = "H-L" 531 | else: 532 | group2_idx = "Group" + str(j) 533 | cond1 = group_ret[group1_name] == group1_idx 534 | cond2 = group_ret[group2_name] == group2_idx 535 | ret_arr = group_ret.loc[cond1 & cond2, "ret"].values 536 | temp_res_dct = backtest.get_backtest_result( 537 | ret_arr, rf=rf, benchmark_returns=benchmark_ret, period=period 538 | ) 539 | temp_res_arr = np.array(list(temp_res_dct.values())).reshape(-1) 540 | res_arr[ind_n * i : ind_n * (i + 1), j] = temp_res_arr 541 | 542 | # 序贯排序回测结果整理 543 | col_lst = ["Group" + str(i) for i in range(n_groups2)] + ["H-L"] 544 | idx1_lst = ["Group" + str(i) for i in range(n_groups1)] 545 | idx2_lst = list(temp_res_dct.keys()) 546 | res_idx = pd.MultiIndex.from_product([idx1_lst, idx2_lst]) 547 | res_df = pd.DataFrame(res_arr, columns=col_lst, index=res_idx) 548 | return res_df 549 | -------------------------------------------------------------------------------- /preprocess.py: -------------------------------------------------------------------------------- 1 | """ 2 | Author: dkl, lhx 3 | Description: 因子预处理代码,包含: 4 | * 去极值: del_outlier 5 | * 标准化: standardize 6 | * 中性化: neutralize 7 | Date: 2023-07-07 11:45:56 8 | """ 9 | import pandas as pd 10 | from sklearn.linear_model import LinearRegression 11 | import utils 12 | 13 | 14 | # 去极值 15 | def del_outlier(factor_df, factor_name, method="mad", n=3): 16 | """ 17 | Description 18 | ---------- 19 | 对每期因子进行去极值 20 | 21 | Parameters 22 | ---------- 23 | factor_df: pandas.DataFrame. 因子数据,格式为trade_date,stock_code,factor 24 | factor_name: str. 因子名称 25 | method: str. 去极值方式,为'mad'或'sigma',默认为mad 26 | n: float.去极值的n值.默认取值为3 27 | 28 | Return 29 | ---------- 30 | pandas.DataFrame. 31 | 去极值后的因子数据, 格式为trade_date,stock_code,factor 32 | """ 33 | utils._check_sub_columns(factor_df, [factor_name]) 34 | factor_df = factor_df.copy() 35 | if method == "mad": 36 | g = factor_df.groupby("trade_date", group_keys=False) 37 | factor_df = g.apply(_single_mad_del, factor_name, n) 38 | elif method == "sigma": 39 | g = factor_df.groupby("trade_date", group_keys=False) 40 | factor_df = g.apply(_single_sigma_del, factor_name, n) 41 | if method not in ["mad", "sigma"]: 42 | raise ValueError("method must be mad or sigma") 43 | return factor_df 44 | 45 | 46 | def _single_mad_del(factor_df, factor_name, n): 47 | """ 48 | Description 49 | ---------- 50 | 单期MAD法去极值 51 | 52 | Parameters 53 | ---------- 54 | factor_df: pandas.DataFrame. 因子值数据 55 | factor_name: str.因子名称 56 | n: float. 去极值的n 57 | 58 | Return 59 | ---------- 60 | 去极值后的因子数据 61 | """ 62 | # 找出当期factor和factor_median的偏差bias_sr 63 | factor_median = factor_df[factor_name].median() 64 | bias_sr = abs(factor_df[factor_name] - factor_median) 65 | # 找到bias_sr的中位数new_median 66 | new_median = bias_sr.median() 67 | # 找到上下界 68 | dt_up = factor_median + n * new_median 69 | dt_down = factor_median - n * new_median 70 | 71 | # 超出上下界的值,赋值为上下界 72 | factor_df[factor_name] = factor_df[factor_name].clip(dt_down, dt_up, axis=0) 73 | return factor_df 74 | 75 | 76 | def _single_sigma_del(factor_df, factor_name, n): 77 | """ 78 | Description 79 | ---------- 80 | 单期Sigma法去极值 81 | 82 | Parameters 83 | ---------- 84 | factor_df: pandas.DataFrame. 因子值数据 85 | factor_name: str. 因子名称 86 | n: float. 去极值的n 87 | 88 | Return 89 | ---------- 90 | 去极值后的因子数据 91 | """ 92 | factor_mean = factor_df[factor_name].mean() 93 | factor_std = factor_df[factor_name].std() 94 | dt_up = factor_mean + n * factor_std 95 | dt_down = factor_mean - n * factor_std 96 | factor_df[factor_name] = factor_df[factor_name].clip( 97 | dt_down, dt_up, axis=0 98 | ) # 超出上下限的值,赋值为上下限 99 | return factor_df 100 | 101 | 102 | # 标准化代码 103 | def standardize(factor_df, factor_name, method="rank"): 104 | """ 105 | Description 106 | ---------- 107 | 标准化 108 | 109 | Parameters 110 | ---------- 111 | factor: pandas.DataFrame,因子值,格式为trade_date,stock_code,factor 112 | factor_name: str.因子名称 113 | method: str.中性化方式,可选为'rank'(排序标准化)或者'zscore'(Z-score标准化),默认为rank 114 | 115 | Return 116 | ---------- 117 | pandas.DataFrame. 118 | 标准化后的因子数据, 格式为trade_date,stock_code,factor 119 | """ 120 | utils._check_sub_columns(factor_df, [factor_name]) 121 | if method == "zscore": 122 | g = factor_df.groupby("trade_date", group_keys=False) 123 | factor_df = g.apply(_single_zscore_standardize, factor_name) 124 | elif method == "rank": 125 | g = factor_df.groupby("trade_date", group_keys=False) 126 | factor_df = g.apply(_single_rank_standardize, factor_name) 127 | else: 128 | raise ValueError("method must be rank or zscore") 129 | return factor_df 130 | 131 | 132 | def _single_rank_standardize(factor_df, factor_name): 133 | """ 134 | Description 135 | ---------- 136 | 单期因子数据排序标准化 137 | 138 | Parameters 139 | ---------- 140 | factor: pandas.DataFrame,因子值,格式为trade_date,stock_code,factor 141 | factor_name: str.因子名称 142 | 143 | Return 144 | ---------- 145 | pandas.DataFrame.排序标准化后的因子数据 146 | """ 147 | factor_df[factor_name] = factor_df[factor_name].rank() 148 | return _single_zscore_standardize(factor_df, factor_name) 149 | 150 | 151 | def _single_zscore_standardize(factor_df, factor_name): 152 | """ 153 | Description 154 | ---------- 155 | 单期因子数据zscore标准化 156 | 157 | Parameters 158 | ---------- 159 | factor: pandas.DataFrame,因子值,格式为trade_date,stock_code,factor 160 | factor_name: str.因子名称 161 | 162 | Return 163 | ---------- 164 | pandas.DataFrame.zscore标准化后的因子数据 165 | """ 166 | factor_mean = factor_df[factor_name].mean() 167 | factor_std = factor_df[factor_name].std() 168 | factor_df[factor_name] = (factor_df[factor_name] - factor_mean) / factor_std 169 | return factor_df 170 | 171 | 172 | # 中性化代码 173 | def neutralize(factor_df, factor_name, mktmv_df=None, industry_df=None): 174 | """ 175 | Description 176 | ---------- 177 | 中性化 178 | 179 | Parameters 180 | ---------- 181 | factor_df: pandas.DataFrame. 182 | 因子值, 格式为trade_date,stock_code,factor 183 | mktmv_df: pandas.DataFrame. 184 | 股票流通市值,格式为trade_date,stock_code,mktmv. 185 | 默认为None即不进行市值中性化 186 | industry_df: pandas.DataFrame, 股票所属行业, 格式为trade_date,stock_code,ind_code.默认为None即不进行行业中性化 187 | 188 | Return 189 | ---------- 190 | pandas.DataFrame. 191 | 中性化后的因子数据, 格式为trade_date,stock_code,factor 192 | """ 193 | neu_factor = factor_df.copy() 194 | if mktmv_df is not None: 195 | neu_factor = mktmv_neutralize(neu_factor, factor_name, mktmv_df) 196 | if industry_df is not None: 197 | neu_factor = ind_neutralize(neu_factor, factor_name, industry_df) 198 | return neu_factor 199 | 200 | 201 | # 市值中性化 202 | def mktmv_neutralize(factor_df, factor_name, mktmv_df): 203 | """ 204 | Description 205 | ---------- 206 | 市值中性化 207 | 208 | Parameters 209 | ---------- 210 | factor_df: pandas.DataFrame, 格式为trade_date, stock_code, factor 211 | factor_name: str.因子名称 212 | mktmv_df: pandas.DataFrame,股票流通市值,格式为trade_date,stock_code,mktmv. 213 | 214 | Return 215 | ---------- 216 | pandas.DataFrame.中性化后的因子值 217 | """ 218 | # 检查输入数据 219 | utils._check_sub_columns(mktmv_df, ["mktmv"]) 220 | utils._check_sub_columns(factor_df, [factor_name]) 221 | # 合并两个数据,groupby做回归 222 | df = pd.merge(factor_df, mktmv_df, on=["trade_date", "stock_code"]) 223 | g = df.groupby("trade_date", group_keys=False) 224 | df = g.apply(_mktmv_reg, factor_name) 225 | df = df.drop(columns=["mktmv"]) 226 | return df 227 | 228 | 229 | def _mktmv_reg(df, factor_name): 230 | """ 231 | Description 232 | ---------- 233 | 对单期因子进行市值中性化 234 | 235 | Parameters 236 | ---------- 237 | df:pandas.DataFrame, 格式为trade_date, stock_code, factor, mktmv 238 | factor_name: str.因子名称 239 | 240 | Return 241 | ---------- 242 | pandas.DataFrame.中性化后的因子值 243 | """ 244 | x = df["mktmv"].values.reshape(-1, 1) 245 | y = df[factor_name] 246 | lr = LinearRegression() 247 | lr.fit(x, y) # 拟合 248 | y_predict = lr.predict(x) # 预测 249 | df[factor_name] = y - y_predict 250 | return df 251 | 252 | 253 | # 行业中性化 254 | def ind_neutralize(factor_df, factor_name, industry_df): 255 | """ 256 | Description 257 | ---------- 258 | 对每期因子进行行业中性化 259 | 方法: 先用pd.get_dummies生成行业虚拟变量, 然后用带截距项回归得到残差作为因子 260 | 261 | Parameters 262 | ---------- 263 | factor_df: pandas.DataFrame,因子值,格式为trade_date,stock_code,factor 264 | factor_name: str. 因子名称 265 | industry_df: pandas.DataFrame, 股票所属行业, 格式为trade_date,stock_code,ind_code 266 | 267 | Return 268 | ---------- 269 | pandas.DataFrame.行业中性化后的因子数据 270 | """ 271 | # 检查输入数据 272 | utils._check_sub_columns(factor_df, [factor_name]) 273 | utils._check_sub_columns(industry_df, ["ind_code"]) 274 | # 生成虚拟变量,拼接形成新的df 275 | ind_dummies = pd.get_dummies(industry_df["ind_code"], drop_first=True, prefix="ind") 276 | # 格式为 trade_date,stock_code,dummies_ind_code 277 | ind_new = pd.concat([industry_df.drop(columns=["ind_code"]), ind_dummies], axis=1) 278 | # 拼接两个表格 279 | df = pd.merge(factor_df, ind_new, on=["trade_date", "stock_code"]) 280 | g = df.groupby("trade_date", group_keys=False) 281 | df = g.apply(_single_ind_neutralize, factor_name) 282 | df = df[["trade_date", "stock_code", factor_name]].copy() 283 | return df 284 | 285 | 286 | def _single_ind_neutralize(df, factor_name): 287 | """ 288 | Description 289 | ---------- 290 | 对单期因子进行行业中性化 291 | 292 | Parameters 293 | ---------- 294 | df: pandas.DataFrame, 因子值和行业的df, 格式为trade_date,stock_code,'factor_name',dummy_ind_code 295 | factor_name: str. 因子名称 296 | 297 | Return 298 | ---------- 299 | pandas.DataFrame.行业中性化后的因子数据 300 | """ 301 | x = df.iloc[:, 3:] 302 | y = df[factor_name] 303 | # 计算回归残差 304 | lr = LinearRegression() 305 | lr.fit(x, y) 306 | y_predict = lr.predict(x) 307 | df[factor_name] = y - y_predict 308 | return df 309 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | linearmodels==5.1 2 | matplotlib==3.7.1 3 | numpy==1.24.2 4 | pandas==1.3.0 5 | scikit_learn==1.3.0 6 | statsmodels==0.14.0 7 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Author: dkl 3 | Description: 常用的辅助函数, 包含: 4 | * 获取上期因子值: get_previous_factor 5 | * 数据堆栈: stackdf 6 | * 数据反堆栈: unstackdf 7 | * 获取交易日历中历史最近的日期: get_last_date 8 | * 获取交易日历中未来最近的日期: get_next_date 9 | Date: 2022-10-04 09:57:32 10 | ''' 11 | import numpy as np 12 | import pandas as pd 13 | import matplotlib.pyplot as plt 14 | plt.rcParams['font.sans-serif'] = ['SimHei'] 15 | plt.rcParams['axes.unicode_minus'] = False 16 | 17 | 18 | # 因子处理部分 19 | def get_previous_factor(factor_df): 20 | ''' 21 | Description 22 |    ---------- 23 | 获取上期因子值 24 | 25 | Parameters 26 |    ---------- 27 | factor_df: pandas.DataFrame. 输入因子数据,必须含有trade_date 28 | 29 | Return 30 |    ---------- 31 | pandas.DataFrame. 上期因子值 32 | ''' 33 | _check_sub_columns(factor_df, ['trade_date']) 34 | factor_df = factor_df.copy() 35 | # 将日期往前挪一期,建立本期交易日和上期交易日的映射DataFrame 36 | this_td_lst = factor_df['trade_date'].drop_duplicates().tolist() 37 | last_td_lst = [np.nan] + this_td_lst[:-1] 38 | td_df = pd.DataFrame({ 39 | 'this_trade_date': this_td_lst, 40 | 'last_trade_date': last_td_lst 41 | }) 42 | # 与原来的收益率数据进行合并 43 | factor_df = pd.merge(td_df, 44 | factor_df, 45 | left_on='last_trade_date', 46 | right_on='trade_date') 47 | # 将上期交易日修改为本期交易日,这样每个交易日对应的是下期的收益率 48 | factor_df = factor_df.drop(columns=['last_trade_date', 'trade_date']) 49 | factor_df = factor_df.rename(columns={'this_trade_date': 'trade_date'}) 50 | # 去除空值 51 | factor_df = factor_df.dropna().reset_index(drop=True) 52 | return factor_df 53 | 54 | 55 | # 堆栈和反堆栈部分 56 | def stackdf(df, var_name, date_name='trade_date', code_name='stock_code'): 57 | ''' 58 | Description 59 |    ---------- 60 | 对输入数据进行堆栈,每行为截面数据,每列为时间序列数据 61 | 62 | Parameters 63 |    ---------- 64 | df: pandas.DataFrame. 65 | 输出数据为堆栈后的数据 66 | date_name: str. 日期名称, 默认为trade_date 67 | code_name: str. 代码名称, 默认为stock_code 68 | 69 | Return 70 |    ---------- 71 | pandas.DataFrame. 72 | 堆栈后的数据,列为trade_date, stock_code和var_name 73 | ''' 74 | df = df.copy() 75 | df = df.stack().reset_index() 76 | df.columns = [date_name, code_name, var_name] 77 | return df 78 | 79 | 80 | def unstackdf(df, date_name='trade_date', code_name='stock_code'): 81 | ''' 82 | Description 83 |    ---------- 84 | 反堆栈函数 85 | 86 | Parameters 87 |    ---------- 88 | df: pandas.DataFrame. 89 | 输入列必须为三列且必须有date_name和code_name 90 | date_name: str. 日期名称, 默认为trade_date 91 | code_name: str. 代码名称, 默认为stock_code 92 | 93 | Return 94 |    ---------- 95 | pandas.DataFrame. 反堆栈后的数据 96 | ''' 97 | _check_sub_columns(df, [date_name, code_name]) 98 | if not (len(df.columns) == 3): 99 | error_message = 'length of df.columns must be 3' 100 | raise ValueError(error_message) 101 | df = df.copy() 102 | df = df.set_index([date_name, code_name]).unstack() 103 | df.columns = df.columns.get_level_values(1).tolist() 104 | df.index = df.index.tolist() 105 | return df 106 | 107 | 108 | # 检查df的列的部分 109 | def _check_sub_columns(df, var_lst): 110 | ''' 111 | Description 112 |    ---------- 113 | 检查var_lst是否是df.columns的列的子集(不考虑排序) 114 | 115 | Parameters 116 |    ---------- 117 | df: pandas.DataFrame. 输入数据 118 | var_lst: List[str]. 变量名列表 119 | 120 | Return 121 |    ---------- 122 | Bool 123 | ''' 124 | if not set(var_lst).issubset(df.columns): 125 | var_name = ','.join(var_lst) 126 | raise ValueError(f'{var_name} must be in the columns of df.') 127 | 128 | 129 | def _check_columns(df, var_lst): 130 | ''' 131 | Description 132 |    ---------- 133 | 检查var_lst是否是df.columns的列(不考虑顺序) 134 | 135 | Parameters 136 |    ---------- 137 | df: pandas.DataFrame. 输入数据 138 | var_lst: List[str]. 变量名列表 139 | 140 | Return 141 |    ---------- 142 | Bool 143 | ''' 144 | lst1 = list(var_lst) 145 | lst2 = df.columns.tolist() 146 | if not sorted(lst1) == sorted(lst2): 147 | var_str = ', '.join(var_lst) 148 | err = 'The columns of df must be var_lst:{}'.format(var_str) 149 | raise ValueError(err) 150 | 151 | 152 | # 日期部分 153 | def get_last_date(date, trade_date_lst): 154 | ''' 155 | Description 156 |    ---------- 157 | 获取交易日历中历史最近的日期 158 | 159 | Parameters 160 |    ---------- 161 | date: str. 所选日期 162 | trade_date_lst: List[str]. 交易日历列表 163 | 164 | Return 165 |    ---------- 166 | str. 交易日历中未来最近的日期 167 | ''' 168 | # 如果输入为空,返回为空 169 | if date is np.nan: 170 | return date 171 | if date < trade_date_lst[0]: 172 | raise ValueError('date must be smaller than trade_date_lst[0]') 173 | # 找未来最近的月频交易日 174 | for i in range(len(trade_date_lst) - 1): 175 | if (trade_date_lst[i] <= date) and (date < trade_date_lst[i + 1]): 176 | return trade_date_lst[i] 177 | 178 | 179 | def get_next_date(date, trade_date_lst): 180 | ''' 181 | Description 182 |    ---------- 183 | 获取交易日历中未来最近的日期 184 | 185 | Parameters 186 |    ---------- 187 | date: str. 所选日期 188 | trade_date_lst: List[str]. 交易日历列表 189 | 190 | Return 191 |    ---------- 192 | str. 交易日历中未来最近的日期 193 | ''' 194 | # 如果输入为空,返回为空 195 | if date is np.nan: 196 | return date 197 | # 如果比提取的交易日历中的第一个交易日来的小,返回他 198 | if date < trade_date_lst[0]: 199 | return trade_date_lst[0] 200 | # 找未来最近的月频交易日 201 | for i in range(len(trade_date_lst) - 1): 202 | if (trade_date_lst[i] < date) and (date <= trade_date_lst[i + 1]): 203 | return trade_date_lst[i + 1] 204 | 205 | 206 | # 画图部分 207 | def plot_bar_line(x1, y1, x2, y2, label1, label2, xlabel, ylabel1, ylabel2, fig_title): 208 | ''' 209 | Description 210 |    ---------- 211 | 绘制双坐标的柱状图和线形图 212 | 213 | Parameters 214 |    ---------- 215 | x1: array_like, 柱状图横坐标 216 | y1: array_like, 柱状图纵坐标 217 | x2: array_like, 线形图横坐标 218 | y2: array_like, 线形图纵坐标 219 | label1: str. 柱状图的图例 220 | label2: str. 线形图的图例 221 | xlabel: str. 横坐标标签 222 | ylabel1: str. 柱状图纵坐标标签 223 | ylabel2: str. 柱状图纵坐标标签 224 | fig_title: str. 图片标题 225 | 226 | Return 227 |    ---------- 228 | figure. 229 | ''' 230 | fig, ax1 = plt.subplots(figsize=(10, 5)) 231 | ax2 = ax1.twinx() 232 | ax1.bar(x1, y1, color='#FF0000', label=label1, width=8) 233 | ax1.set_xlabel(xlabel) 234 | ax1.set_ylabel(ylabel1) 235 | ax2.plot(x2, y2, color='#FFCC00', linewidth=3, label=label2) 236 | ax2.set_ylabel(ylabel2) 237 | # 获取主坐标轴和辅助坐标轴的图例和标签 238 | lines_1, labels_1 = ax1.get_legend_handles_labels() 239 | lines_2, labels_2 = ax2.get_legend_handles_labels() 240 | # 合并图例和标签 241 | lines = lines_1 + lines_2 242 | labels = labels_1 + labels_2 243 | # 在右上角创建一个新的子图对象,并将图例添加到其中 244 | ax_legend = fig.add_subplot(111) 245 | ax_legend.legend(lines, labels, loc=2) 246 | # 设置标题 247 | plt.title(fig_title) 248 | # 隐藏新的子图对象的坐标轴 249 | ax_legend.axis('off') 250 | plt.close() 251 | return fig 252 | 253 | 254 | def plot_multi_line(x_lst, y_lst, label_lst, xlabel, ylabel, fig_title): 255 | ''' 256 | Description 257 |    ---------- 258 | 绘制多根折线图 259 | 260 | Parameters 261 |    ---------- 262 | x_lst: array_like, 折线图横坐标列表 263 | y_lst: array_like, 折线图纵坐标列表 264 | label_lst: array_like, 折线图标签列表 265 | xlabel: str. 折线图横坐标标签 266 | ylabel: str. 折线图纵坐标标签 267 | fig_title: str. 图片标题 268 | 269 | Return 270 |    ---------- 271 | figure. 272 | ''' 273 | n = len(x_lst) 274 | color_range=np.linspace(0.05, 0.95, n) 275 | fig = plt.figure(figsize=(10, 5)) 276 | for i in range(n): 277 | x = x_lst[i] 278 | y = y_lst[i] 279 | label = label_lst[i] 280 | color = plt.cm.jet(color_range[i]) 281 | plt.plot(x, y, color=color, label=label) 282 | plt.legend(loc=2) 283 | plt.xlabel(xlabel) 284 | plt.ylabel(ylabel) 285 | plt.title(fig_title) 286 | plt.close() 287 | return fig --------------------------------------------------------------------------------