Skip to content

Commit

Permalink
Add files via upload
Browse files Browse the repository at this point in the history
  • Loading branch information
HaoningChen authored Dec 27, 2023
1 parent 0bcbad4 commit dd21383
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 101 deletions.
18 changes: 10 additions & 8 deletions scutquant/account.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class Account:

def __init__(self, init_cash: float, position: dict, available: dict, init_price: dict):
self.cash = init_cash # 可用资金
self.cash_available = deepcopy(init_cash)
# self.cash_available = deepcopy(init_cash)
self.position = position # keys应包括所有资产,如无头寸则值为0,以便按照keys更新持仓
self.available = available # 需要持有投资组合的底仓,否则按照T+1制度无法做空
self.price = init_price # 资产价格
Expand All @@ -39,7 +39,7 @@ def __init__(self, init_cash: float, position: dict, available: dict, init_price
self.turnover = []
self.trade_value = 0.0

def generate_total_order(self, order: dict, freq: int) -> dict:
def adjust_order(self, order: dict, freq: int) -> dict:
order_offset = self.auto_offset(freq)
if order_offset is not None:
for key in order_offset["buy"].keys():
Expand All @@ -55,8 +55,8 @@ def generate_total_order(self, order: dict, freq: int) -> dict:
order["sell"][key] += order_offset["sell"][key]
return order

def check_order(self, order: dict, price: dict, cost_rate: float = 0.0015, min_cost: float = 5) -> \
tuple[dict, bool]: # 检查是否有足够的资金完成order, 如果不够则不买
def check_order(self, order: dict, price: dict, cost_rate: float = 0.0015, min_cost: float = 5) -> dict:
# 检查是否有足够的资金完成order, 如果不够则调整订单(sell不变, buy按比例减少)
cash_inflow = 0.0
cash_outflow = 0.0
order_copy = deepcopy(order)
Expand All @@ -74,11 +74,13 @@ def check_order(self, order: dict, price: dict, cost_rate: float = 0.0015, min_c
order["buy"].pop(code)
cost = max(min_cost, (cash_inflow + cash_outflow) * cost_rate)
cash_needed = cash_outflow - cash_inflow + cost
# print("cash_needed: ", cash_needed, "cash: ", self.cash)
if cash_needed > self.cash:
return order, False
else:
return order, True
# c_n = buy + r(buy + sell) - sell > c, 令n * buy + r(n * buy + sell) - sell = c
# 则n * buy * (1 + r) - (1 - r) * sell = c, 即 n * buy * (1 + r) = c + (1 - r)sell
# n = (c + (1 - r)sell) / buy(1 + r)
ratio = (self.cash + (1 - cost_rate) * cash_inflow) / (cash_outflow * (1 + cost_rate))
order["buy"] = {k: int(v * ratio + 0.5) for k, v in order["buy"].items()} # 这样虽然不是整手下单, 但只要在1手以上都没问题
return order

def update_price(self, price: dict): # 更新市场价格
for code in price.keys():
Expand Down
10 changes: 5 additions & 5 deletions scutquant/alpha.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,11 @@ def market_neutralize(x: pd.Series, long_only: bool = False) -> pd.Series:
"""
_mean = x.groupby(level=0).mean()
x -= _mean
if long_only: # 考虑到A股有做空限制, 因此将权重为负的股票(即做空的股票)的权重调整为0(即纯多头), 并相应调整多头的权重
x[x.values < 0] = 0
abs_sum = x[x.values > 0].groupby(level=0).sum()
else:
abs_sum = abs(x).groupby(level=0).sum()
abs_sum = abs(x).groupby(level=0).sum()
x /= abs_sum
if long_only:
x[x < 0] = 0
x *= 2
return x


Expand Down Expand Up @@ -119,6 +118,7 @@ def call(self):
pass

def normalize(self):
self.result = mad_winsor(inf_mask(self.result))
if self.norm_method == "zscore":
self.result = cs_zscore(self.result)
elif self.norm_method == "robust_zscore":
Expand Down
86 changes: 64 additions & 22 deletions scutquant/executor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,43 @@
from . import account, strategy
from .signal_generator import *
import pandas as pd
import numpy as np
import warnings

warnings.filterwarnings("ignore")


def get_daily_inter(data: pd.Series | pd.DataFrame, shuffle=False):
daily_count = data.groupby(level=0).size().values
daily_index = np.roll(np.cumsum(daily_count), 1)
daily_index[0] = 0
if shuffle:
daily_shuffle = list(zip(daily_index, daily_count))
np.random.shuffle(daily_shuffle)
daily_index, daily_count = zip(*daily_shuffle)
return daily_index, daily_count


def prepare(predict: pd.DataFrame, data: pd.DataFrame, price: str, volume: str, real_ret: pd.Series) -> pd.DataFrame:
"""
:param predict: pd.DataFrame, 预测值, 应包括"predict"
:param data: pd.DataFrame, 提供时间和价格信息
:param price: str, data中表示价格的列名
:param volume: str, data中表示成交量的列名
:param real_ret: pd.Series, 真实收益率
:return: pd.DataFrame
"""
data_ = data.copy()
predict.columns = ["predict"]
index = predict.index
data1 = data_[data_.index.isin(index)]
data1 = data1.reset_index()
data1 = data1.set_index(predict.index.names).sort_index()
predict["price"] = data1[price]
predict["volume"] = data1[volume] # 当天的交易量, 假设交易量不会发生大的跳跃
predict.index.names = ["time", "code"]
predict["price"] = predict["price"].groupby(["code"]).shift(-1) # 指令是T时生成的, 但是T+1执行, 所以是shift(-1)
predict["R"] = real_ret[real_ret.index.isin(predict.index)] # 本来就是T+2对T+1的收益率, 因此不用前移
return predict.dropna()


class Executor:
Expand Down Expand Up @@ -61,18 +99,17 @@ def create_account(self):
self.benchmark = account.Account(self.ben_cash, {}, {}, self.price.copy())

def get_cash_available(self):
self.value_hold = 0.0
for code in self.user_account.price.keys(): # 更新持仓市值, 如果持有的资产在价格里面, 更新资产价值
if code in self.user_account.position.keys():
self.value_hold += self.user_account.position[code] * self.user_account.price[code]
# value是账户总价值, 乘risk_deg后得到所有可交易资金, 减去value_hold就是剩余可交易资金
return self.user_account.value * self.s.risk_degree - self.value_hold
"""
fixme: 调整计算方式使其适应先卖后买的情况
之所以不用cash * risk_degree是因为先卖后买的情况下, 当前的cash跟实际可支配的cash不一样(因为卖了就有钱了)
"""
return self.user_account.value * self.s.risk_degree

def execute(self, data: pd.DataFrame, verbose: int = 0):
"""
:param data: pd.DataFrame, 包括三列:'predict', 'volume', 'price', 'label' 以及多重索引[('time', 'code')]
:param verbose: int, 是否输出交易记录
:return: self
:return:
"""

def check_names(index=data.index, predict="predict", price="price"):
Expand All @@ -88,26 +125,31 @@ def check_names(index=data.index, predict="predict", price="price"):
self.init_account(data)
self.create_account()
if self.mode == "generate":
time = data.index.get_level_values(0).unique().values
for t in time:
idx = data["R"].groupby(data.index.names[0]).mean() # 大盘收益率
self.time.append(t)
data_select = data[data.index.get_level_values(0) == t]
signal = generate(data=data_select, strategy=self.s, cash_available=self.get_cash_available())
order, current_price = signal["order"], signal["current_price"]
benchmark = data["R"].groupby(level=0).transform(lambda x: x.mean()) # 大盘收益率
daily_idx, daily_count = get_daily_inter(data)
for idx, count in zip(daily_idx, daily_count):
batch = slice(idx, idx + count)
data_batch = data.iloc[batch]
benchmark_batch = benchmark.iloc[batch]
current_day = data_batch.index.get_level_values(0)[0]
self.time.append(current_day)
order, current_price = self.s.to_signal(data_batch, position=self.user_account.position,
cash_available=self.get_cash_available())

if self.s.auto_offset:
order = self.user_account.generate_total_order(order=order, freq=self.s.offset_freq)
order, trade = self.user_account.check_order(order, current_price)

if verbose == 1 and trade:
print(t, '\n', "buy:", '\n', order["buy"], '\n', "sell:", order["sell"], '\n')
order = self.user_account.adjust_order(order=order, freq=self.s.offset_freq)
order = self.user_account.check_order(order, current_price)
# print(trade)
# trade = True
if verbose == 1:
print(current_day, '\n', "buy:", '\n', order["buy"], '\n', "sell:", order["sell"], '\n')

self.user_account.update_all(order=order, price=current_price, cost_buy=self.cost_buy,
cost_sell=self.cost_sell, min_cost=self.min_cost, trade=trade)
cost_sell=self.cost_sell, min_cost=self.min_cost)
self.user_account.risk_control(risk_degree=self.s.risk_degree, cost_rate=self.cost_sell,
min_cost=self.min_cost)
self.benchmark.value *= (1 + idx[idx.index == t][0]) # 乘上1+大盘收益率, 相当于等权指数

self.benchmark.value *= 1 + benchmark_batch.values[0] # 乘上1+大盘收益率, 相当于等权指数
self.benchmark.val_hist.append(self.benchmark.value)
else:
raise ValueError("simulate mode is not available by far")
67 changes: 32 additions & 35 deletions scutquant/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,15 @@ def plot(data, label, title: str = None, xlabel: str = None, ylabel: str = None,
plt.figure(figsize=figsize)
# plt.clf()
if mode == "plot":
for d in range(len(data)):
plt.plot(data[d], label=label[d])
plt.xticks(rotation=45)
if len(data) > 10:
cmap = plt.get_cmap('tab20')
for d in range(len(data)):
plt.plot(data[d], label=label[d], color=cmap(d))
plt.xticks(rotation=45)
else:
for d in range(len(data)):
plt.plot(data[d], label=label[d])
plt.xticks(rotation=45)
elif mode == "bar":
bar = plt.bar(label, data, label="value")
plt.bar_label(bar, label_type='edge')
Expand Down Expand Up @@ -122,16 +128,13 @@ def accuracy(pred: pd.Series, y: pd.Series, sign: str = ">=") -> float:
return len(data_true) / len(data)


def report_all(user_account, benchmark, show_raw_value: bool = False, excess_return: bool = True, risk: bool = True,
turnover: bool = True, rf: float = 0.03, freq: float = 1, time=None, figsize: tuple = (10, 6)) -> None:
def report_all(user_account, benchmark, show_raw_value: bool = False, rf: float = 0.03, freq: float = 1, time=None,
figsize: tuple = (10, 6)) -> None:
"""
:param user_account: account类
:param benchmark: account类
:param show_raw_value: 显示原始市值(具体金额)
:param excess_return: 显示超额收益曲线
:param risk: 显示风险度
:param turnover: 显示换手率
:param rf: 显示无风险利率
:param freq: 频率, 日频为1,月频为30,其它类推
:param time: 显示时间轴
Expand Down Expand Up @@ -159,20 +162,22 @@ def report_all(user_account, benchmark, show_raw_value: bool = False, excess_ret
days += 1
days /= len(acc_ret)

acc_dd = calc_drawdown(pd.Series(acc_ret))
ben_dd = calc_drawdown(pd.Series(ben_ret))
acc_ret = pd.Series(acc_ret, name="acc_ret", index=time) # 累计收益率
ben_ret = pd.Series(ben_ret, name="ben_ret", index=time) # benchmark的累计收益率
excess_ret = pd.Series(excess_ret, name="excess_ret", index=time)

ret = pd.Series(acc_ret) # 累计收益率
ben = pd.Series(ben_ret) # benchmark的累计收益率
acc_dd = calc_drawdown(acc_ret)
ben_dd = calc_drawdown(ben_ret)
excess_dd = calc_drawdown(excess_ret)

ann_return = annualized_return(ret, freq=freq)
ann_std = annualized_volatility(ret, freq=freq)
ben_ann_return = annualized_return(ben, freq=freq)
ben_ann_std = annualized_volatility(ben, freq=freq)
ann_return = annualized_return(acc_ret, freq=freq)
ann_std = annualized_volatility(acc_ret, freq=freq)
ben_ann_return = annualized_return(ben_ret, freq=freq)
ben_ann_std = annualized_volatility(ben_ret, freq=freq)

beta = ret.cov(ben) / ben.var()
alpha = ret.mean() - beta * ben.mean()
epsilon = pd.Series(ret - beta * ben - alpha).std()
beta = acc_ret.cov(ben_ret) / ben_ret.var()
alpha = acc_ret.mean() - beta * ben_ret.mean()
epsilon = (acc_ret - beta * ben_ret - alpha).std()

sharpe = sharpe_ratio(acc_ret, rf=rf, freq=freq * 365)
sortino = sortino_ratio(acc_ret, ben_ret)
Expand Down Expand Up @@ -207,10 +212,6 @@ def report_all(user_account, benchmark, show_raw_value: bool = False, excess_ret
plt.legend()
plt.show()
else:
acc_ret = pd.Series(acc_ret, name="acc_ret", index=time)
ben_ret = pd.Series(ben_ret, name="ben_ret", index=time)
excess_ret = pd.Series(excess_ret, name="excess_ret", index=time)

plt.figure(figsize=(10, 6))
plt.plot(acc_ret, label="return", color="red")
plt.plot(ben_ret, label="benchmark", color="blue")
Expand All @@ -222,21 +223,19 @@ def report_all(user_account, benchmark, show_raw_value: bool = False, excess_ret
plt.clf()
plt.figure(figsize=(10, 6))
plt.plot(acc_dd, label="drawdown")
plt.plot(ben_dd, label="excess_return_drawdown")
plt.plot(excess_dd, label="excess_return_drawdown")
plt.legend()
plt.title("Drawdown")
plt.show()

if risk:
risk = pd.DataFrame({'risk': user_account.risk_curve}, index=time)
plot([risk], label=['risk_degree'], title='Risk Degree', ylabel='value', figsize=figsize)
risk = pd.DataFrame({'risk': user_account.risk_curve}, index=time)
plot([risk], label=['risk_degree'], title='Risk Degree', ylabel='value', figsize=figsize)

if turnover:
risk = pd.DataFrame({'turnover': user_account.turnover}, index=time)
plot([risk], label=['turnover'], title='Turnover', figsize=figsize)
risk = pd.DataFrame({'turnover': user_account.turnover}, index=time)
plot([risk], label=['turnover'], title='Turnover', figsize=figsize)


def group_return_ana(pred: pd.DataFrame | pd.Series, y_true: pd.Series, n: int = 5, groupby: str = "time",
def group_return_ana(pred: pd.DataFrame | pd.Series, y_true: pd.Series, n: int = 10, groupby: str = "time",
figsize: tuple = (10, 6)) -> None:
"""
因子对股票是否有良好的区分度, 若有, 则应出现明显的分层效应(即单调性)
Expand Down Expand Up @@ -283,12 +282,10 @@ def group_return_ana(pred: pd.DataFrame | pd.Series, y_true: pd.Series, n: int =
win_rate = []
mean_ret = []
for c in cols:
# dt = t_df[c] + 1
# data.append(dt.cumprod() - 1)
data.append(t_df[c].cumsum())
label.append(c)
win_rate.append(len(t_df[t_df[c] >= 0]) / len(t_df))
mean_ret.append(t_df[c].cumsum().values[-1] / len(t_df) * 100)
win_rate.append(round(len(t_df[t_df[c] >= 0]) / len(t_df), 4))
mean_ret.append(round(t_df[c].cumsum().values[-1] / len(t_df) * 100, 4))
plot(data, label, title='Grouped Return', xlabel='time_id', ylabel='value', figsize=figsize)
plot(win_rate, label=cols, title="Win Rate of Each Group", mode="bar", figsize=figsize)
plot(mean_ret, label=cols, title="Mean Return of Each Group(%)", mode="bar", figsize=figsize)
Expand Down
Loading

0 comments on commit dd21383

Please sign in to comment.