tf
tf代码
# momentums.py
# import sys, io, os, inspect
import numpy as np, pandas as pd; _=np
pd.set_option('display.precision', 6)
# import matplotlib.pyplot as plt
# from dqsbt.util2 import util as aid # 加载数据
# from util import ttr # 计算指标
# import candleplotter as cplt
# from qsutil import pkl
# from texttable import Texttable
# from packages import report as rp
# from qsutil import pkl; _cn_dict = pkl.pkl_load('d:/db/amipy/data/codename_dict.pkl')
_cn_dict = {
'399006': '创板100',
'399300': '沪深300',
'399905': '中证500',
}
import momentums_config as cfg
# import utility as ut
from qsutil import utility as ut
from qsutil import bbplot as bbp; _=bbp
import flower as fl; _=fl
p=dict(mfile='D:\\algolab\\study\\momentums\\momentums.py') # __file__
_pn = [n for n in dir(cfg) if not n.startswith('__')]
_pv = [getattr(cfg, n) for n in _pn]
p.update(dict(zip(_pn, _pv)))
from collections import namedtuple
Ticktime = namedtuple('Ticktime', 'oTime, bnTime, anTime, ccTime, slTime, cTime')
tt = Ticktime('9:25', '11:30', '13:00', '14:55', '14:56', '15:00')
NamedOrderMultiple = namedtuple('NamedOrderMultiple',
'i date time operation code price percent')
NamedOrderSingle = namedtuple('NamedOrderSingle',
'i date time operation code price percent')
def pre_proc():
'''
>>> ohlc, ind = pre_proc()
'''
ohlc, _codes, _dnames=ut.load(subset=cfg.subset)
ind = ut.indicator(ohlc, n1=cfg.n1, n2=cfg.n2)
return ohlc, ind
class Order(object):
'''
多股组合下单器
创建买单: 倒数第8行的数据, 收盘前的, 买入, 第二只个股, 目标仓位10%
创建查单: 倒数第8行的数据, 收盘时刻, 查资产,
创建卖单: 倒数第2行的数据, 收盘前的, 卖出, 第二只个股, 目标仓位0%
o=Order(ind)
bo = o.create_buy_order (-8, o.codes[1], 0.1)
print(o)
co = o.create_check_totcap_order(-8, )
print(o)
co = o.create_check_totcap_order(-7, )
print(o)
co = o.create_check_totcap_order(-6, )
print(o)
so = o.create_sell_order(-2, o.codes[1], 0.)
print(o) # 单个订单
bbo = o.create_batch_buy_order(10, o.codes[:2], [.5,.3])
bbo = o.create_batch_buy_order(100, o.codes[:2], [.5,.3])
print(o.orders) # 订单列表(不包括查单)
'''
def __init__(self, ind):
self.ind = ind
self.codes = list(self.ind.columns.get_level_values(1).unique())
self.init()
def init(self):
self.rowid, self.date, self.time, \
self.operation, self.code, self.price, self.percent = \
(None, None, None, None, None, None, None)
self.orders = []
def __repr__(self):
dt = self.date.isoformat()[:10]
return '<Order {}>'.format(' '.join([
f'{self.rowid:4d}',
dt,
self.time,
f'{self.operation:2d}',
self.code,
f'{self.price:.2f}',
f'{self.percent:3.0%}'
]))
def _create(self, rowid, time, operation, code, percent):
mdf=self.ind.iloc[rowid]
date = mdf.name
if operation==1 or operation==-1:
price = mdf.loc['close', code]
print(f'code={code}, price={price}')
self.order = NamedOrderSingle(
rowid, date, time, operation, code, price, percent)
self.rowid, self.date, self.time, \
self.operation, self.code, self.price, self.percent = \
(rowid, date, time, \
operation, code, price, percent)
elif operation==0:
_code=self.codes
_price=mdf.loc['close'].tolist()
_price_dict={ k:v for k,v in zip(self.codes, _price)}
self.order = NamedOrderSingle(
rowid, date, time, operation, _code, _price_dict, percent)
self.rowid, self.date, self.time, \
self.operation, self.code, self.price, self.percent = \
(rowid, date, time, \
operation, ' ', 0.0, 0.0)
if operation==1 or operation==-1:
self.orders.append(self.__repr__())
return self.order
def create_buy_order(self, rowid, code, percent):
time, operation = tt.ccTime, 1
self._create(rowid, time, operation, code, percent)
return self.order
def create_batch_buy_order(self, rowid, code, percent):
time, operation = tt.ccTime, 1
assert isinstance(code, list) and len(code)>1, 'Can not create batch order'
orders=[]
for _code, _pct in zip(code, percent):
self._create(rowid, time, operation, _code, _pct)
orders.append(self.order)
return orders
def create_sell_order(self, rowid, code, percent):
time, operation = tt.ccTime, -1
self._create(rowid, time, operation, code, percent)
return self.order
def create_check_totcap_order(self, rowid, ):
time, operation = tt.cTime, 0
code, percent = '', 0.0
self._create(rowid, time, operation, code, percent)
return self.order
def porders(self):
'''print history orders'''
print(self.order._fields)
for o in self.orders: print(o)
class JiaoGeDan(object):
# 交割单列名字符串, 其格式与打印单笔交割单相匹配,
# 同时用来提取交割单列索引
jgd_cols = 'i date time op code name price pct ' + \
'volume amount fee cashflow ' + \
'balance ntrade '
Jgd=namedtuple('Jgd', jgd_cols) # 将具名元组类初始化为属性
def __init__(self, *args):
self.jgd = self.Jgd(*args)
def __repr__(self):
_dict = self.jgd._asdict()
_dict.pop('i')
_dict.pop('date')
s = '<jgd {:>4d}'.format(self.jgd[0])
s += ' {}'.format(self.jgd[1].isoformat()[:10])
s += ''' \
{time:5s} \
{op:3d} \
{code:6s} \
{name:8s} \
{price:8.2f} \
{pct:3.0%} \
{volume:5d} \
{amount:>10.2f} \
{fee:>8.2f} \
{cashflow:>10.2f} \
{balance:>10.2f} \
{ntrade:6d} \
>'''.format(**_dict)
return s
class Account(object):
def __init__(self, n):
self.n=n
f = 'i date names'.split()
f.extend(list(map(lambda x:''.join(['psn', str(x), '_mval']), range(n))))
f.extend('totmktcap balance totcap'.split())
f.extend(list(map(lambda x:''.join(['psn', str(x), '_pct']), range(n))))
f.append('tot_pos_pct')
self.Account = namedtuple('Account', f)
class Broker(object):
'''
'''
def __init__(self, cfg, ind,
nComponent=3,
nAdjustPos=5,
dbg=True,
):
self.cfg, self.ind=cfg, ind
# 投资组合的代码列表一定要升序排列, 保持与mdf的一致性
# 简单地, 直接取值于ind mdf的内层列索引就好了
# self.codes=cfg.codes
self.codes = list(self.ind.columns.get_level_values(1).unique())
self.nComponent, self.nAdjustPos, self.dbg = nComponent, nAdjustPos, dbg
self.commission=cfg.commission
self.capital = cfg.capital
self.balance, self.totmktcap, self.totcap = None, 0., 0.
self.ntrades=0
# mcols = 'code name volume price mktcap pospct'.split()
self.position_mcols = pd.MultiIndex.from_product(
[range(self.nComponent),
'code name price volume'.split()
]
)
self.position_mdf=pd.DataFrame({}, None, columns=self.position_mcols)
self.init()
self._init_account()
def init(self):
self.onehand=1 if self.codes[0][:2]=='39' else 100
self.fee, self.fees=None, []
self.positions, self.totcaps, self.jgds=[], [], []
def _init_account(self):
# 单行账户信息构造函数
# 使用方法: self.account = self.call_account(*args)
self.call_account = Account(self.nComponent).Account
self.account=None
self.accounts=[]
def _calc_afc(self, *args, ):
'''calc amount/fee/cashflow'''
volume, price=args
op=self.order.operation
if op==1:
amount = volume * price
fee = amount * self.commission
cf = - (amount + fee);
elif op==-1:
amount = volume * price
fee = amount * self.commission
cf = amount - fee
return (amount, fee, cf)
def _init_position(self, batch_buy_order):
''' 初始化头寸MultiCols df (mcdf)
broker接收到首次批量买单之后, 进行的操作. 初始股数设置为零.
'''
date = batch_buy_order[0].date
data=[]
for o in batch_buy_order:
cnpv=o.code, _cn_dict[o.code], o.price, 0
data.extend(cnpv)
self.position = pd.DataFrame([data], index=[date], columns=self.position_mcols)
def _first_batch_buy(self, bbo):
'''首次买单专用, 用来去除在买入操作(_buy方法)里的检查balance状态 '''
assert self.balance is None, '不应该是首次买单'
self.totcap = self.balance = self.capital
self._init_position(bbo)
# weight=[1/self.nComponent for i in self.buy_list]
# self.money = [self.totcap * w for w in self.weight]
totcap = self.totcap
for o in bbo:
price = o.price
money = totcap * o.percent
volume = int( (money/price)//self.onehand) * self.onehand
self._buy(o, volume)
pass
def buy_target_percent(self, order, buylist=None):
'''
单股买入, 包括加仓和新建仓位两种情况
成交量由目标仓位来决定
buylist的用法:
加仓时用不着
新建仓位时用来确定psn/头寸的席位号
'''
self.order = order
target_pct = order.percent
psn = self._get_psn()
price = order.price
# 加仓操作
if psn is not None:
volume = self.get_position_volume()[psn]
addon_vol = (self.totcap * target_pct - price * volume )/price
addon_vol = int(addon_vol//self.onehand) * self.onehand
if addon_vol >= 1:
self._buy(order, addon_vol)
# 想要买入新的个股
else:
volume = int(
(self.totcap * target_pct)//price
)
seat_dict = dict(zip(
buylist,
range(len(buylist))
))
psn = seat_dict[order.code]
if volume >= 1:
self._buy(order, volume, flag=psn)
pass
def sell_target_percent(self, order):
self.order = order
target_pct = order.percent
psn = self._get_psn()
price = order.price; _=price
if psn is not None: # 减仓 清仓操作
volume = self.get_position_volume()[psn]
if target_pct==0:
reduce_vol = volume # 暂且为清仓
# reduce_vol = (self.totcap * target_pct - price * volume )/price
# reduce_vol = int(addon_vol//self.onehand) * self.onehand
if reduce_vol >= 1:
self._sell(order, reduce_vol)
else:
print('功能还需完善!')
raise RuntimeError('抱歉, 功能还不完善!')
else:
raise RuntimeError('想要卖出不在头寸里的个股')
pass
def sort_position_seat(self, buylist= ['399300', '399905', '399006']):
''' 每次批操作之前应该先调用该函数, 以便使头寸进入到正确的席位上
依照buylist订单里的席位顺序
动能大的进首席
'''
pos = self.position
# pos = utest_position(3)
date = pos.index.values[0]
seat_dict = dict(zip( buylist, range(len(buylist)), ))
df = pos.loc[date].unstack(1) # 对于单行的mcdf, 这样做就消除了时间戳, 使之成为常规df
# df = pos.loc[ [date]].unstack(1) # 对于单行的mcdf, 这样做就没有降低维度
# df1 = pos.stack(0).loc[date] # 非常不一样的df
df['key'] = df.code.apply(lambda x: seat_dict[x])
# 处理过程中也可以忽略重置索引和索引名称
df=df.sort_values(by='key').drop('key', axis=1) #.reset_index(drop=True)
# df.index.name='seat'
flat_val = df.values.flatten()
self.position = new_pos= pd.DataFrame([flat_val], [date], pos.columns)
_=new_pos
# p.stack().sort_values( by= (date, 'code') , axis=1)
self.position.columns.set_levels(range(self.nComponent), level=0, inplace=True)
# return new_pos
def _get_psn(self):
'''获取头寸坐席号psn'''
order=self.order
date = order.date; _=date
code = order.code
# 因为头寸df是单行的时间索引, 2层列索引(第一层是席位号,第二层是名称和持股数量)
# 该时间戳对应最后一次操作日期, 所以用iloc方法较为通用
# tmp = self.position.loc[date].unstack()
# tmp 是消灭掉时间戳的df, 方便过滤出来席位号
# code name price volume
# 0 399006 创板100 2151.39 4
# 1 399300 沪深300 3982.19 2
# 2 399905 中证500 5576.75 1
tmp = self.position.iloc[0].unstack()
try:
psn = tmp[tmp.code==code].index.values[0] # position seat number
except:
psn = None # 应该是订单里要操作的个股不在头寸里面, (不属于加减仓的操作)
print(f'Warning: {order.i} {order.code} 要操作的个股目前不在头寸里.')
return psn
def _buy(self, order, volume, flag=-1):
'''用账户当前的资金余额买入股票, 成交量和成交价分别是volume和price.
资金余额不足时会抛出异常.
flag: 加仓或者开新仓标志.
-1: 表示加仓, seat头寸坐席号psn保持不变
>0的整数: 建新仓的psn坐席号
'''
self.order=order
# self.balance = self.capital if self.balance==None else self.balance
psn = self._get_psn() if flag==-1 else flag
price = order.price
if self.balance > self.onehand*price*(1+self.commission):
self.bprice = price * (1+self.commission)
else:
raise RuntimeError(f'资金余额不足! 想要买入数量={volume} @价格={price}')
amount, fee, cf = self._calc_afc(volume, price, )
self.balance += cf # 更新现金余额
# 更新"动态头寸"里的时间戳和个股的股数, 最新买入价
self.position.index = (order.date,)
if flag==-1:
self.position.loc[order.date, (psn, 'volume')] += volume
self.position.loc[order.date, (psn, 'price')] = price
elif flag>=0:
# 新建一个临时头寸变量other, 然后并联到position里
self.other_pos = other = self.create_tmp_pos(order, volume, psn)
self.position = pd.concat(
[self.position, other], axis=1, ignore_index=False
)
self._jgd = (volume, amount, fee, cf, self.balance, self.ntrades)
self.log_jgd()
def create_tmp_pos(self, order, volume, psn):
'''
注意列名清单一定要升序排列, 因为后续需要行列转换操作.
这些操作默认情况下pandas会对索引名称自动地进行升序排列.
'''
data = (order.code,
_cn_dict[order.code],
order.price,
volume,
)
other = pd.DataFrame(
[data],
index=[order.date,],
columns=pd.MultiIndex.from_product([
[100+psn], # +100表示: 避免并联时席位号冲突
'code name price volume'.split() # 一定要升序排列
])
)
# print(f'\n临时新建头寸other_pos \n {other}\n')
return other
def log_jgd(self):
# 构造交割单: 在原始订单第6位置插入 name, 然后合并上交割字段
# 'i date time op code name price pct \
# volume amount fee cashflow \
# balance \
# ntrade '
_ = self.order[:5] + (_cn_dict[self.order.code],) + self.order[5:7] + self._jgd
self.jgd = JiaoGeDan(*_)
self.jgds.append(self.jgd.__repr__())
'''
if self.dbg:
print(jgd)
self.printAcc1(*jgd)
'''
def _sell(self, order, volume):
'''用账户当前的股票余额卖出股票, 成交量和成交价分别是volume和price.
股票余额不足时会抛出异常.'''
self.order = order
psn = self._get_psn()
price = self.order.price
# if not self.possize >= volume:
# raise RuntimeError(f'股票余额不足! 想要卖出数量={volume} @价格={price:.2f}')
volume = int(volume//self.onehand * self.onehand)
amount, fee, cf = self._calc_afc(volume, price)
self.balance += cf # 更新可用资金
self.ntrades += 1 # 交易次数
# 更新"动态头寸"里的时间戳和个股的股数
self.position.index = (order.date,)
self.position.loc[order.date, (psn, 'volume')] -= volume
# 持股数量为零时, 及时清楚该席位psn
if self.position.loc[order.date, (psn,'volume')]==0:
self.position.drop(columns=psn, level=0, inplace=True)
self._jgd = (volume, amount, fee, cf, self.balance, self.ntrades)
self.log_jgd()
def _check_totcap(self, order, picktime=1500):
'''picktime:1500 表示收盘时刻的查资金操作, 会把账户信息保存到历史清单里
别的时间的 计算总资产, 不会保存, 只是用来为目标仓位下单提必要数据.
'''
self.order=order
curr = self.calc_totcap(picktime); _=curr
def _gcp(self,order):
''' get close price at closing belling time of each day'''
self.order=order
price=order.price
if self.possize>0:
op=10 #'hs' #hold shares
order = order._replace(operation=op)
mktcap = price * self.possize;
totcap = mktcap + self.balance;
self.pos = self.pospct = mktcap/totcap #市值 总资产 仓位占比
_=(0, 0, 0, 0, self.balance, mktcap, totcap, self.pospct, self.possize)
_=order + _ + (self.ntrades, )
jgd=self.Jgd(*_)
if self.dbg: self.printAcc1(*jgd)
self.jgds.append(jgd)
elif self.possize==0:
op=0 #'hc' # hold cash
order = order._replace(operation=op)
mktcap = 0
totcap = self.capital if self.balance is None else self.balance
self.pos=self.pospct=0
_=(0, 0, 0, 0, self.balance, 0, totcap, 0, 0)
_=order + _ + (self.ntrades, )
jgd=self.Jgd(*_)
if self.dbg: self.printAcc1(*jgd)
self.jgds.append(jgd)
gcp=_gcp # alias name of method
def _get_position(self, iloc=None):
'''
>>> p.get_position()
code mktcap name pospct price volume
0 399006 54668.5 创业板指 0.499637 2186.74 25
1 399905 51360.4 中证500 0.469403 5706.71 9
>>> p.get_position().volume.tolist()
Out[522]: [25, 9]
>>> p.get_position(1)
code 399905
mktcap 51360.4
name 中证500
pospct 0.469403
price 5706.71
volume 9
Name: 1, dtype: object
>>> p.get_position(1).code
Out[518]: '399905'
>>> p.get_position(1)['volume']
Out[519]: 9
'''
if iloc is None:
p_df = self.position_mdf.iloc[-1].unstack() #level=-1, 表示内层索引, 把头寸信息平放到列轴里
elif isinstance(iloc, int):
p_df = self.position_mdf.iloc[-1].unstack().loc[iloc]
else:
raise RuntimeError('头寸的索引iloc越界了!')
return p_df
def get_position(self):
return self.position.stack(0)
def get_position_volume(self):
return self.position.loc[:, (range(self.nComponent),'volume')].values[0]
get_position_size = get_position_volume
def get_position_code(self):
return self.position.loc[:, (range(self.nComponent),'code')].values[0]
def get_position_name(self):
return [_cn_dict[c] for c in self.get_position_code()]
def calc_totcap(self, picktime=1500):
# np.array([2,3]) * np.array([4,5])
'''
account = self.position.stack(0).loc[:, ('price volume'.split())].prod(axis=1).unstack(level=1)
account['totmktcap'] = account.sum(axis=1)
account['balance'] = self.balance
account['totcap'] = account['balance totmktcap'.split()].sum(axis=1)
'''
code = self.get_position_code()
names = self.get_position_name()
volume = self.get_position_volume()
price = [self.order.price[_code] for _code in code ]
mktcap = volume * price
totmktcap = sum(mktcap)
mkt_value = tuple(mktcap) + (totmktcap, )
self.totcap = totmktcap + self.balance
pospct = np.array(mkt_value) / self.totcap
_ = (self.order.i, self.order.date, ' '.join(names)) + \
mkt_value + \
(self.balance, self.totcap) + \
tuple(pospct)
curr = self.call_account(*_)
if picktime==1500:
self.account = curr
self.accounts.append(self.account)
def _paccount(self, rdata):
''' print one row of account'''
_ = ' {:4d}'.format(rdata[0]) # 序号 日期 标的名称串
_ += ' {:10s}'.format(rdata[1].isoformat()[:10]);
_ += " '{}'".format(rdata[2]); iloc_mv = 3+self.nComponent+3
_ += ' '.join(map(lambda x: f'{x:10.2f}', rdata[3:iloc_mv])) # 市值
_ += ' '.join(map(lambda x: f'{x:8.2%}', rdata[iloc_mv: ])) # 头寸占比
# _ = ''.join([_1, _2, _3])
print(f'<Acnt {_} >')
def paccount(self):
self._paccount(self.account)
def paccounts(self, tail=5):
for data in self.accounts[-tail:]:
self._paccount(data)
def check_code_to_be_killed(self, order):
''' 利用集合运算更简单: 先求出交集, 然后求出: 出仓股和入仓股
'''
symbols = [s for s in self.get_position().code if s not in order.code]
if bool(symbols):
return symbols
else: return None
def print_info(b):
print('交割单:\n', b.jgd);
print('历史交割单');
for j in b.jgds:print('', j)
print(f'现金余额:{b.balance:.2f}'); print('当前头寸:\n', b.position.stack(0));
def utest(nCom=2, weight=[0.7, 0.3]):
'''
>>> reset -f
>>> ohlc, ind, o, b = utest(2, (0.7, 0.3))
>>> ohlc, ind, o, b = utest(3, (0.3, 0.3, 0.3))
'''
ohlc, ind = pre_proc()
nCom=3
o=Order(ind)
b=Broker(cfg, ind, nComponent=nCom)
# 第100行时 下批量买单
i=100
bbo = o.create_batch_buy_order(i, o.codes[:nCom], percent=(.1,.1,.1))
print('最新订单:\n', o); print('\n历史订单:'); o.porders() # 最新订单和历史订单
b._first_batch_buy(bbo)
print_info(b)
ct = o.create_check_totcap_order(i) # 查资金订单
# totcap = b._check_totcap(ct, picktime=1400)
b._check_totcap(ct, )
b.paccount()
for i in range(101, 130):
# 第105行时, 买单, 第1只个股, 目标仓位.4
# 第110行时, 买单, 第2只个股, 目标仓位.3
# 第115行时, 买单, 第3只个股, 目标仓位.2
# 120行时, 清仓 第3只个股
if i==105:
ct = o.create_check_totcap_order(i) # 查资金订单
b._check_totcap(ct, picktime=1400) # 执行查资金操作, 来刷新self.totcap
bo = o.create_buy_order(i, o.codes[0], 0.4)
b.buy_target_percent(bo)
elif i==110:
ct = o.create_check_totcap_order(i) # 查资金订单
b._check_totcap(ct, picktime=1400) # 执行查资金操作, 来刷新self.totcap
bo = o.create_buy_order(i, o.codes[1], 0.3)
b.buy_target_percent(bo)
elif i==115:
ct = o.create_check_totcap_order(i) # 查资金订单
b._check_totcap(ct, picktime=1400) # 执行查资金操作, 来刷新self.totcap
bo = o.create_buy_order(i, o.codes[2], 0.2)
b.buy_target_percent(bo)
elif i==120:
so = o.create_sell_order(i, o.codes[2], 0.)
b.sell_target_percent(so)
print(f'i={i}')
ct = o.create_check_totcap_order(i) # 查资金订单
b._check_totcap(ct, )
# for i in range(100, 220):
# ct = o.create_check_totcap_order(i) # 查资金订单
# b._check_totcap(ct)
# b.paccount()
# print()
# b.paccounts(tail=5)
return ohlc, ind, o, b
duanqs