恒生电子(上市公司,股票代码:600570),是一个做券商PB系统的公司,非常的高大上!恒生有一个量化平台叫Pro-Trade,简称PTrade,基于恒生做券商系统的优势,PTrade可以直接对接实盘。
中小板最小市值策略,一个大家都非常熟悉的策略。下面看看恒生PTrade的示例程序是如何实现,实现得怎么样?
大家来品一品,评一评。
```
'''
PTrade示例程序
策略名称:
小市值日线交易策略
运行周期:
日线
策略流程:
盘前将中小板综成分股中st、停牌、退市的股票过滤得到股票池
盘中换仓,始终持有当日流通市值最小的股票(涨停标的不换仓)。
'''
# 导入函数库
import pandas as pd
import numpy as np
import time
from decimal import Decimal
#初始化
def initialize(context):
# set_volume_ratio(volume_ratio=0.99)
set_limit_mode('UNLIMITED')
set_benchmark('000300.XSHG')
#股票池
g.index = '399101.XBHS' #中小板综
#持有股票数量
g.buy_stock_count = 3
#筛选股票数量
g.screen_stock_count = 15
#盘前处理
def before_trading_start(context, data):
g.position_last_map = []
g.stock_list = get_index_stocks(g.index)
#获取股票的状态ST、停牌、退市
st_status = get_stock_status(g.stock_list, 'ST')
halt_status = get_stock_status(g.stock_list, 'HALT')
delisting_status = get_stock_status(g.stock_list, 'DELISTING')
#将三种状态的股票剔除当日的股票池
for stock in g.stock_list.copy():
if st_status[stock] or halt_status[stock] or delisting_status[stock]:
g.stock_list.remove(stock)
# g.current_date = context.blotter.current_dt.strftime("%Y%m%d")
g.current_date = str(get_trading_day(-1))
count = 0
flag = False
#获取财务数据尝试5次
while count < 5:
if get_df(context):
log.info('本次获取财务数据成功')
flag = True
break
else:
count +=1
time.sleep(60)
if not flag:
g.handle_data_flag = False
log.info('本次获取财务数据不成功,请检查数据')
else:
g.handle_data_flag = True
#盘中处理
def handle_data(context, data):
if not g.handle_data_flag:
return
position_last_close_init(context)
# g.data = data
buy_stocks = get_trade_stocks(context, data)
log.info('buy_stocks:%s'%buy_stocks)
trade(context, buy_stocks)
#交易函数
def trade(context, buy_stocks):
#卖出
for stock in context.portfolio.positions:
if stock not in buy_stocks:
order_target_value(stock, 0)
log.info('sell:%s'%stock)
#买入
#持有股票数量
position_last_map = [
position.sid
for position in context.portfolio.positions.values()
if position.amount != 0]
position_count = len(position_last_map)
log.info('position_count%s'%position_count)
# position_count = len(context.portfolio.positions)
if g.buy_stock_count > position_count:
value = context.portfolio.cash / (g.buy_stock_count - position_count)
for stock in buy_stocks:
if stock not in context.portfolio.positions:
order_target_value(stock, value)
#获取市值数据函数
def get_df(context):
try:
df = get_fundamentals(g.stock_list, 'valuation', fields=['total_value','a_floats'], date=g.current_date)
g.df2 = df.sort_values(by='a_floats')
return True
except:
return False
#获取买入股票池(涨停股不参与换仓)
def get_trade_stocks(context, data):
g.df2['curr_float_value'] = 0
stocks = list(g.df2.index)
for stock in stocks:
df = g.df2[g.df2.index == stock]
if not df.empty:
g.df2['curr_float_value'][stock] = g.df2['a_floats'][stock]*data[stock].close
else:
g.df2['curr_float_value'][stock] = 0
g.df2 = g.df2[g.df2['curr_float_value']!=0]
df3 = g.df2.sort_values(by='curr_float_value')
stocks = list(df3.head(g.screen_stock_count).index)
up_limit_stock = get_limit_stock(context, stocks)['up_limit']
stocks = list(set(stocks)-set(up_limit_stock))
hold_up_limit_stock = get_limit_stock(context, g.position_last_map)['up_limit']
log.info('持仓涨停股:%s'%hold_up_limit_stock)
count = g.buy_stock_count - len(hold_up_limit_stock)
check_out_lists = stocks[:count]
check_out_lists = hold_up_limit_stock + check_out_lists
return check_out_lists
#生成昨日持仓股票列表
def position_last_close_init(context):
g.position_last_map = [
position.sid
for position in context.portfolio.positions.values()
if position.amount != 0
]
#保留小数点两位
def replace(x):
y = Decimal(x)
y = float(str(round(x, 2)))
return y
#日级别回测获取涨停和跌停状态的股票
def get_limit_stock(context, stock_list):
out_info = {'up_limit':[], 'down_limit':[]}
history = get_history(5, '1d', ['close','volume'], stock_list, fq='dypre', include=True)
history = history.swapaxes("minor_axis", "items")
def get_limit_rate(stock):
rate = 0.1
if stock[:2] == '68':
rate = 0.2
elif stock[0] == '3':
rate = 0.2
return rate
for stock in stock_list:
df = history[stock]
df = df[df['volume']>0]
if len(df.index)< 2:
continue
last_close = df['close'].values[:][-2]
curr_price = df['close'].values[:][-1]
rate = get_limit_rate(stock)
up_limit_price = last_close*(1+rate)
up_limit_price = replace(up_limit_price)
if curr_price >= up_limit_price:
out_info['up_limit'].append(stock)
down_limit_price = last_close*(1-rate)
down_limit_price = replace(down_limit_price)
if curr_price < = down_limit_price:
out_info['down_limit'].append(stock)
return out_info
```
## 我的评论
示例程序就如同一个人的脸面,现在来看看PTrade的这张脸:
### 1. 程序员的优雅和认真
### 1.1 不优雅
看这段代码:
```
# 获取股票的状态ST、停牌、退市
st_status = get_stock_status(g.stock_list, 'ST')
halt_status = get_stock_status(g.stock_list, 'HALT')
delisting_status = get_stock_status(g.stock_list, 'DELISTING')
# 将三种状态的股票剔除当日的股票池
for stock in g.stock_list.copy():
if st_status[stock] or halt_status[stock] or delisting_status[stock]:
g.stock_list.remove(stock)
```
g.stock_list又是copy,又是remove,这么写不香么?
```python
g.stock_list = [stock for stock in g.stock_list if not (
st_status[stock] or halt_status[stock] or delisting_status[stock])]
```
### 1.2 不认真
```python
# 获取市值数据函数
def get_df(context):
try:
df = get_fundamentals(g.stock_list, 'valuation', fields=['total_value', 'a_floats'], date=g.current_date)
g.df2 = df.sort_values(by='a_floats')
return True
except:
return False
```
接下来的内容,请憋住,别笑!
取估值表valuation的字段`a_floats`,a_floats是什么呢?是`可流通A股数量`,嗯**数量**!**数量**!**数量**!按流通股的数量来排序,从未听说过啊。
正确的应该是取字段:`float_value`,A股流通市值(元)。
不信的话,看看字段的说明,API文档:
http://121.41.137.161:9091/hub/data/finance#valuation
http://121.41.137.161:9091/hub/help/api#get_fundamentals
## 2. 系统的缺陷
其实示例程序不优雅、不认真,也没什么,毕竟宽客自己就能改过来 。
下面看看,宽客改不了的地方,即系统固有的缺陷:
```python
count = 0
flag = False
# 获取财务数据尝试5次
while count < 5:
if get_df(context):
log.info('本次获取财务数据成功')
flag = True
break
else:
count += 1
time.sleep(60)
if not flag:
g.handle_data_flag = False
log.info('本次获取财务数据不成功,请检查数据')
else:
g.handle_data_flag = True
```
get_df函数刚刚已经看过了,就是到估值表valuation取流通市值,已经在通过`try...except...`来捕捉错误免得策略嗝屁了。
现在调用get_df。不就是取一个流通市值么,犯得着写这么一大段程序来包围这个get_df函数么?还Retry了5次?
这样的奇观,咱们在聚宽是从来没有看见过吧。
为什么PTrade的程序能写成这样?原因是它的`get_fundamentals`函数是`六脉神剑,时灵时不灵`。
表现为:1)某天能取到数,某天取不到,2)某天有的股票能取到,有的取不到。
`你恐惧不?时有时无的六脉神剑!`
如果不担心这个时有时无,上面好几十条语句,完全可以优雅地写成一条语句:
```python
stock_list = get_fundamentals(
stock_list, 'valuation', fields=['total_value', 'float_value']
).sort_values(
by='float_value'
).index[:5 * g.buy_stock_count].tolist()
```
## 3. 不方便不准确
本策略需要过滤掉涨停跌停的股票,PTrade如何过滤的呢?
```python
# 日级别回测获取涨停和跌停状态的股票
def get_limit_stock(context, stock_list):
out_info = {'up_limit': [], 'down_limit': []}
history = get_history(5, '1d', ['close', 'volume'], stock_list, fq='dypre', include=True)
history = history.swapaxes("minor_axis", "items")
def get_limit_rate(stock):
rate = 0.1
if stock[:2] == '68':
rate = 0.2
elif stock[0] == '3':
rate = 0.2
return rate
for stock in stock_list:
df = history[stock]
df = df[df['volume'] > 0]
if len(df.index) < 2:
continue
last_close = df['close'].values[:][-2]
curr_price = df['close'].values[:][-1]
rate = get_limit_rate(stock)
up_limit_price = last_close * (1 + rate)
up_limit_price = replace(up_limit_price)
if curr_price >= up_limit_price:
out_info['up_limit'].append(stock)
down_limit_price = last_close * (1 - rate)
down_limit_price = replace(down_limit_price)
if curr_price < = down_limit_price:
out_info['down_limit'].append(stock)
return out_info
```
相比较之下:
```python
current_data = get_current_data()
...
if current_data[stock].low_limit < current_data[stock].last_price < current_data[stock].high_limit:
...
```
谁优雅简洁准确,不言自明!
## 4. 其它槽点,想好了再继续
我在聚宽社区发布的帖子"收益狂飙,年化收益100%,11年1700倍,绝无未来函数"的代码,公开出来,比较一下:
```python
# 导入函数库
from jqdata import *
# 初始化函数,设定基准等等
def initialize(context):
# 设定沪深300作为基准
set_benchmark('000300.XSHG')
# 开启动态复权模式(真实价格)
set_option('use_real_price', True)
# 过滤掉order系列API产生的比error级别低的log
log.set_level('order', 'error')
# 股票池
g.security_universe_index = "399101.XSHE" # 中小板
g.buy_stock_count = 3 # 持仓股票只数
# 定时运行
run_daily(my_trade, time='14:40', reference_security='000300.XSHG')
# 开盘时运行函数
def my_trade(context):
# type: (Context) -> None
# 选取中小板中市值最小的若干只
check_out_list = get_index_stocks(g.security_universe_index)
q = query(valuation.code).filter(
valuation.code.in_(check_out_list)
).order_by(
valuation.circulating_market_cap.asc()
).limit(
g.buy_stock_count * 5
)
check_out_list = list(get_fundamentals(q)['code'])
# 过滤: 三停(停牌、涨停、跌停)及st,*st,退市
current_data = get_current_data()
check_out_list = [
stock for stock in check_out_list if
not (current_data[stock].paused or
current_data[stock].is_st or
'ST' in current_data[stock].name or
'*' in current_data[stock].name or
'退' in current_data[stock].name)
and
(current_data[stock].low_limit < current_data[stock].last_price < current_data[stock].high_limit)
]
# 取需要的g.buy_stock_count只股票
target_stocks = check_out_list[:g.buy_stock_count]
# 卖出:不在目标股票池中,又没有涨停的
for stock in context.portfolio.positions:
if (stock not in target_stocks) and \
(current_data[stock].last_price < current_data[stock].high_limit):
order_target(stock, 0)
# 买入:等仓位
position_count = len(context.portfolio.positions)
if g.buy_stock_count > position_count:
value = context.portfolio.available_cash / (g.buy_stock_count - position_count)
for stock in target_stocks:
if stock not in context.portfolio.positions:
order_target_value(stock, value)
if len(context.portfolio.positions) >= g.buy_stock_count:
break
```
2021-07-23
首先,重点不是去说抄袭之类的,没有意义。只是想比较一下实现的优雅程度。
一看,有很熟悉也是很相似的感觉。但明显某Trade的代码长,长其实问题也不大,就是不那么优雅而已,能实现就行呗。
2021-07-23
PTrade的API文档:
http://121.41.137.161:9091/hub/data/finance#valuation
http://121.41.137.161:9091/hub/help/api#get_fundamentals
2021-07-23
a_floats这个太搞了吧,话说某Trade是还是聚宽某coder么。
2021-07-23
@眠いいい 当然不是,是上市公司600570旗下的产品。吹的非常厉害的。
2021-07-23
@WangkJ 君弘君智?如果是君弘君智,就是迅投QMT贴牌。跟聚宽的渊源,肯定是有的。
2021-07-23
@WangkJ 国君web版的就是用的聚宽的系统。
2021-07-23
@MR.叉烧 听了蒋老师的介绍 的确劝退 ptrade的确是一手好牌 可惜可惜
2021-07-23
蒋老师,我刚开始准备量化,啥都不懂。聚宽,PTRADE,掘金各个平台代码看上去都不通用的是么?
2021-07-24
确实,我自己初入量化的时候也对比过几个平台,包括自建平台,最后选了JQ,虽然还是各种不适最终也没走,很大的原因就是API的代码风格。但是最近感觉JQ好像不是很投入开发了,不知道是不是错觉。
2021-07-24