diff --git a/回测/CreateConbinations.py b/回测/CreateConbinations.py new file mode 100755 index 0000000..8925a69 --- /dev/null +++ b/回测/CreateConbinations.py @@ -0,0 +1 @@ +# 创建组合 \ No newline at end of file diff --git a/回测/EmailTest.py b/回测/EmailTest.py new file mode 100755 index 0000000..91faee5 --- /dev/null +++ b/回测/EmailTest.py @@ -0,0 +1,88 @@ +import imaplib +import smtplib +import email +from email.header import decode_header +from email.mime.text import MIMEText +from imapclient import IMAPClient + +def send_email(subject, body, to_email): + # 这个函数名为send_email,它接受三个参数: + # subject(邮件主题)、body(邮件内容)和to_email(收件人的电子邮件地址)。 + from_email = 'yizeguo1@126.com' + + mail_pass = 'CHRIZKWQSRWYLBOL'# 126授权码 + # mail_pass = 'pwvzuqbiysqshgha'# qq授权码 + + msg = MIMEText(body) + msg['Subject'] = subject + msg['From'] = from_email + msg['To'] = to_email + + server = smtplib.SMTP('smtp.126.com', 25) + # server = smtplib.SMTP('smtp.qq.com', 465) + server.starttls() + server.login(from_email, mail_pass) + server.sendmail(from_email, to_email, msg.as_string()) + server.quit() + + +def get_latest_email_body(to_email): + # 连接到126邮箱的IMAP服务器 + mail = IMAPClient("imap.126.com") + + from_email = 'yizeguo1@126.com' + mail_pass = 'CHRIZKWQSRWYLBOL'# 126授权码 + mail.login(from_email, mail_pass) + mail.id_({"name": "IMAPClient", "version": "2.1.0"}) + + # mail.list_folders() + print(mail.list_folders()) + # 选择收件箱 + messages = mail.select_folder(folder="inbox", readonly=True) + + # 搜索发件人为指定邮箱的所有邮件 + messages = mail.search(None, f'(FROM "{to_email}")') + + # 获取邮件ID列表 + mail_ids = messages[0].split() + if not mail_ids: + print("No emails found.") + return None + + # 获取最新一封邮件的ID + latest_email_id = mail_ids[-1] + + # 获取最新一封邮件的数据 + status, msg_data = mail.fetch(latest_email_id, "(RFC822)") + + for response_part in msg_data: + if isinstance(response_part, tuple): + # 解析邮件 + msg = email.message_from_bytes(response_part[1]) + + # 获取纯文本邮件正文 + if msg.get_content_type() == "text/plain": + body = msg.get_payload(decode=True).decode() + print("Body:", body) + return body + + # 关闭连接并登出 + mail.close() + mail.logout() + return None + + +if __name__ == "__main__": + + # send_email("测试", 'test', "guoyize2209@163.com") + # id_info = { + # 'name': 'myname', + # 'version': '1.0.0', + # 'vendor': 'myclient', + # 'support-email': 'yizeguo1@126.com' + # } + body = get_latest_email_body("guoyize2209@163.com") + if body: + print("Latest email body:", body) + else: + print("No plain text email found.") \ No newline at end of file diff --git a/回测/TurtleClassNew.py b/回测/TurtleClassNew.py new file mode 100755 index 0000000..e75d99b --- /dev/null +++ b/回测/TurtleClassNew.py @@ -0,0 +1,861 @@ +import numpy as np +import math +import akshare as ak +import os +from datetime import datetime, timedelta +import pandas as pd +import mplfinance as mpf + +def CalTrueFluc(data, day): + + H_L = data.iloc[day]['最高'] - data.iloc[day]['最低'] + H_PDC = data.iloc[day]['最高'] - data.iloc[day-1]['收盘'] + PDC_L = data.iloc[day-1]['收盘'] - data.iloc[day]['最低'] + TrueFluc = np.max([H_L, H_PDC, PDC_L]) + print('high', data.iloc[day]['最高'], 'low', data.iloc[day]['最低'], 'TrueRange', TrueFluc) + + return TrueFluc + +def calc_sma_atr_pd(kdf,period): + """计算TR与ATR + + Args: + kdf (_type_): 历史数据 + period (_type_): ATR周期 + + Returns: + _type_: 返回kdf,增加TR与ATR列 + """ + kdf['HL'] = kdf['最高'] - kdf['最低'] + kdf['HC'] = np.abs(kdf['最高'] - kdf['收盘'].shift(1)) + kdf['LC'] = np.abs(kdf['最低'] - kdf['收盘'].shift(1)) + kdf['TR'] = np.round(kdf[['HL','HC','LC']].max(axis=1), 3) + # ranges = pd.concat([high_low, high_close, low_close], axis=1) + # true_range = np.max(ranges, axis=1) + kdf['ATR'] = np.round(kdf['TR'].rolling(period).mean(), 3) + + return kdf.drop(['HL','HC','LC'], axis = 1) + +# A股数据 东方财富网 +# all_data = ak.stock_zh_a_spot_em() + +# 基金实时数据 东方财富网 +# fund_etf_spot_em_df = ak.fund_etf_spot_em() + +# 后复权历史数据 +# fund_etf_hist_em_df = ak.fund_etf_hist_em(symbol="513300", period="daily", start_date="20130101", end_date="20240408", adjust="hfq") +# fund_etf_hist_em_df.to_csv('513300data.csv', index=False) + +# data = pd.read_csv('513300data.csv') + + +# # 一、计算头寸规模 + +# # 真实波动幅度 = max (H-L, H-pdc, pdc-L) + +# today = datetime.today() +# # print(today) + +# # print(data.iloc[-1]['成交额']) + +# TrueFlucs = [] +# Nserious = np.zeros(101) +# last120days = np.arange(-120, -100) +# for i in last120days: +# H_L = data.iloc[i]['最高'] - data.iloc[i]['最低'] +# H_PDC = data.iloc[i]['最高'] - data.iloc[i-1]['收盘'] +# PDC_L = data.iloc[i-1]['收盘'] - data.iloc[i]['最低'] +# TrueFlucs.append(np.max([H_L, H_PDC, PDC_L])) + +# # 求简单平均,放入N序列第一个 +# Nsimple = np.average(TrueFlucs) +# Nserious[0] = Nsimple +# # 计算-21到-1的N +# last100days = np.arange(-100, 0) + +# for i in range(0,100): +# day = last100days[i] +# H_L = data.iloc[day]['最高'] - data.iloc[day]['最低'] +# H_PDC = data.iloc[day]['最高'] - data.iloc[day-1]['收盘'] +# PDC_L = data.iloc[day-1]['收盘'] - data.iloc[day]['最低'] +# TrueFluc = np.max([H_L, H_PDC, PDC_L]) + +# Ntemp = (19 * Nserious[i] + TrueFluc)/20 +# Nserious[i+1] = Ntemp + +# # print(Nserious) + + +# total_rows = len(data) +# Ndata = np.zeros(total_rows) +# Ndata[total_rows-101:] = Nserious +# # NewColumn = [0]*(total_rows-101) + Nserious +# data['N'] = Ndata + +# data.to_csv('513300data-N.csv', index=False) +# pass + + +# -----------------------更新atr---------------------- + """已有数据与新数据对比,补充新的N,同时更新数据库 + """ + +# Today = datetime.today() +# # print(Today) +# formatted_date = Today.strftime("%Y%m%d") + +# # print(formatted_date) +# CurrentData = ak.fund_etf_hist_em(symbol="513300", period="daily", start_date="20130101", end_date=formatted_date, adjust="hfq") + +# CurrentData = calc_sma_atr_pd(CurrentData, 20) +# CurrentData.to_csv('513300data-N.csv', index=False) +# pass + + +# ------------------计算头寸规模 资金10w, 1%波动------------ + +# money = 100000 +# OldData = pd.read_csv('513300data-N.csv') + +# N = OldData.iloc[-1]['ATR'] +# # N = 0.473 +# Price = OldData.iloc[-1]['收盘'] +# # Price = 5.60 +# EveryUnit = 0.0025 * money /(N*100*Price) + +# print('单位',EveryUnit) + +# print(113*100*Price) + + +class TurtleTrading(object): + """对象范围较小,对某一个标的创建一个海龟,如513300, + 计算ATR、 + Position Size, + 买入、卖出、加仓等行为 + Args: + object (_type_): _description_ + """ + def __init__(self, TradeCode) -> None: + self.TradeCode = TradeCode + + def GetRecentData(self): + Today = datetime.today() + # print(Today) + formatted_date = Today.strftime("%Y%m%d") + + # print(formatted_date) + Code = f"{self.TradeCode}" + CurrentData = ak.fund_etf_hist_em(symbol=Code, period="daily", start_date="20130101", end_date=formatted_date, adjust="") + return CurrentData + + def CalATR(self, data, ATRday, SaveOrNot): + """计算某个标的的ATR,从上市日到今天, 计算后的数据保存在self.CurrentData + + Args: + ATRday: 多少日ATR + SaveOrNot (_type_): 是否保存.csv数据 + """ + + self.CurrentData = calc_sma_atr_pd(data, ATRday) + self.N = self.CurrentData['ATR'] + if SaveOrNot: + self.CurrentData.to_csv('513300data-N.csv', index=False) + print("csv保存成功") + + return self.N + + + def CalPositionSize(self, RiskCoef, Capital): + """计算PosizionSize 持有的单位,该单位某标的,1N波动对应RiskCoef * Capital资金 + + Args: + RiskCoef (_type_): 风险系数 + Capital (_type_): 资金 + """ + + N = self.CurrentData.iloc[-1]['ATR'] + # N = 0.473 + Price = self.CurrentData.iloc[-1]['收盘'] + # Price = 5.60 + self.PositionSize = RiskCoef * Capital /( N*100*Price) # 默认用股票形式了 100 + + + return self.PositionSize + + + def ReadExistData(self, data): + """除了通过发请求获取数据,也可以读本地的数据库,赋值给self.CurrentData + + Args: + data (_type_): 本地csv名称 + """ + self.CurrentData = pd.read_csv(data) + + def DrawKLine(self, days): + """画出k线图看看,画出最近days天的K线图 + """ + + # 日期部分 + + dates = pd.to_datetime(self.CurrentData['日期'][-days:]) + # Klinedf['Data'] = pd.to_datetime(self.CurrentData['日期']) + Klinedf = pd.DataFrame() + # Klinedf.set_index = Klinedf['Data'] + + # 其他数据 + Klinedf['Open'] = self.CurrentData['开盘'][-days:] + Klinedf['High'] = self.CurrentData['最高'][-days:] + Klinedf['Low'] = self.CurrentData['最低'][-days:] + Klinedf['Close'] = self.CurrentData['收盘'][-days:] + Klinedf['Volume'] = self.CurrentData['成交量'][-days:] + + Klinedf.set_index(dates, inplace=True) + # 画图 + mpf.plot(Klinedf, type='candle', style='yahoo', volume=False, mav=(5,), addplot=[mpf.make_addplot(self.Donchian[['Upper', 'Lower']])]) + + def calculate_donchian_channel(self, days, n): + """ + 计算唐奇安通道days一共多少日, n多少日唐奇安 + + 参数: + self.CurrentData (DataFrame): 包含价格数据的Pandas DataFrame,至少包含"High"和"Low"列 + n (int): 时间周期 + + 返回:self.Donchian + DataFrame: 唐奇安通道的DataFrame,包含"Upper", "Lower", 和 "Middle"列 + """ + Donchian = pd.DataFrame() + # 计算最高价和最低价的N日移动平均线 + Donchian['Upper'] = self.CurrentData['最高'][-days:].rolling(n).max() + Donchian['Lower'] = self.CurrentData['最低'][-days:].rolling(n).min() + + # # 计算中间线 + # Donchian['Middle'] = (self.Donchian['Upper'] + self.Donchian['Lower']) / 2 + + return Donchian + +class Trade(object): + """具有以下功能: + 接收Turtle Class作为输入 + 1 数据准备: + a 获取数据 + b 计算atr + c 计算55日、20日、10日Donchian + + 2 总持有单位判断 + 3 定义系统2 超过55日xxx,分段加仓 + 4 回测功能 + 5 实时功能 问题实时功能如何同时监控几个item + + 78 + + Args: + object (_type_): _description_ + """ + def __init__(self, turtles, riskcoe, cash, StartTime, EndTime) -> None: + """接收所有的turtles + + Args: + turtles (_type_): _description_ + """ + self.turtles = turtles + self.riskcoe = riskcoe + self.cash = cash + self.Capital = cash + + self.StartTime = StartTime + self.EndTime = EndTime + self.TrigerTime = 0 + # self.BuyStates = { + # "trigger_count": 0, # 触发次数 + # "BuyPrice": None, # 买入/持有价格 + # "StopPrice":None, # 止损价格 + # "quantity": 0, # 持有份数 + # "N": 0, # ATR + # "available_cash": self.cash # 可用资金 + # } + # 0"trigger_count", 1"BuyPrice", 2"StopPrice", 3"quantity", 4"N", 5"available_cash" + self.BuyStates = [[0, None, None, 0, 0, self.cash]] + + self.tradeslog = [] # 交易记录 + + self.current_week = None # 当前周数 + # def TurtleDataPre(self): + # for turtle in self.turtles: + # turtle.CalATR(20, True) + + # turtle.Donchian20 = turtle.calculate_donchian_channel(500, 20) + # turtle.Donchian10 = turtle.calculate_donchian_channel(500, 10) + # turtle.Donchian55 = turtle.calculate_donchian_channel(500, 55) + # pass + + def PortfolioPositon(self): + """总共能持有多少个单位 + """ + pass + + def TestBuyStocks(self, PriceNow, date): + # 回测中的买入函数 如果开盘价大于55日,最高价大于四份价格,执行加满 + # 返回self.BuyStates + # 实盘中应该是触发买入信号,发送买入邮件,价格,份数。当前Turtle程序暂停,收到邮件返回,更新买入价格,份数 + + # 更新BuyStates:"触发次数"、"买入/持有价格"、"持有份数"、"N"、"可用资金" + N = self.ThisWeekN + Shares = self.ThisWeekPosizionSize + + + # 更新 BuyStates + if self.TrigerTime == 0: # 第一次买入是直接修改 + # if self.BuyStates[0] == 0: + + self.TrigerTime += 1 + BuyPrice = PriceNow + AddPrice = PriceNow + 1/2 * N + StopPrice = PriceNow - 2 * N + available_cash = self.cash - Shares * BuyPrice + + # 0"trigger_count", 1"BuyPrice", 2"AddPrice", 3"StopPrice", 4"quantity", 5"N", 6"available_cash" + self.BuyStates = [[self.TrigerTime, BuyPrice, AddPrice, StopPrice, Shares, N, available_cash]] + + # 更新log + # + AllShares = sum(row[4] for row in self.BuyStates) + NetValue = available_cash + AllShares * BuyPrice + self.tradeslog.append([date, 'Buy', Shares, PriceNow, N, available_cash, NetValue]) + + else: # 加仓的操作,在BuyStates后边追加 + self.TrigerTime += 1 + BuyPrice = PriceNow + AddPrice = PriceNow + 1/2 * N + StopPrice = PriceNow - 2 * N + available_cash = self.BuyStates[self.TrigerTime-2][6] - Shares * BuyPrice + + self.BuyStates.append([self.TrigerTime, BuyPrice, AddPrice, StopPrice, Shares, N, available_cash]) + + # 更新log + AllShares = sum(row[4] for row in self.BuyStates) + NetValue = available_cash + AllShares * BuyPrice + self.tradeslog.append([date, 'Buy', Shares, PriceNow, N, available_cash, NetValue]) + + pass + + + + def TestStopSaleStocks(self, PriceNow, date): + # 回测中的卖出函数,仓位全卖 + N = self.ThisWeekN + + + # Shares应该等于所有持仓的和 + Shares = sum(row[4] for row in self.BuyStates) + # Shares = sum(self.BuyStates[:, 4]) + available_cash = self.BuyStates[-1][6] + Shares * PriceNow + # 更新log + NetValue = available_cash + self.tradeslog.append([date, 'StopSale', Shares, PriceNow, N, available_cash, NetValue]) + # self.trades.append((date, 'Sale', Shares, PriceNow, N, available_cash)) + + + # TrigerTime归0 + self.TrigerTime = 0 + # self.cash更新 + self.cash = available_cash + # 回到初始状态 + self.BuyStates = [[0, None, None, None, 0, 0, self.cash]] + + def TestOutSaleStocks(self, PriceNow, date): + # 回测中的卖出函数,仓位全卖 + N = self.ThisWeekN + + + # Shares应该等于所有持仓的和 + Shares = sum(row[4] for row in self.BuyStates) + # Shares = sum(self.BuyStates[:, 4]) + available_cash = self.BuyStates[-1][6] + Shares * PriceNow + # 更新log + NetValue = available_cash + self.tradeslog.append([date, 'OutSale', Shares, PriceNow, N, available_cash, NetValue]) + # self.trades.append((date, 'Sale', Shares, PriceNow, N, available_cash)) + + + # TrigerTime归0 + self.TrigerTime = 0 + # self.cash更新 + self.cash = available_cash + # 回到初始状态 + self.BuyStates = [[0, None, None, None, 0, 0, self.cash]] + + + def system2Enter(self, PriceNow, TempDonchian55Upper): + """以50日突破为基础的较简单的长线系统 + 入市:价格超过了前55日的最高价或最低价就建立头寸。 + - 如果价格超过55日最高价,买入一个单位建立多头头寸。 + - 如果价格跌破55日最低价,卖出一个单位建立空头头寸。 + 退出: + + 增加单位:突破时只建立一个单位,建立后以1/2N的间隔增加头寸,以前面指令的实际成交价为基础。实际成交价+1/2N + 单个品种,最大4个单位。 + + 以多头编写 + """ + + # # 触发次数 + # Trigertime = 0 + + # 只有没买入过,且 现价超过55日最高价,是一次突破,直接以突破价格买入--没有持仓且价格向上突破 + if self.TrigerTime == 0 and PriceNow > TempDonchian55Upper[-1]: + # 买入 + return True + + else: + return False + + + def system1EnterNormal(self, PriceNow, TempDonchian20Upper, BreakOutLog): + # 没有持仓且价格向上突破---此时包含两种情形:1 对某标的首次使用系统,2 已发生过突破,此时上次突破天然是失败的 + if self.TrigerTime == 0 and PriceNow > TempDonchian20Upper[-1]: + # 买入 + return True + elif self.TrigerTime != 0 and PriceNow > TempDonchian20Upper[-1]: + self.system1BreakoutValid(PriceNow) + if BreakOutLog[-1][5] == 'Lose': # TT!= 0且突破且上一次突破unseccessful + return True + else: + return False + else: + return False + + def system1EnterSafe(self, PriceNow, TempDonchian55Upper): + + if PriceNow > TempDonchian55Upper[-1]: # 保底的55日突破 + return True + else: + return False + + def system1BreakoutValid(self, priceNow): + """判断前一次突破是否成功,是log[-1][5]写入“win”,否则写入“Lose” + """ + if priceNow < self.BreakOutLog[-1][3]: + self.BreakOutLog[-1][5] = 'Lose' + else: + self.BreakOutLog[-1][5] = 'None' + pass + + def system2Out(self, PriceNow, TempDonchian20Lower): + # 退出:低于20日最低价(多头方向),空头以突破20日最高价为止损价格--有持仓且价格向下突破 + if self.TrigerTime != 0 and PriceNow < TempDonchian20Lower[-1]: + # 退出 + return True + + else: + return False + + def system1Out(self, PriceNow, TempDonchian10Lower): + # 退出:低于20日最低价(多头方向),空头以突破20日最高价为止损价格--有持仓且价格向下突破 + if self.TrigerTime != 0 and PriceNow < TempDonchian10Lower[-1]: + # 退出 + return True + + else: + return False + + def system2Add(self, PriceNow): + """加仓判断:如果当前价格>上一次买入后的加仓价格则加仓 + """ + if self.TrigerTime < 4 and PriceNow > self.BuyStates[self.TrigerTime - 1][2]: + # 买入 + return True + else: + return False + + def system2Stop(self, PriceNow): + """止损判断:如果当前价格<上一次买入后的止损价格则止损 + """ + if PriceNow < self.BuyStates[self.TrigerTime - 1][3]: + # 买入 + return True + else: + return False + + def system2CalDonchian(self): + # 按照system2的要求计算唐奇安通道上下边界--55日上界,20日下界 + + Donchian = pd.DataFrame() + # 计算最高价和最低价的N日移动平均线 + Donchian['Upper'] = self.RecentAlldata['最高'].rolling(55).max() + Donchian['Lower'] = self.RecentAlldata['最低'].rolling(20).min() + + # # 计算中间线 + # Donchian['Middle'] = (self.Donchian['Upper'] + self.Donchian['Lower']) / 2 + + return Donchian + + def system1CalDonchian(self): + # 按照system2的要求计算唐奇安通道上下边界--55日上界,20日下界 + + Donchian = pd.DataFrame() + # 计算最高价和最低价的N日移动平均线 + Donchian['Upper55'] = self.RecentAlldata['最高'].rolling(55).max() + Donchian['Upper20'] = self.RecentAlldata['最高'].rolling(20).max() + Donchian['Lower'] = self.RecentAlldata['最低'].rolling(10).min() + + # # 计算中间线 + # Donchian['Middle'] = (self.Donchian['Upper'] + self.Donchian['Lower']) / 2 + + return Donchian + + def CalPositionSize(self, N, price): + PositionSize = self.riskcoe * self.Capital /(N) # 默认用股票形式了 100 + IntPositionSize = int(PositionSize // 100) * 100 + return IntPositionSize + + def TestSys2Function(self): + """回测函数,对于某个标的, + 准备:获取价格数据,计算ATR,N,头寸单位,唐奇安通道上下边界 + 运行过程:按时间推进,开始时间-结束时间,按照系统1或2或组合形式,执行买入、加仓、退出、止损 + 操作记录:形成一个log + """ + + # ------------------准备阶段---------------- + # 1、获取标的价格 + self.RecentAlldata = self.turtles.GetRecentData() + self.RecentAlldata['日期'] = pd.to_datetime(self.RecentAlldata['日期']) + self.RecentAlldata.set_index('日期', inplace=True) + + # 2、计算ATR + self.N = self.turtles.CalATR(self.RecentAlldata, 20, None) + # self.PositionSize = self.turtles.CalPositionSize(self.riskcoe, self.cash) + # 3、计算唐奇安通道上下边界--system2 + self.Donchian = self.system2CalDonchian() + + # 开始、结束日期 + RowStart = self.RecentAlldata.index.get_loc(self.StartTime) + RowEnd = self.RecentAlldata.index.get_loc(self.EndTime) + + # 准备迭代使用的数据 + TempData = pd.DataFrame() # 存储最高、最低价等信息 + TempN = [] + TempDonchian55Upper = [] + TempDonchian20Lower = [] + # ------------------运行阶段---------------- + + for date, row in self.RecentAlldata[RowStart:RowEnd].iterrows(): + + # 增加一个迭代更新的数据 + TempData = pd.concat([TempData, row.to_frame().T], ignore_index=True) + + # day <=55,不执行任何操作,跳过,等待 + + # day < 55,<20刚开始运行,N、Donchian数据从总数据拿, 不用框时间,直接从总数据拿 + # if TempData.shape[0] < 20: + # 每天更新上下边界 + TempDonchian20Lower.append(self.Donchian['Lower'].iloc[RowStart + TempData.shape[0]-2]) + + # if TempData.shape[0] < 55: + TempDonchian55Upper.append(self.Donchian['Upper'].iloc[RowStart + TempData.shape[0]-2]) + + # 检查是否是新的一周 + if self.current_week is None or date.week != self.current_week: + self.current_week = date.week + + # 更新atr、头寸规模 + self.ThisWeekN = self.N.iloc[RowStart + TempData.shape[0]-2] + TempN.append(self.ThisWeekN) + + self.ThisWeekPosizionSize = self.CalPositionSize(self.ThisWeekN, TempData.iloc[-1]['收盘']) + + + # 如果空仓,判断当天开盘价是否突破,收盘价是否突破,突破则买入 + if self.TrigerTime == 0: + if self.system2Enter(row["开盘"], TempDonchian55Upper): + self.TestBuyStocks(row["开盘"], date) + + # 开盘突破后,最高价能否加仓? + # 价格差取整是能加仓几手,循环加仓 + delta = math.floor((row['最高']-row['开盘']) / (1/2 * self.ThisWeekN)) + if 1 <= delta <= 3: + for i in range(delta): + self.TestBuyStocks((row["开盘"] + (i+1) * 1/2 * self.ThisWeekN +0.001), date) + elif 3 < delta: + for i in range(3): + self.TestBuyStocks((row["开盘"] + (i+1) * 1/2 * self.ThisWeekN +0.001), date) + # 最高价突破,买入 + elif self.system2Enter(row["最高"], TempDonchian55Upper): + self.TestBuyStocks(TempDonchian55Upper[-1]+0.001, date) + + delta = math.floor((row['最高']-TempDonchian55Upper[-1]) / (1/2 * self.ThisWeekN)) + if 1 <= delta <= 3: + for i in range(delta): + self.TestBuyStocks((TempDonchian55Upper[-1] + (i+1) * 1/2 * self.ThisWeekN +0.001), date) + elif 3 < delta: + for i in range(3): + self.TestBuyStocks((TempDonchian55Upper[-1] + (i+1) * 1/2 * self.ThisWeekN +0.001), date) + + # 0"trigger_count", 1"BuyPrice", 2"AddPrice", 3"StopPrice", 4"quantity", 5"N", 6"available_cash" + # self.BuyStates = [self.TrigerTime, BuyPrice, AddPrice, StopPrice, Shares, N, available_cash] + elif 1 <= self.TrigerTime < 4: # 加仓1-3次,考虑止损和加仓 + # 开盘价加仓 + if self.system2Add(row["开盘"]): + self.TestBuyStocks(row["开盘"], date) + + if self.TrigerTime == 4: + # 满仓了,不能再加 + pass + # 开盘加仓后,最高价能否继续加仓? + else: + delta = math.floor((row['最高']-row['开盘']) / (1/2 * self.ThisWeekN)) + + if 1 <= delta <= 4-self.TrigerTime: + for i in range(delta): + self.TestBuyStocks((row["开盘"] + (i+1) * 1/2 * self.ThisWeekN +0.001), date) + + # 最高价加仓 + elif self.system2Add(row["最高"]): + self.TestBuyStocks(self.BuyStates[self.TrigerTime - 1][2] + 0.001, date) + + + if self.TrigerTime == 4: + # 满仓了,不能再加 + pass + # 开盘加仓后,最高价能否继续加仓? + else: + delta = math.floor((row['最高']-self.BuyStates[self.TrigerTime - 1][2]) / (1/2 * self.ThisWeekN)) + + if 1 <= delta <= 4-self.TrigerTime: + for i in range(delta): + self.TestBuyStocks((self.BuyStates[self.TrigerTime - 1][2] + (i+1) * 1/2 * self.ThisWeekN +0.001), date) + + + # 止损 + elif self.system2Stop(row["收盘"]): + self.TestStopSaleStocks(self.BuyStates[self.TrigerTime - 1][3] - 0.001, date) + elif self.system2Stop(row["最低"]): + self.TestStopSaleStocks(self.BuyStates[self.TrigerTime - 1][3] - 0.001, date) + # 止盈 + elif self.system2Out(row["收盘"], TempDonchian20Lower): + self.TestOutSaleStocks(TempDonchian20Lower[-1] - 0.001, date) + elif self.system2Out(row["最低"], TempDonchian20Lower): + self.TestOutSaleStocks(TempDonchian20Lower[-1] - 0.001, date) + + elif self.TrigerTime == 4: # 满仓 考虑止损和退出 + # 止损 + if self.system2Stop(row["收盘"]): + self.TestStopSaleStocks(self.BuyStates[self.TrigerTime - 1][3] - 0.001, date) + elif self.system2Stop(row["最低"]): + self.TestStopSaleStocks(self.BuyStates[self.TrigerTime - 1][3] - 0.001, date) + # 止盈 + elif self.system2Out(row["收盘"], TempDonchian20Lower): + self.TestOutSaleStocks(TempDonchian20Lower[-1] - 0.001, date) + elif self.system2Out(row["最低"], TempDonchian20Lower): + self.TestOutSaleStocks(TempDonchian20Lower[-1] - 0.001, date) + + # 交易日结束,重新计算高低突破点: + + print("---------------回测结束,回测日志如下----------------") + + for sublist in self.tradeslog: + print(sublist) + + + def TestSys1Function(self): + """回测函数,对于某个标的, + 准备:获取价格数据,计算ATR,N,头寸单位,唐奇安通道上下边界 + 运行过程:按时间推进,开始时间-结束时间,按照系统1或2或组合形式,执行买入、加仓、退出、止损 + 操作记录:形成一个log + """ + + # ------------------准备阶段---------------- + # 1、获取标的价格 + self.RecentAlldata = self.turtles.GetRecentData() + self.RecentAlldata['日期'] = pd.to_datetime(self.RecentAlldata['日期']) + self.RecentAlldata.set_index('日期', inplace=True) + + # 2、计算ATR + self.N = self.turtles.CalATR(self.RecentAlldata, 20, None) + # self.PositionSize = self.turtles.CalPositionSize(self.riskcoe, self.cash) + # 3、计算唐奇安通道上下边界--system2 + self.Donchian = self.system1CalDonchian() + + # 开始、结束日期 + RowStart = self.RecentAlldata.index.get_loc(self.StartTime) + RowEnd = self.RecentAlldata.index.get_loc(self.EndTime) + + # 准备迭代使用的数据 + TempData = pd.DataFrame() # 存储最高、最低价等信息 + TempN = [] + TempDonchian55Upper = [] + TempDonchian20Upper = [] + TempDonchian10Lower = [] + self.BreakOutLog = [['date', 'breakout', 'BOprice', 'LosePrice', 'ValidOrNot', 'WinOrLose']] + # ------------------运行阶段---------------- + + for date, row in self.RecentAlldata[RowStart:RowEnd].iterrows(): + + # 增加一个迭代更新的数据 + TempData = pd.concat([TempData, row.to_frame().T], ignore_index=True) + + # day <=55,不执行任何操作,跳过,等待 + + # day < 55,<20刚开始运行,N、Donchian数据从总数据拿, 不用框时间,直接从总数据拿 + # if TempData.shape[0] < 20: + # 每天更新上下边界 + TempDonchian10Lower.append(self.Donchian['Lower'].iloc[RowStart + TempData.shape[0]-2]) + + # if TempData.shape[0] < 55: + TempDonchian55Upper.append(self.Donchian['Upper55'].iloc[RowStart + TempData.shape[0]-2]) + TempDonchian20Upper.append(self.Donchian['Upper20'].iloc[RowStart + TempData.shape[0]-2]) + + # 检查是否是新的一周 + if self.current_week is None or date.week != self.current_week: + self.current_week = date.week + + # 更新atr、头寸规模 + self.ThisWeekN = self.N.iloc[RowStart + TempData.shape[0]-2] + TempN.append(self.ThisWeekN) + + self.ThisWeekPosizionSize = self.CalPositionSize(self.ThisWeekN, TempData.iloc[-1]['收盘']) + + + # 如果空仓,判断当天开盘价是否突破,收盘价是否突破,突破则买入 + if self.TrigerTime == 0: + if self.system1EnterNormal(row["开盘"], TempDonchian20Upper, self.BreakOutLog): + self.TestBuyStocks(row["开盘"], date) + # 写入BreakOut状态 + self.BreakOutLog.append([date, 'breakout', row["开盘"], row["开盘"] - 2*self.ThisWeekN, 'Valid','None']) + # 开盘突破后,最高价能否加仓? + # 价格差取整是能加仓几手,循环加仓 + delta = math.floor((row['最高']-row['开盘']) / (1/2 * self.ThisWeekN)) + if 1 <= delta <= 3: + for i in range(delta): + self.TestBuyStocks((row["开盘"] + (i+1) * 1/2 * self.ThisWeekN + 0.001), date) + elif 3 < delta: + for i in range(3): + self.TestBuyStocks((row["开盘"] + (i+1) * 1/2 * self.ThisWeekN + 0.001), date) + + elif self.system1EnterSafe(row["开盘"], TempDonchian55Upper): + self.TestBuyStocks(row["开盘"], date) + # 价格差取整是能加仓几手,循环加仓 + delta = math.floor((row['最高']-row['开盘']) / (1/2 * self.ThisWeekN)) + if 1 <= delta <= 3: + for i in range(delta): + self.TestBuyStocks((row["开盘"] + (i+1) * 1/2 * self.ThisWeekN + 0.001), date) + elif 3 < delta: + for i in range(3): + self.TestBuyStocks((row["开盘"] + (i+1) * 1/2 * self.ThisWeekN + 0.001), date) + + # 最高价突破买入 + elif self.system1EnterNormal(row["最高"], TempDonchian20Upper, self.BreakOutLog): + self.TestBuyStocks(TempDonchian20Upper[-1] + 0.001, date) + # 写入BreakOut状态 + self.BreakOutLog.append([date, 'breakout', TempDonchian20Upper[-1] + 0.001, TempDonchian20Upper[-1] + 0.001 - 2*self.ThisWeekN, 'Valid','None']) + + # 价格差取整是能加仓几手,循环加仓 + delta = math.floor((row['最高']-TempDonchian20Upper[-1]) / (1/2 * self.ThisWeekN)) + if 1 <= delta <= 3: + for i in range(delta): + self.TestBuyStocks((TempDonchian20Upper[-1] + (i+1) * 1/2 * self.ThisWeekN + 0.001), date) + elif 3 < delta: + for i in range(3): + self.TestBuyStocks((TempDonchian20Upper[-1] + (i+1) * 1/2 * self.ThisWeekN + 0.001), date) + + elif self.system1EnterSafe(row["最高"], TempDonchian55Upper): + self.TestBuyStocks(TempDonchian55Upper[-1] + 0.001, date) + + # 0"trigger_count", 1"BuyPrice", 2"AddPrice", 3"StopPrice", 4"quantity", 5"N", 6"available_cash" + # self.BuyStates = [self.TrigerTime, BuyPrice, AddPrice, StopPrice, Shares, N, available_cash] + elif 1 <= self.TrigerTime < 4: # 加仓1-3次,考虑止损和加仓 + # 开盘价突破 + if self.system1EnterNormal(row["开盘"], TempDonchian20Upper, self.BreakOutLog): + self.TestBuyStocks(row["开盘"], date) + self.BreakOutLog.append([date, 'breakout', TempDonchian20Upper[-1] + 0.001, TempDonchian20Upper[-1] + 0.001 - 2*self.ThisWeekN, 'Valid','None']) + elif self.system1EnterSafe(row["开盘"], TempDonchian55Upper): + self.TestBuyStocks(row["开盘"], date) + + # 开盘价加仓 + if self.system2Add(row["开盘"]): + self.TestBuyStocks(row["开盘"], date) + + if self.TrigerTime == 4: + # 满仓了,不能再加 + pass + # 开盘加仓后,最高价能否继续加仓? + else: + delta = math.floor((row['最高']-row['开盘']) / (1/2 * self.ThisWeekN)) + + if 1 <= delta <= 4-self.TrigerTime: + for i in range(delta): + self.TestBuyStocks((row["开盘"] + (i+1) * 1/2 * self.ThisWeekN + 0.001), date) + + # 最高价加仓 + elif self.system2Add(row["最高"]): + self.TestBuyStocks(self.BuyStates[self.TrigerTime - 1][2] + 0.001, date) + + if self.TrigerTime == 4: + # 满仓了,不能再加 + pass + # 开盘加仓后,最高价能否继续加仓? + else: + delta = math.floor((row['最高']-self.BuyStates[self.TrigerTime - 1][2]) / (1/2 * self.ThisWeekN)) + + if 1 <= delta <= 4-self.TrigerTime: + for i in range(delta): + self.TestBuyStocks((self.BuyStates[self.TrigerTime - 1][2] + (i+1) * 1/2 * self.ThisWeekN + 0.001), date) + + # 止损 + elif self.system2Stop(row["收盘"]): + self.TestStopSaleStocks(self.BuyStates[self.TrigerTime - 1][3] - 0.001, date) + elif self.system2Stop(row["最低"]): + self.TestStopSaleStocks(self.BuyStates[self.TrigerTime - 1][3] - 0.001, date) + # 止盈 + elif self.system2Out(row["收盘"], TempDonchian10Lower): + self.TestOutSaleStocks(TempDonchian10Lower[-1] - 0.001, date) + elif self.system2Out(row["最低"], TempDonchian10Lower): + self.TestOutSaleStocks(TempDonchian10Lower[-1] - 0.001, date) + + elif self.TrigerTime == 4: # 满仓 考虑止损和退出 + # 止损 + if self.system2Stop(row["收盘"]): + self.TestStopSaleStocks(self.BuyStates[self.TrigerTime - 1][3] - 0.001, date) + elif self.system2Stop(row["最低"]): + self.TestStopSaleStocks(self.BuyStates[self.TrigerTime - 1][3] - 0.001, date) + # 止盈 + elif self.system2Out(row["收盘"], TempDonchian10Lower): + self.TestOutSaleStocks(TempDonchian10Lower[-1] - 0.001, date) + elif self.system2Out(row["最低"], TempDonchian10Lower): + self.TestOutSaleStocks(TempDonchian10Lower[-1] - 0.001, date) + + # 交易日结束,重新计算高低突破点: + + print("---------------回测结束,回测日志如下----------------") + + for sublist in self.tradeslog: + print(sublist) + for sublist in self.BreakOutLog: + print(sublist) + + +# nsdk513300 = TurtleTrading(513300) + +# # 每周更新 +# nsdk513300.GetRecentData() +# nsdk513300.CalATR(20, True) +# nsdk513300.ReadExistData('513300data-N.csv') +# nsdk513300.CalPositionSize(0.0025, 100000) + +# # 每天更新 +# nsdk513300.Donchian20 = nsdk513300.calculate_donchian_channel(500, 20) +# nsdk513300.Donchian10 = nsdk513300.calculate_donchian_channel(500, 10) +# nsdk513300.Donchian55 = nsdk513300.calculate_donchian_channel(500, 55) +# nsdk513300.DrawKLine(500) + +# print(nsdk513300.PositionSize) + + +if __name__ == "__main__": + nsdk513300 = TurtleTrading(513300) + # nsdk513300test = Trade(nsdk513300, 0.0025, 100000, '2023-1-3', '2024-05-9') + nsdk513300test = Trade(nsdk513300, 0.0025, 100000, '2014-1-15', '2025-02-18') + + nsdk513300test.TestSys2Function() + # nsdk513300test.TestSys1Function() \ No newline at end of file diff --git a/回测/TurtleClass_old.py b/回测/TurtleClass_old.py new file mode 100755 index 0000000..4c15206 --- /dev/null +++ b/回测/TurtleClass_old.py @@ -0,0 +1,235 @@ +import numpy as np +import akshare as ak +import os +from datetime import datetime, timedelta +import pandas as pd +import mplfinance as mpf + +def CalTrueFluc(data, day): + + H_L = data.iloc[day]['最高'] - data.iloc[day]['最低'] + H_PDC = data.iloc[day]['最高'] - data.iloc[day-1]['收盘'] + PDC_L = data.iloc[day-1]['收盘'] - data.iloc[day]['最低'] + TrueFluc = np.max([H_L, H_PDC, PDC_L]) + print('high', data.iloc[day]['最高'], 'low', data.iloc[day]['最低'], 'TrueRange', TrueFluc) + + return TrueFluc + +def calc_sma_atr_pd(kdf,period): + """计算TR与ATR + + Args: + kdf (_type_): 历史数据 + period (_type_): ATR周期 + + Returns: + _type_: 返回kdf,增加TR与ATR列 + """ + kdf['HL'] = kdf['最高'] - kdf['最低'] + kdf['HC'] = np.abs(kdf['最高'] - kdf['收盘'].shift(1)) + kdf['LC'] = np.abs(kdf['最低'] - kdf['收盘'].shift(1)) + kdf['TR'] = np.round(kdf[['HL','HC','LC']].max(axis=1), 3) + # ranges = pd.concat([high_low, high_close, low_close], axis=1) + # true_range = np.max(ranges, axis=1) + kdf['ATR'] = np.round(kdf['TR'].rolling(period).mean(), 3) + + return kdf.drop(['HL','HC','LC'], axis = 1) + +# A股数据 东方财富网 +# all_data = ak.stock_zh_a_spot_em() + +# 基金实时数据 东方财富网 +# fund_etf_spot_em_df = ak.fund_etf_spot_em() + +# 后复权历史数据 +# fund_etf_hist_em_df = ak.fund_etf_hist_em(symbol="513300", period="daily", start_date="20130101", end_date="20240408", adjust="hfq") +# fund_etf_hist_em_df.to_csv('513300data.csv', index=False) + +# data = pd.read_csv('513300data.csv') + + +# # 一、计算头寸规模 + +# # 真实波动幅度 = max (H-L, H-pdc, pdc-L) + +# today = datetime.today() +# # print(today) + +# # print(data.iloc[-1]['成交额']) + +# TrueFlucs = [] +# Nserious = np.zeros(101) +# last120days = np.arange(-120, -100) +# for i in last120days: +# H_L = data.iloc[i]['最高'] - data.iloc[i]['最低'] +# H_PDC = data.iloc[i]['最高'] - data.iloc[i-1]['收盘'] +# PDC_L = data.iloc[i-1]['收盘'] - data.iloc[i]['最低'] +# TrueFlucs.append(np.max([H_L, H_PDC, PDC_L])) + +# # 求简单平均,放入N序列第一个 +# Nsimple = np.average(TrueFlucs) +# Nserious[0] = Nsimple +# # 计算-21到-1的N +# last100days = np.arange(-100, 0) + +# for i in range(0,100): +# day = last100days[i] +# H_L = data.iloc[day]['最高'] - data.iloc[day]['最低'] +# H_PDC = data.iloc[day]['最高'] - data.iloc[day-1]['收盘'] +# PDC_L = data.iloc[day-1]['收盘'] - data.iloc[day]['最低'] +# TrueFluc = np.max([H_L, H_PDC, PDC_L]) + +# Ntemp = (19 * Nserious[i] + TrueFluc)/20 +# Nserious[i+1] = Ntemp + +# # print(Nserious) + + +# total_rows = len(data) +# Ndata = np.zeros(total_rows) +# Ndata[total_rows-101:] = Nserious +# # NewColumn = [0]*(total_rows-101) + Nserious +# data['N'] = Ndata + +# data.to_csv('513300data-N.csv', index=False) +# pass + + +# -----------------------更新atr---------------------- + """已有数据与新数据对比,补充新的N,同时更新数据库 + """ + +# Today = datetime.today() +# # print(Today) +# formatted_date = Today.strftime("%Y%m%d") + +# # print(formatted_date) +# CurrentData = ak.fund_etf_hist_em(symbol="513300", period="daily", start_date="20130101", end_date=formatted_date, adjust="hfq") + +# CurrentData = calc_sma_atr_pd(CurrentData, 20) +# CurrentData.to_csv('513300data-N.csv', index=False) +# pass + + +# ------------------计算头寸规模 资金10w, 1%波动------------ + +# money = 100000 +# OldData = pd.read_csv('513300data-N.csv') + +# N = OldData.iloc[-1]['ATR'] +# # N = 0.473 +# Price = OldData.iloc[-1]['收盘'] +# # Price = 5.60 +# EveryUnit = 0.0025 * money /(N*100*Price) + +# print('单位',EveryUnit) + +# print(113*100*Price) + + +class TurtleTrading(object): + """对象范围较小,对某一个标的创建一个海龟,如513300, + 计算ATR、 + Position Size, + 买入、卖出、加仓等行为 + Args: + object (_type_): _description_ + """ + def __init__(self, TradeCode) -> None: + self.TradeCode = TradeCode + + def CalATR(self, ATRday, SaveOrNot): + """计算某个标的的ATR,从上市日到今天, 计算后的数据保存在self.CurrentData + + Args: + ATRday: 多少日ATR + SaveOrNot (_type_): 是否保存.csv数据 + """ + Today = datetime.today() + # print(Today) + formatted_date = Today.strftime("%Y%m%d") + + # print(formatted_date) + Code = f"{self.TradeCode}" + CurrentData = ak.fund_etf_hist_em(symbol=Code, period="daily", start_date="20130101", end_date=formatted_date, adjust="hfq") + + self.CurrentData = calc_sma_atr_pd(CurrentData, ATRday) + if SaveOrNot: + self.CurrentData.to_csv('513300data-N.csv', index=False) + print("csv保存成功") + + + def CalPositionSize(self, RiskCoef, Capital): + """计算PosizionSize 持有的单位,该单位某标的,1N波动对应RiskCoef * Capital资金 + + Args: + RiskCoef (_type_): 风险系数 + Capital (_type_): 资金 + """ + + N = self.CurrentData.iloc[-1]['ATR'] + # N = 0.473 + Price = self.CurrentData.iloc[-1]['收盘'] + # Price = 5.60 + self.PositionSize = RiskCoef * Capital /( N*100*Price) # 默认用股票形式了 100 + + + def ReadExistData(self, data): + """除了通过发请求获取数据,也可以读本地的数据库,赋值给self.CurrentData + + Args: + data (_type_): 本地csv名称 + """ + self.CurrentData = pd.read_csv(data) + + def DrawKLine(self, days): + """画出k线图看看,画出最近days天的K线图 + """ + + # 日期部分 + + dates = pd.to_datetime(self.CurrentData['日期'][-days:]) + # Klinedf['Data'] = pd.to_datetime(self.CurrentData['日期']) + Klinedf = pd.DataFrame() + # Klinedf.set_index = Klinedf['Data'] + + # 其他数据 + Klinedf['Open'] = self.CurrentData['开盘'][-days:] + Klinedf['High'] = self.CurrentData['最高'][-days:] + Klinedf['Low'] = self.CurrentData['最低'][-days:] + Klinedf['Close'] = self.CurrentData['收盘'][-days:] + Klinedf['Volume'] = self.CurrentData['成交量'][-days:] + + Klinedf.set_index(dates, inplace=True) + # 画图 + mpf.plot(Klinedf, type='candle', style='yahoo', volume=False, mav=(5,), addplot=[mpf.make_addplot(self.Donchian[['Upper', 'Lower']])]) + + def calculate_donchian_channel(self, days, n): + """ + 计算唐奇安通道days一共多少日, n多少日唐奇安 + + 参数: + self.CurrentData (DataFrame): 包含价格数据的Pandas DataFrame,至少包含"High"和"Low"列 + n (int): 时间周期 + + 返回:self.Donchian + DataFrame: 唐奇安通道的DataFrame,包含"Upper", "Lower", 和 "Middle"列 + """ + self.Donchian = pd.DataFrame() + # 计算最高价和最低价的N日移动平均线 + self.Donchian['Upper'] = self.CurrentData['最高'][-days:].rolling(n).max() + self.Donchian['Lower'] = self.CurrentData['最低'][-days:].rolling(n).min() + + # 计算中间线 + self.Donchian['Middle'] = (self.Donchian['Upper'] + self.Donchian['Lower']) / 2 + + # return data[['Upper', 'Lower', 'Middle']] + +nsdk513300 = TurtleTrading(513300) +# nsdk513300.CalATR(20, True) +nsdk513300.ReadExistData('513300data-N.csv') +# nsdk513300.CalPositionSize(0.0025, 100000) +nsdk513300.calculate_donchian_channel(500, 20) +nsdk513300.DrawKLine(500) + +# print(nsdk513300.PositionSize) \ No newline at end of file diff --git a/回测/TurtleOnTime.py b/回测/TurtleOnTime.py new file mode 100644 index 0000000..bec200d --- /dev/null +++ b/回测/TurtleOnTime.py @@ -0,0 +1,139 @@ +import numpy as np +import math +import akshare as ak +import os +from datetime import datetime, timedelta, date +import pandas as pd +import mplfinance as mpf + +def calc_sma_atr_pd(kdf,period): + """计算TR与ATR + + Args: + kdf (_type_): 历史数据 + period (_type_): ATR周期 + + Returns: + _type_: 返回kdf,增加TR与ATR列 + """ + kdf['HL'] = kdf['最高'] - kdf['最低'] + kdf['HC'] = np.abs(kdf['最高'] - kdf['收盘'].shift(1)) + kdf['LC'] = np.abs(kdf['最低'] - kdf['收盘'].shift(1)) + kdf['TR'] = np.round(kdf[['HL','HC','LC']].max(axis=1), 3) + # ranges = pd.concat([high_low, high_close, low_close], axis=1) + # true_range = np.max(ranges, axis=1) + kdf['ATR'] = np.round(kdf['TR'].rolling(period).mean(), 3) + + return kdf.drop(['HL','HC','LC'], axis = 1) + +class TurtleTrading(object): + """对象范围较小,对某一个标的创建一个海龟,如513300, + 计算ATR、 + Position Size, + 买入、卖出、加仓等行为 + Args: + object (_type_): _description_ + """ + def __init__(self, TradeCode) -> None: + self.TradeCode = TradeCode + + def GetRecentData(self): + """获取某个标的的最近数据,从两年前到今天, 计算后的数据保存在self.CurrentData + + Returns: + _type_: _description_ + """ + Today = datetime.today() + # print(Today) + formatted_date = Today.strftime("%Y%m%d") + two_years_ago = date.today() - timedelta(days=365*2).strftime("%Y%m%d") + # print(formatted_date) + Code = f"{self.TradeCode}" + self.CurrentData = ak.fund_etf_hist_em(symbol=Code, period="daily", start_date=two_years_ago, end_date=formatted_date, adjust="") + # return CurrentData + + def CalATR(self, data, ATRday, SaveOrNot): + """计算某个标的的ATR,从上市日到今天, 计算后的数据保存在self.CurrentData + + Args: + ATRday: 多少日ATR + SaveOrNot (_type_): 是否保存.csv数据 + """ + + self.CurrentData = calc_sma_atr_pd(data, ATRday) + self.N = self.CurrentData['ATR'] + if SaveOrNot: + self.CurrentData.to_csv('513300data-N.csv', index=False) + print("csv保存成功") + + return self.N + + + def CalPositionSize(self, RiskCoef, Capital): + """计算PosizionSize 持有的单位,该单位某标的,1N波动对应RiskCoef * Capital资金 + + Args: + RiskCoef (_type_): 风险系数 + Capital (_type_): 资金 + """ + + N = self.CurrentData.iloc[-1]['ATR'] + # N = 0.473 + Price = self.CurrentData.iloc[-1]['收盘'] + # Price = 5.60 + self.PositionSize = RiskCoef * Capital /( N*100*Price) # 默认用股票形式了 100 + + + return self.PositionSize + + + def ReadExistData(self, data): + """除了通过发请求获取数据,也可以读本地的数据库,赋值给self.CurrentData + + Args: + data (_type_): 本地csv名称 + """ + self.CurrentData = pd.read_csv(data) + + def DrawKLine(self, days): + """画出k线图看看,画出最近days天的K线图 + """ + + # 日期部分 + + dates = pd.to_datetime(self.CurrentData['日期'][-days:]) + # Klinedf['Data'] = pd.to_datetime(self.CurrentData['日期']) + Klinedf = pd.DataFrame() + # Klinedf.set_index = Klinedf['Data'] + + # 其他数据 + Klinedf['Open'] = self.CurrentData['开盘'][-days:] + Klinedf['High'] = self.CurrentData['最高'][-days:] + Klinedf['Low'] = self.CurrentData['最低'][-days:] + Klinedf['Close'] = self.CurrentData['收盘'][-days:] + Klinedf['Volume'] = self.CurrentData['成交量'][-days:] + + Klinedf.set_index(dates, inplace=True) + # 画图 + mpf.plot(Klinedf, type='candle', style='yahoo', volume=False, mav=(5,), addplot=[mpf.make_addplot(self.Donchian[['Upper', 'Lower']])]) + + def calculate_donchian_channel(self, days, n): + """ + 计算唐奇安通道days一共多少日, n多少日唐奇安 + + 参数: + self.CurrentData (DataFrame): 包含价格数据的Pandas DataFrame,至少包含"High"和"Low"列 + n (int): 时间周期 + + 返回:self.Donchian + DataFrame: 唐奇安通道的DataFrame,包含"Upper", "Lower", 和 "Middle"列 + """ + Donchian = pd.DataFrame() + # 计算最高价和最低价的N日移动平均线 + Donchian['Upper'] = self.CurrentData['最高'][-days:].rolling(n).max() + Donchian['Lower'] = self.CurrentData['最低'][-days:].rolling(n).min() + + # # 计算中间线 + # Donchian['Middle'] = (self.Donchian['Upper'] + self.Donchian['Lower']) / 2 + + return Donchian \ No newline at end of file diff --git a/回测/TurtleOnTime_ai.py b/回测/TurtleOnTime_ai.py new file mode 100755 index 0000000..e38e0cc --- /dev/null +++ b/回测/TurtleOnTime_ai.py @@ -0,0 +1,645 @@ +"""海龟实时 +""" +import numpy as np +import math +import akshare as ak +import os +from datetime import datetime, timedelta +import pandas as pd +import mplfinance as mpf +import TurtleClassNew + +# ----------------------------------- +# 创建组合,先用一个测试 +conbinations = [] +# 我是否需要当前组合的信息:需要 +# 什么东西 股票还是etf +# 风险系数risk_coef; atr;头寸单位; +# 系数是多少,每1%波动多少钱 atr 买4份一共多少钱 +# 组合总共会花掉多少钱 + +# 每个item应该具有的属性 +# code +# ATR +# price +# risk_coef +# capital + +# 每个月调整risk_coef和captial + +# 初始化函数 +# 初始化conbinations中的数据 + + +# 监盘函数: + +# 数据整理保存函数,收盘后开始 + +for item in conbinations: + + +# 创建Turtle实例(ETF与股票获取数据代码不同) + pass +# 获取数据每5分钟获取一次 + +# 计算唐奇安通道 每天收盘计算 + +# + +# https://akshare.akfamily.xyz/data/stock/stock.html#id9 + +import akshare as ak +import pandas as pd +import numpy as np +import sqlite3 +from datetime import datetime +import smtplib +from email.mime.text import MIMEText +from email.header import Header +import json +from typing import Dict, List, Tuple +import logging +from decimal import Decimal + +class DatabaseManager: + def __init__(self, db_path: str = "turtle_trading.db"): + self.conn = sqlite3.connect(db_path) + self.create_tables() + + def create_tables(self): + """创建必要的数据表""" + # 交易信号记录表 + self.conn.execute(''' + CREATE TABLE IF NOT EXISTS signals ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + stock_code TEXT, + signal_type TEXT, + suggested_price REAL, + suggested_quantity INTEGER, + timestamp DATETIME, + status TEXT + )''') + + # 实际交易记录表 + self.conn.execute(''' + CREATE TABLE IF NOT EXISTS trades ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + signal_id INTEGER, + actual_price REAL, + actual_quantity INTEGER, + timestamp DATETIME, + FOREIGN KEY (signal_id) REFERENCES signals (id) + )''') + + # 持仓状态表 + self.conn.execute(''' + CREATE TABLE IF NOT EXISTS positions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + stock_code TEXT, + quantity INTEGER, + avg_cost REAL, + entry_price REAL, + last_price REAL, + stop_loss REAL, + target_price REAL, + position_type TEXT, + timestamp DATETIME, + status TEXT + )''') + + def get_position(self, stock_code: str) -> Dict: + """获取股票当前持仓信息""" + cursor = self.conn.execute(''' + SELECT * FROM positions + WHERE stock_code = ? AND status = 'ACTIVE' + ''', (stock_code,)) + position = cursor.fetchone() + if position: + return { + 'id': position[0], + 'stock_code': position[1], + 'quantity': position[2], + 'avg_cost': position[3], + 'entry_price': position[4], + 'last_price': position[5], + 'stop_loss': position[6], + 'target_price': position[7], + 'position_type': position[8], + 'timestamp': position[9], + 'status': position[10] + } + return None + + def update_position(self, stock_code: str, last_price: float): + """更新持仓的最新价格""" + self.conn.execute(''' + UPDATE positions + SET last_price = ?, timestamp = ? + WHERE stock_code = ? AND status = 'ACTIVE' + ''', (last_price, datetime.now(), stock_code)) + self.conn.commit() + + def create_position(self, stock_code: str, quantity: int, + entry_price: float, position_type: str, + stop_loss: float, target_price: float): + """创建新持仓""" + self.conn.execute(''' + INSERT INTO positions ( + stock_code, quantity, avg_cost, entry_price, last_price, + stop_loss, target_price, position_type, timestamp, status + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ''', (stock_code, quantity, entry_price, entry_price, entry_price, + stop_loss, target_price, position_type, datetime.now(), 'ACTIVE')) + self.conn.commit() + + def close_position(self, stock_code: str): + """关闭持仓""" + self.conn.execute(''' + UPDATE positions + SET status = 'CLOSED', timestamp = ? + WHERE stock_code = ? AND status = 'ACTIVE' + ''', (datetime.now(), stock_code)) + self.conn.commit() + +class TurtleStrategy: + def __init__(self, lookback_days: int = 20): + self.lookback_days = lookback_days + + def calculate_signals(self, df: pd.DataFrame, + current_position: Dict = None) -> Dict: + """计算交易信号,考虑当前持仓状态""" + # 计算技术指标 + df['high_20'] = df['high'].rolling(20).max() + df['low_10'] = df['low'].rolling(10).min() + df['atr'] = self._calculate_atr(df) + + current = df.iloc[-1] + prev = df.iloc[-2] + + signal = { + 'type': None, + 'price': None, + 'quantity': 0, + 'stop_loss': None, + 'target_price': None + } + + if current_position: + # 持仓状态下的信号计算 + return self._calculate_position_signals( + current_position, current, prev, df + ) + else: + # 无持仓状态下的信号计算 + return self._calculate_entry_signals(current, prev, df) + + def _calculate_position_signals(self, position: Dict, + current: pd.Series, + prev: pd.Series, + df: pd.DataFrame) -> Dict: + """计算持仓状态下的信号""" + signal = { + 'type': None, + 'price': current['close'], + 'quantity': 0, + 'stop_loss': position['stop_loss'], + 'target_price': position['target_price'] + } + + # 检查止损 + if current['low'] <= position['stop_loss']: + signal['type'] = 'STOP_LOSS' + signal['quantity'] = position['quantity'] + return signal + + # 检查获利目标 + if current['high'] >= position['target_price']: + signal['type'] = 'TAKE_PROFIT' + signal['quantity'] = position['quantity'] + return signal + + # 检查加仓条件 + if position['position_type'] == 'LONG': + if current['close'] > position['entry_price'] * 1.05: # 5%盈利时考虑加仓 + signal['type'] = 'ADD' + signal['quantity'] = self._calculate_position_size( + current['close'], df['atr'].iloc[-1] + ) + # 更新止损为前低 + signal['stop_loss'] = df['low'].rolling(5).min().iloc[-1] + + # 检查减仓条件 + elif position['position_type'] == 'SHORT': + if current['close'] < position['entry_price'] * 0.95: # 5%盈利时考虑加仓 + signal['type'] = 'REDUCE' + signal['quantity'] = self._calculate_position_size( + current['close'], df['atr'].iloc[-1] + ) + # 更新止损为前高 + signal['stop_loss'] = df['high'].rolling(5).max().iloc[-1] + + return signal + + def _calculate_entry_signals(self, current: pd.Series, + prev: pd.Series, + df: pd.DataFrame) -> Dict: + """计算入场信号""" + signal = { + 'type': None, + 'price': current['close'], + 'quantity': 0, + 'stop_loss': None, + 'target_price': None + } + + atr = df['atr'].iloc[-1] + + # 多头入场 + if current['close'] > prev['high_20']: + signal['type'] = 'BUY' + signal['quantity'] = self._calculate_position_size( + current['close'], atr + ) + signal['stop_loss'] = current['close'] - 2 * atr + signal['target_price'] = current['close'] + 4 * atr + + # 空头入场 + elif current['close'] < prev['low_10']: + signal['type'] = 'SELL' + signal['quantity'] = self._calculate_position_size( + current['close'], atr + ) + signal['stop_loss'] = current['close'] + 2 * atr + signal['target_price'] = current['close'] - 4 * atr + + return signal + + def _calculate_atr(self, df: pd.DataFrame) -> pd.Series: + """计算ATR指标""" + df['tr'] = np.maximum( + df['high'] - df['low'], + np.maximum( + abs(df['high'] - df['close'].shift(1)), + abs(df['low'] - df['close'].shift(1)) + ) + ) + return df['tr'].rolling(20).mean() + + def _calculate_position_size(self, price: float, atr: float) -> int: + """计算持仓规模""" + risk_per_trade = 100000 * 0.01 # 假设账户规模100000,每次风险1% + return int(risk_per_trade / (atr * 100)) + +class EmailManager: + def __init__(self, config_path: str = "email_config.json"): + with open(config_path) as f: + self.config = json.load(f) + + def send_signal(self, stock_code: str, signal_type: str, + suggested_price: float, suggested_quantity: int, + stop_loss: float = None, target_price: float = None) -> bool: + """发送交易信号邮件""" + subject = f"交易信号: {stock_code} - {signal_type}" + content = f""" + 股票代码: {stock_code} + 信号类型: {signal_type} + 建议价格: {suggested_price} + 建议数量: {suggested_quantity} + 止损价位: {stop_loss if stop_loss else '无'} + 目标价位: {target_price if target_price else '无'} + + 请回复实际成交价格和数量, 格式: + 价格,数量 + 例如: 10.5,100 + """ + + return self._send_email(subject, content) + + def send_position_update(self, position: Dict, + current_price: float) -> bool: + """发送持仓更新邮件""" + subject = f"持仓更新: {position['stock_code']}" + + # 计算收益 + profit = (current_price - position['avg_cost']) * position['quantity'] + profit_pct = (current_price / position['avg_cost'] - 1) * 100 + + content = f""" + 股票代码: {position['stock_code']} + 当前价格: {current_price} + 持仓数量: {position['quantity']} + 平均成本: {position['avg_cost']} + 止损价位: {position['stop_loss']} + 目标价位: {position['target_price']} + 当前收益: {profit:.2f} ({profit_pct:.2f}%) + 持仓类型: {position['position_type']} + """ + + return self._send_email(subject, content) + + def _send_email(self, subject: str, content: str) -> bool: + """发送邮件的具体实现""" + try: + msg = MIMEText(content, 'plain', 'utf-8') + msg['Subject'] = Header(subject, 'utf-8') + msg['From'] = self.config['sender'] + msg['To'] = self.config['receiver'] + + with smtplib.SMTP_SSL(self.config['smtp_server'], + self.config['smtp_port']) as server: + server.login(self.config['username'], self.config['password']) + server.sendmail(self.config['sender'], + [self.config['receiver']], + msg.as_string()) + return True + except Exception as e: + logging.error(f"发送邮件失败: {str(e)}") + return False + +class TurtleTrader: + def __init__(self, config_path: str = "config.json"): + self.db = DatabaseManager() + self.strategy = TurtleStrategy() + self.email = EmailManager() + + # 加载配置 + with open(config_path) as f: + self.config = json.load(f) + + def process_stock(self, stock_code: str): + """处理单个股票""" + try: + # 获取当前持仓状态 + position = self.db.get_position(stock_code) + + # 获取股票数据 + df = ak.stock_zh_a_hist( + symbol=stock_code, + period="daily", + start_date="20230101", + end_date=datetime.now().strftime("%Y%m%d"), + adjust="qfq" + ) + + # 计算信号 + signal = self.strategy.calculate_signals(df, position) + + current_price = df['close'].iloc[-1] + + # 更新持仓的最新价格 + if position: + self.db.update_position(stock_code, current_price) + # 定期发送持仓更新 + self.email.send_position_update(position, current_price) + + if signal['type']: + # 保存信号 + signal_id = self.db.save_signal( + stock_code, signal['type'], + signal['price'], signal['quantity'] + ) + + # 发送邮件 + self.email.send_signal( + stock_code, signal['type'], + signal['price'], signal['quantity'], + signal['stop_loss'], signal['target_price'] + ) + + except Exception as e: + logging.error(f"处理股票 {stock_code} 时发生错误: {str(e)}") + + + def process_feedback(self, signal_id: int, actual_price: float, + actual_quantity: int): + """处理交易反馈并更新持仓状态""" + try: + # 获取原始信号 + cursor = self.db.conn.execute(''' + SELECT stock_code, signal_type, suggested_price + FROM signals WHERE id = ? + ''', (signal_id,)) + signal = cursor.fetchone() + + if not signal: + raise ValueError(f"Signal ID {signal_id} not found") + + stock_code, signal_type, suggested_price = signal + + # 保存实际交易记录 + self.db.save_trade(signal_id, actual_price, actual_quantity) + + # 更新持仓状态 + current_position = self.db.get_position(stock_code) + + if signal_type in ['BUY', 'ADD']: + if current_position: + # 计算新的平均成本 + total_cost = (current_position['avg_cost'] * + current_position['quantity'] + + actual_price * actual_quantity) + total_quantity = (current_position['quantity'] + + actual_quantity) + new_avg_cost = total_cost / total_quantity + + # 更新持仓 + self.db.conn.execute(''' + UPDATE positions + SET quantity = ?, avg_cost = ?, last_price = ?, + timestamp = ? + WHERE id = ? + ''', (total_quantity, new_avg_cost, actual_price, + datetime.now(), current_position['id'])) + else: + # 创建新持仓 + # 使用ATR计算止损和目标价位 + df = self._get_stock_data(stock_code) + atr = self.strategy._calculate_atr(df).iloc[-1] + + stop_loss = actual_price - 2 * atr + target_price = actual_price + 4 * atr + + self.db.create_position( + stock_code, actual_quantity, actual_price, + 'LONG', stop_loss, target_price + ) + + elif signal_type in ['SELL', 'REDUCE', 'STOP_LOSS', 'TAKE_PROFIT']: + if current_position: + remaining_quantity = (current_position['quantity'] - + actual_quantity) + + if remaining_quantity > 0: + # 部分平仓 + self.db.conn.execute(''' + UPDATE positions + SET quantity = ?, last_price = ?, timestamp = ? + WHERE id = ? + ''', (remaining_quantity, actual_price, + datetime.now(), current_position['id'])) + else: + # 完全平仓 + self.db.close_position(stock_code) + + self.db.conn.commit() + + except Exception as e: + logging.error(f"处理交易反馈时发生错误: {str(e)}") + raise + + def _get_stock_data(self, stock_code: str, days: int = 30) -> pd.DataFrame: + """获取股票历史数据""" + end_date = datetime.now() + start_date = end_date - timedelta(days=days) + + df = ak.stock_zh_a_hist( + symbol=stock_code, + period="daily", + start_date=start_date.strftime("%Y%m%d"), + end_date=end_date.strftime("%Y%m%d"), + adjust="qfq" + ) + return df + +class PerformanceAnalyzer: + def __init__(self, db_manager: DatabaseManager): + self.db = db_manager + + def analyze(self) -> Dict: + """分析交易和持仓表现""" + # 分析交易执行质量 + trade_metrics = self._analyze_trade_execution() + + # 分析持仓表现 + position_metrics = self._analyze_positions() + + return { + "trade_execution": trade_metrics, + "position_performance": position_metrics + } + + def _analyze_trade_execution(self) -> Dict: + """分析交易执行质量""" + cursor = self.db.conn.execute(''' + SELECT s.stock_code, s.signal_type, s.suggested_price, + s.suggested_quantity, t.actual_price, t.actual_quantity + FROM signals s + JOIN trades t ON s.id = t.signal_id + WHERE s.status = 'EXECUTED' + ''') + + trades = cursor.fetchall() + + if not trades: + return {"message": "没有足够的交易数据进行分析"} + + # 计算关键指标 + price_slippage = [] + quantity_fill = [] + execution_delay = [] + + for trade in trades: + price_diff = (trade[4] - trade[2]) / trade[2] * 100 + quantity_diff = trade[5] / trade[3] * 100 + + price_slippage.append(price_diff) + quantity_fill.append(quantity_diff) + + return { + "total_trades": len(trades), + "avg_price_slippage": np.mean(price_slippage), + "max_price_slippage": max(price_slippage), + "avg_quantity_fill": np.mean(quantity_fill), + "price_slippage_std": np.std(price_slippage) + } + + def _analyze_positions(self) -> Dict: + """分析持仓表现""" + cursor = self.db.conn.execute(''' + SELECT stock_code, quantity, avg_cost, entry_price, + last_price, stop_loss, target_price, position_type, + timestamp, status + FROM positions + ''') + + positions = cursor.fetchall() + + if not positions: + return {"message": "没有持仓数据进行分析"} + + active_positions = [] + closed_positions = [] + total_profit = 0 + win_count = 0 + + for pos in positions: + profit = (pos[4] - pos[2]) * pos[1] # (last_price - avg_cost) * quantity + profit_pct = (pos[4] / pos[2] - 1) * 100 + + if pos[9] == 'ACTIVE': + active_positions.append({ + 'stock_code': pos[0], + 'profit': profit, + 'profit_pct': profit_pct + }) + else: + closed_positions.append({ + 'stock_code': pos[0], + 'profit': profit, + 'profit_pct': profit_pct + }) + + if profit > 0: + win_count += 1 + total_profit += profit + + return { + "active_positions": len(active_positions), + "closed_positions": len(closed_positions), + "total_profit": total_profit, + "win_rate": win_count / len(closed_positions) if closed_positions else 0, + "avg_profit_active": np.mean([p['profit'] for p in active_positions]) if active_positions else 0, + "avg_profit_closed": np.mean([p['profit'] for p in closed_positions]) if closed_positions else 0, + "best_position": max([p['profit_pct'] for p in active_positions + closed_positions]) if positions else 0, + "worst_position": min([p['profit_pct'] for p in active_positions + closed_positions]) if positions else 0 + } + +def main(): + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + filename='turtle_trader.log' + ) + + # 创建控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + console_handler.setFormatter(formatter) + logging.getLogger().addHandler(console_handler) + + try: + trader = TurtleTrader() + analyzer = PerformanceAnalyzer(trader.db) + + while True: + # 处理所有配置的股票 + for stock_code in trader.config['stock_codes']: + trader.process_stock(stock_code) + + # 定期进行性能分析 + if datetime.now().hour == 15: # 每天收盘后进行分析 + analysis = analyzer.analyze() + logging.info(f"每日性能分析报告: {json.dumps(analysis, indent=2)}") + + # 等待下一个检查周期 + time.sleep(trader.config['check_interval']) + + except KeyboardInterrupt: + logging.info("系统正常关闭") + except Exception as e: + logging.error(f"系统运行出错: {str(e)}") + raise + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/回测/海龟.xmind b/回测/海龟.xmind new file mode 100755 index 0000000..3584fc2 Binary files /dev/null and b/回测/海龟.xmind differ diff --git a/回测/海龟回测 1.xmind b/回测/海龟回测 1.xmind new file mode 100755 index 0000000..9f541b0 Binary files /dev/null and b/回测/海龟回测 1.xmind differ diff --git a/回测/海龟实时.xmind b/回测/海龟实时.xmind new file mode 100755 index 0000000..3867843 Binary files /dev/null and b/回测/海龟实时.xmind differ