Skip to content

Commit

Permalink
✨ feat: stash for a76
Browse files Browse the repository at this point in the history
* 增加backtestlog模块,用于输出回测日志时,将时间替换为回测时间
* 增加行情预取功能
* 增加回测报告中绘制自定义指标功能(仅支持Scatter)
  • Loading branch information
aaron-yang-biz committed Oct 28, 2023
1 parent 2745b44 commit 81629d4
Show file tree
Hide file tree
Showing 14 changed files with 483 additions and 22 deletions.
4 changes: 4 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# History

## 2.0.0-alpha76
* 增加backtestlog模块,用于输出回测日志时,将时间替换为回测时间
* 增加行情预取功能
* 增加回测报告中绘制自定义指标功能(仅支持Scatter)
## 2.0.0-alpha.69
* BaseStrategy增加`available_shares`方法
## 2.0.0-alpha.68
Expand Down
2 changes: 2 additions & 0 deletions docs/api/omicron.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@
::: omicron.extensions
# Notify package
::: omicron.notify
# Backtesting Log Facility
::: omicron.core.backtestlog
23 changes: 23 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -235,3 +235,26 @@ omicron提供了mean_absolute_error函数和pct_error函数。它们在scipy或
您应该把这里提供的函数当成实验性的。这些API也可能在某天被废弃、重命名、修改,或者这些API并没有多大作用,或者它们的实现存在错误。

但是,如果我们将来会抛弃这些API的话,我们一定会通过depracted方法提前进行警告。

# 策略编写

omicron通过[strategy](/api/strategy)来提供策略框架。通过该框架编写的策略,可以在实盘和回测之间无缝转换 -- 根据初始化时传入的服务器不同而自动切换。

