聚宽服务
在聚宽官网申请账号,然后阅读相关 api 文档。
https://www.joinquant.com/view/user/floor?type=mainFloor
安装 SDK 并且登录
使用 pip install jqdatasdk 进行安装, 已经安装之后进行更新安装。
登录
def login(user_name, password):
try:
auth(user_name, password)
except Exception as e:
logger.info(e)
return False
# 判断是否登录成功
if not is_auth():
return False
return True
在官网申请之后, user_name 一般是手机号, password 默认是手机号的后 6 位。 当前使用期 15 天内,每天有 100w 条的数据权限,100w 条数到了的时候给出提醒,自动升级到 1000 w 条。 需要更多的话可能即需要进行付费。
调用 get_price 接口
对调用聚宽 sdk 的 get_price 接口进行封装。首先查看一下接口的说明,可以参照官网文档,或直接点进代码调用的地方进行查看。
相关说明如下:
@assert_auth
def get_price(security, start_date=None, end_date=None, frequency='daily',
fields=None, skip_paused=False, fq='pre', count=None, panel=True, fill_paused=True):
"""
获取一支或者多只证券的行情数据
:param security 一支证券代码或者一个证券代码的list
:param count 与 start_date 二选一,不可同时使用.数量, 返回的结果集的行数, 即表示获取 end_date 之前几个 frequency 的数据
:param start_date 与 count 二选一,不可同时使用. 字符串或者 datetime.datetime/datetime.date 对象, 开始时间
:param end_date 格式同上, 结束时间, 默认是'2015-12-31', 包含此日期.
:param frequency 单位时间长度, 几天或者几分钟, 现在支持'Xd','Xm', 'daily'(等同于'1d'), 'minute'(等同于'1m'), X是一个正整数, 分别表示X天和X分钟
:param fields 字符串list, 默认是None(表示['open', 'close', 'high', 'low', 'volume', 'money']这几个标准字段), 支持以下属性 ['open', 'close', 'low', 'high', 'volume', 'money', 'factor', 'high_limit', 'low_limit', 'avg', 'pre_close', 'paused']
:param skip_paused 是否跳过不交易日期(包括停牌, 未上市或者退市后的日期). 如果不跳过, 停牌时会使用停牌前的数据填充, 上市前或者退市后数据都为 nan
:param panel: 当传入一个标的列表的时候,是否返回一个panel对象,默认为True,表示返回一哥panel对象
注意:
当security为一个标的列表,且panel=False的时候,会返回一个dataframe对象,
在这个对象中额外多出code、time两个字段,分别表示该条数据对应的标的、时间
:param fill_paused : False 表示使用NAN填充停牌的数据,True表示用close价格填充,默认True
:return 如果是一支证券, 则返回pandas.DataFrame对象, 行索引是datetime.datetime对象, 列索引是行情字段名字; 如果是多支证券, 则返回pandas.Panel对象, 里面是很多pandas.DataFrame对象, 索引是行情字段(open/close/…), 每个pandas.DataFrame的行索引是datetime.datetime对象, 列索引是证券代号.
"""
因为不同的期货一段时间内的根数是不一致的,所以是决定 security 参数每次只传入一只期货。 因为我们需要的是分钟线,所以传入的 frequency 是 “1m”, 对应的字段 fields 没有传入, 默认是期货的全部字段,在这里是
[‘open’, ‘close’, ‘low’, ‘high’, ‘volume’, ‘money’]
因为是免费账号,调用是计算次数的,为了避免次数用完,我们可以将数据先在 csv 文件中进行备份,然后下次直接读取 cvs 文件,避免浪费掉调用次数。缺点就是这个csv 文件也是有点大的,我是保存在本地,然后 IDE 就卡住了。所以根据自己的需求对关键数据进行保存。
保存的时候,要指明这是哪一只合约的哪段时间的数据。
相关的代码:
def jz_get_price(security, start_date: datetime.datetime, end_date: datetime.datetime,
frequency='1m'):
# 调用聚宽的接口
df = get_price(security, start_date, end_date, frequency)
if save_csv:
# 将结果写入 csv 文件 (因为聚宽每天的条数是有限制的)
dt_format = "%Y-%m-%d-%H-%M-%S"
file_name = "_".join([security, start_date.strftime(dt_format), end_date.strftime(dt_format)])
file_name = os.path.join("./csv", file_name)
# 保存
df.to_csv(file_name, index=True, sep=',')
return df
def read_df_from_csv(csv_file):
# index_col = 0 的意思是直接使用第一列作为索引
df = pd.read_csv(csv_file, index_col=0)
return df
关于 mongo 数据库
在我们的 mongo 数据库里面已经存在了一个叫做 info 的集合,可以利用它筛选出每天未过期的期货合约。
mongo 数据库区别一下,可能是线上的,也可能是本地的。所以写了两个 get_coll 函数:
def get_124_coll():
return pymongo.MongoClient("127.0.0.1:27017")
def get_local_coll():
return pymongo.MongoClient("127.0.0.1:27018")
def fetch_un_expire_codes(dt: datetime.datetime):
# 获取到每日的未过期合约;统一 code 的格式
cli = get_124_coll()
futures = cli.test_future.info.find({"expire_date": {"$gte": dt}})
codes = [future.get("code") for future in futures]
return codes
统一合约代码的格式
聚宽的合约代码是有 交易所后缀的,我们自己的需求是不需要的。从 info 数据库中筛选出来的合约要经过转换才能作为聚宽 get_price 接口的参数。
def _jq_code_format(code):
# 转换合约代码为聚宽要求的后缀模式
CON_EXCHANGE_DICT = {'SH': 'XSHG', 'SZ': 'XSHE', 'IX': 'INDX', 'SF': 'XSGE', 'DF': 'XDCE',
'ZF': 'XZCE', 'CF': 'CCFX', 'IF': 'XINE'}
exchange, id = code[:2], code[2:]
assert exchange in ("CF", "DF", "SF", "ZF", "IF")
con_exchange = CON_EXCHANGE_DICT.get(exchange, "")
if con_exchange:
return ".".join([id, con_exchange])
根据要求对每一行数据进行修正
在拿到的 df 结构中,时间是索引。但是在插入的 mongo 数据中, 时间是作为每一条数据中的一个字段的。首先我们就要把索引字段作为一个正常的列加入 df 中。
另外,聚宽的分钟线标识规则是 用 9:01 这个时间点来标识 9:00 - 9:01 这个时间段内的分钟线数据,我们自己的规则是用 9:00 来标识同样的一段分钟线。
所以,对于聚宽的索引要进行减 1 min 的操作。
同时,我们拿聚宽某一天的分钟线,就应该是从 00:01 到下一天的 00:00 , 对应于我们系统的 00:00到同一天的 00:59 。
最后,聚宽的字段是: [‘open’, ‘close’, ‘low’, ‘high’, ‘volume’, ‘money’]
我们系统的字段是: [‘open’, ‘close’, ‘low’, ‘high’, ‘volume’, ‘amount’]
也就是说我们要把 money 字段改名为 amount 字段插入数据库中。
最后注意我们还要把字符串形式的时间转换 datetime ,这样我们使用 python 的库进行插入的时候,才会自动转换为 ISODate 类型。
def generate_inserts(df: pd.DataFrame):
# 将 money 列重命名为 amount
df = df.rename(columns={'money': 'amount'})
# 将索引转换为其中的一列
df['time'] = pd.to_datetime(df.index)
# 将聚宽的时间整体减1min
df['time'] = df['time'].map(lambda dt: dt - pd.Timedelta(minutes=1))
# 转换时间格式
df.time = df.time.astype(str)
inserts = list(json.loads(df.to_json(orient="index", date_format="iso")).values())
for insert in inserts:
insert.update({"time": datetime.datetime.strptime(insert['time'], "%Y-%m-%d %H:%M:%S")})
# 生成待插入的列表
return inserts
插入数据
数据准备好之后,就要向 mongo 数据库中进行插入了。
首先我们要确定好数据库的索引。在本例中,很显然,我们要使用 code 和 time 作为联合索引。
创建 mongo 索引可以直接在终端进行,也可以使用 pymongo 进行。
终端方式:
> db.price.ensureIndex({"code": 1, "time": 1}, {unique: true})
{
"createdCollectionAutomatically" : true,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
主程序
将以上步骤整合起来即可:
def main(start: datetime.datetime, end: datetime.datetime):
t1 = time.time()
# 登录
login("xxxxxxxxxxx", "xxxxxx")
# 待插入的数据库
conn = get_local_coll().test_futures.prices
for dt in pd.date_range(start, end, freq="1d"):
dt = dt.to_pydatetime()
# 当天筛选出的合约
futures = fetch_un_expire_codes(dt)
logger.info(f"{dt}, {futures}")
for info_future in futures:
future = _jq_code_format(info_future)
df = jz_get_price(future, dt+datetime.timedelta(minutes=1), dt+datetime.timedelta(days=1))
datas = generate_inserts(df)
mongo_bulk_insert(conn, info_future, datas)
logger.info(f"{future} {dt} 插入成功 ")
t2 = time.time()
logger.info(f"耗时是: {(t2 - t1) / 60} min")
main(datetime.datetime(2019, 11, 1), datetime.datetime(2019, 11, 20))
导出数据库文件
完整代码: https://github.com/furuiyang0715/JQFuturesLoader/blob/master/loader.py
更新时间: 2019-11-22