omicron提供了一个简单的[双均线策略](/api/strategy/#omicron.strategy.sma)作为策略编写的示范。

# 绘图

omicron通过[Candlestick](/api/plotting/candlestick/)提供了k线绘制功能。默认地,它将绘制一幅显示120个bar,可拖动(以加载更多bar),并且可以叠加副图、主图叠加各种指标的k线图:

![](https://images.jieyu.ai/images/2023/05/20230508164848.png)

上图显示了自动检测出来的平台。此外,还可以进行顶底自动检测和标注。

omicron通过[metris](/api/plotting/metrics)提供回测报告。该报告类似于:

![](https://images.jieyu.ai/images/2023/05/20230508160012.png)

它同样提供可拖动的绘图,并且在买卖点上可以通过鼠标悬停,显示买卖点信息。

omicron的绘图功能只能在notebook中使用。

2 changes: 1 addition & 1 deletion mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ repo_name: omicron
nav:
- 简介: index.md
- 安装: installation.md
- 教程: usage.md
- 快速入门: usage.md
- 版本历史: history.md
- 开发者文档: developer.md
- API文档:
Expand Down
101 changes: 101 additions & 0 deletions notebooks/conner_rsi.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
{
"cells": [
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"Nirvana Systems把Conners RSI称作终极技术指标,并在它的网站上发布了关于如何生成和使用这一指标的文章。著名的回测框架 backtrader也在文档和示例中引用了这篇文章。TradingView也内置了这一指标。\n",
"\n",
"为什么Conners RSI被称为终极技术指标,它有哪些优势?成功背后的原理是什么,又该如何实现这一指标呢?这篇文章就为你介绍。\n",
"\n",
"Conners RSI是在标准RSI的基础上,混合了另外两个指标得到的。\n",
"\n",
"第一个指标就是Streaks。它本质上是统计连续上涨或者下跌的周期数,并将该指标的RSI作为一个分数。我们知道,股票连涨周期数越长,则越可能反生反转(即下跌);反之亦然。大家可以按照我们在《数据分析与Python实现》那几节课中,介绍的PDF/CDF的方法,来自行估计当某个标的连续上涨N天后,接下来继续上涨的概率有多大。\n",
"\n",
"下图显示了每日收盘价,及由此计算的streaks指标:\n",
"\n",
"![50%](https://i.stack.imgur.com/SIQUq.png)\n",
"\n",
"第二个指标就是今天的涨跌幅,在过去一段时间内的的涨跌幅中的排名。它从另一个维度,描绘了当前市场的强弱。如果在过去20天内,只有3天的涨幅低于今天,那么今天的相对强弱就是15%,次日反转可能性大;如果17天的涨幅低于今天,那么今天的相对强弱就是85%,次日下跌的可能性变大。\n",
"\n",
"这两个指标加上经典RSI,就合成了Conners RSI:\n",
"\n",
"$$\n",
" CRSI(3, 2, 100) = [RSI(3) + RSI(Streak, 2) + PercentRank(100)] / 3\n",
"$$\n",
"\n",
"与单一的RSI只计算了上涨与下跌的空间相比,streaks指标则强调了上涨和下跌的时间周期 -- 只有一轮趋势的空间和时间都到位,才有更有可能发生反转;而PercentRank往往可以寻找到行情的加速阶段:一旦出现明显的涨跌异常 -- 这也是一段行情进入尾声的标志之一。正因为如此,Conners RSI才被认为是更有效的摆动类指标,可以比较精准地捕捉顶和底。\n",
"\n",
"Conners RSI的实际表现如何?我们拿最近1000天的沪指进行测试:\n",
"\n",
"![50%](https://images.jieyu.ai/images/2023/08/corners_rsi.png)\n",
"\n",
"backtrader的回测表明,最近4年以来(近似于1000个交易日),沪指仅上涨5.76%,但通过cornner's RSI策略抄底逃顶,我们在指数上竟然获得超过44%的收益。如果是对个股进行操作,收益很可能是数倍。\n",
"\n",
"由于这段时间毕竟沪指是上涨的,如果遇到行情极端不好,访策略表现如何?近两年来恒生指数就宛如下跌的飞刀,假设我们要强接飞刀,表现又是怎样?\n",
"\n",
"![50%](https://www.taindicators.com/blog/2022/connors/2800hk.png)\n",
"<caption>图片来源: www.taindicators.com</caption>\n",
"\n",
"结论是,还是不要去接下跌的飞刀。但即使如此,即使是遇到下跌的飞刀,corner's RSI的表现也大大超过指数。"
]
},
{
"attachments": {},
"cell_type": "markdown",
"metadata": {},
"source": [
"from coursea import *\n",
"await init()\n",
"\n",
"class ConnerRSIStrategy(BaseStrategy):\n",
" def __init__(self, url: str, sec: str, **kwargs):\n",
" self.sec = sec\n",
" self.pstreak = kwargs.get(\"pstreak\", 2)\n",
" self.prank = kwargs.get(\"prank\", 20)\n",
" \n",
" self.low_watermark = kwargs.get(\"low_watermark\", 13)\n",
" self.high_watermark = kwargs.get(\"high_watermark\", 58)\n",
" super().__init__(url, **kwargs)\n",
" \n",
" \n",
" async def predict(self, frame: Frame, frame_type: FrameType, i: int, barss, **kwargs):\n",
" bars = barss[self.sec]\n",
" close = bars[\"close\"]\n",
" \n",
" rsi = ta.RSI(close, 6)\n",
" streak = Streak(close)\n",
" rsi_streak = ta.RSI(streak, self.pstreak)\n",
" \n",
" pclose = close[-self.prank:]\n",
" prank = fsum(x < pclose[-1] for x in pclose)/ pclose\n",
" \n",
" crsi = (rsi[-1] + rsi_streak[-1] + prank)/3.0\n",
" \n",
" print(\"crsi is\", crsi)\n",
" if crsi <= self.low_watermark:\n",
" await self.buy(self.sec, money=self.cash, order_time=tf.combine_time(frame, 14, 56))\n",
" elif crsi >= self.high_watermark:\n",
" avail = self.available_shares(sec, frame)\n",
" await self.sell(self.sec, vol = avail, order_time=tf.combine_time(frame, 14, 56))\n",
" \n",
"start = datetime.date(2023, 1, 4)\n",
"end = datetime.date(2023, 4, 14)\n",
"\n",
"cs = ConnerRSIStrategy(cfg.backtest.url, \"002344.XSHE\", start=start, end=end, frame_type=FrameType.DAY, baseline=\"399300.XSHE\")\n",
"await cs.backtest(portfolio = [\"002344.XSHE\"], n=30)\n",
"await cs.plot_metrics()"
]
}
],
"metadata": {
"language_info": {
"name": "python"
},
"orig_nbformat": 4
},
"nbformat": 4,
"nbformat_minor": 2
}
101 changes: 101 additions & 0 deletions omicron/core/backtestlog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
"""
回测时,打印时间一般要求为回测当时的时间,而非系统时间。这个模块提供了改写日志时间的功能。
使用方法:
```python
from omicron.core.backtestlog import BacktestLogger
logger = BacktestLogger.getLogger(__name__)
logger.setLevel(logging.INFO)
handler = logging.StreamHandler()
# 通过bt_date域来设置日期,而不是asctime
handler.setFormatter(Formatter("%(bt_date)s %(message)s"))
logging.basicConfig(level=logging.INFO, handlers=[handler])
# 调用时与普通日志一样,但要增加一个date参数
logger.info("this is info", date=datetime.date(2022, 3, 1))
```
上述代码将输出:
```
2022-03-01 this is info
```
使用本日志的核心是上述代码中的第3行和第9行,最后,在输出日志时加上`date=...`,如第15行所示。
注意在第9行,通常是`logging.getLogger(__nam__)`,而这里是`BacktestLogger.getLogger(__name__)`
如果上述调用中没有传入`date`,则将使用调用时间,此时行为跟原日志系统一致。
!!! warning
当调用logger.exception时,不能传入date参数。
"""

import datetime
import logging
from typing import Union

from coretypes import Frame


class BacktestLogger(object):
logger = None

def __init__(self, name):
self._log = logging.getLogger(name)

@classmethod
def getLogger(cls, name: str):
if cls.logger is None:
cls.logger = BacktestLogger(name)

return cls.logger

@classmethod
def debug(cls, msg, *args, date: Union[str, Frame, None] = None):
cls.logger._log.debug(
msg, *args, extra={"bt_date": date or datetime.datetime.now()}
)

@classmethod
def info(cls, msg, *args, date: Union[str, Frame, None] = None):
cls.logger._log.info(
msg, *args, extra={"bt_date": date or datetime.datetime.now()}
)

@classmethod
def warning(cls, msg, *args, date: Union[str, Frame, None] = None):
cls.logger._log.warning(
msg, *args, extra={"bt_date": date or datetime.datetime.now()}
)

@classmethod
def error(cls, msg, *args, date: Union[str, Frame, None] = None):
cls.logger._log.error(
msg, *args, extra={"bt_date": date or datetime.datetime.now()}
)

@classmethod
def critical(cls, msg, *args, date: Union[str, Frame, None] = None):
cls.logger._log.critical(
msg, *args, extra={"bt_date": date or datetime.datetime.now()}
)

@classmethod
def exception(cls, e):
cls.logger._log.exception(e, extra={"bt_date": ""})

@classmethod
def log(cls, level, msg, *args, date: Union[str, Frame, None] = None):
cls.logger._log.log(
level, msg, *args, extra={"bt_date": date or datetime.datetime.now()}
)

@classmethod
def setLevel(cls, level):
cls.logger._log.setLevel(level)
3 changes: 1 addition & 2 deletions omicron/plotting/candlestick.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,13 @@ def __init__(
Args:
bars: 行情数据
ma_groups: 均线组参数。比如[5, 10, 20]表明向k线图中添加5, 10, 20日均线。如果不提供,将从数组[5, 10, 20, 30, 60, 120, 250]中取直到与`len(bars) - 5`匹配的参数为止。比如bars长度为30,则将取[5, 10, 20]来绘制均线。
win_size: 缺省绘制多少个bar,超出部分将不显示。
title: k线图的标题
show_volume: 是否显示成交量图
show_rsi: 是否显示RSI图。缺省显示参数为6的RSI图。
show_peaks: 是否标记检测出来的峰跟谷。
width: the width in 'px' units of the figure
height: the height in 'px' units of the figure
kwargs:
Keyword Args:
rsi_win: default is 6
"""
self.title = title
Expand Down
40 changes: 36 additions & 4 deletions omicron/plotting/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,19 @@

class MetricsGraph:
def __init__(
self, bills: dict, metrics: dict, baseline_code: Optional[str] = "399300.XSHE"
self,
bills: dict,
metrics: dict,
baseline_code: str = "399300.XSHE",
indicator: Optional[pd.DataFrame] = None,
):
"""
Args:
bills: 回测生成的账单,通过Strategy.bills获得
metrics: 回测生成的指标,通过strategy.metrics获得
baseline_code: 基准证券代码
indicator: 回测时使用的指标。如果存在,将叠加到策略回测图上。它应该是一个以日期为索引,指标值列名为"value"的pandas.DataFrame。如果不提供,将不会绘制指标图
"""
self.metrics = metrics
self.trades = bills["trades"]
self.positions = bills["positions"]
Expand All @@ -49,7 +60,14 @@ def __init__(
tf.int2date(f) for f in tf.get_frames(self.start, self.end, FrameType.DAY)
]

# 记录日期到下标的反向映射,这对于在不o
if indicator is not None:
self.indicator = indicator.join(
pd.Series(index=self.frames, name="frames"), how="right"
)
else:
self.indicator = None

# 记录日期到下标的反向映射
self._frame2pos = {f: i for i, f in enumerate(self.frames)}
self.ticks = self._format_tick(self.frames)

Expand Down Expand Up @@ -253,7 +271,7 @@ async def plot(self):
cols=2,
shared_xaxes=False,
specs=[
[{"type": "scatter"}, {"type": "table"}],
[{"secondary_y": True}, {"type": "table"}],
],
column_width=[0.75, 0.25],
horizontal_spacing=0.01,
Expand All @@ -262,7 +280,6 @@ async def plot(self):

fig.add_trace(await self._metrics_trace(), row=1, col=2)

print("baseline", len(baseline_prices))
baseline_trace = go.Scatter(
y=baseline_prices,
x=self.ticks,
Expand All @@ -277,6 +294,21 @@ async def plot(self):
)
fig.add_trace(nv_trace, row=1, col=1)

if self.indicator is not None:
ind_trace = go.Scatter(
y=self.indicator["value"],
x=self.ticks,
mode="lines",
name="indicator",
showlegend=True,
)
fig.add_trace(
ind_trace,
row=1,
col=1,
secondary_y=True,
)

for trace in await self._trade_info_trace():
fig.add_trace(trace, row=1, col=1)

Expand Down
Loading

0 comments on commit 81629d4

Please sign in to comment.