QyJEHbuz
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
Wy)mU}k~B?X&SxByrUuHA9sGaXsR||l
literal 0
HcmV?d00001
diff --git a/src/backtest/__pycache__/router.cpython-311.pyc b/src/backtest/__pycache__/router.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5dc9f4e1166e6186cb3059d80512058386e28a63
GIT binary patch
literal 1211
zcmZ`&%TE(Q9G=;2w=W8csGtG4HWGYbDJp1u;e$X35~EF%X5E?Am2J1qZh82KM2L@Q
zV)Ww4=m}%=Knw@}3v1HDW=NqG^-D?|{%2Fl<#&PZ_y3tKYej3@4KxomhdN?gH6LKH8-j1RSSP~1m-
zZ~b+%fe_CUhD;chM!Zx>Nkm!n{Ix=W_%5S}e_fCcjPPRmHg0i58uV0m}iW8HN&(h_t<)dMnXK8(y2j`hHlwBXxXMVRpCmG
zGTkJ?C*%{^+)uNl!n~T9&)Pgp^l6>A-nks*fs}3-Nut^+_hrCe-J=&%zVEJn{ehXz)B|^
zCTfX^BI0dup5EGddaDKQipx7iC^tvctgUb6B&)T}M6H32m0w3rQ`jqO4w)GUCBx8&n)KmCZUq!LP*6yHaVQXIvv_9^5viI5Ed8MQv$^U52
jgFS_&&ab$uh`UNs7`MLNUqbNOgbf)<`)bimx4YRtz;HT%
literal 0
HcmV?d00001
diff --git a/src/backtest/__pycache__/rsi_strategy.cpython-311.pyc b/src/backtest/__pycache__/rsi_strategy.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8e81093e6e069614e56af94598060f0e18328f44
GIT binary patch
literal 14095
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
VfNARi)57v81X@Z{1x1tX_rKQ}lLG(%
literal 0
HcmV?d00001
diff --git a/src/backtest/__pycache__/service.cpython-311.pyc b/src/backtest/__pycache__/service.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ce8746d943a54ae4a05412d83a537cf2c2cb41f7
GIT binary patch
literal 2476
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
aV{^$%*(wC`)s(FvealAB_1{hDUi}ZM{m=XW
literal 0
HcmV?d00001
diff --git a/src/backtest/backtest.py b/src/backtest/backtest.py
new file mode 100644
index 0000000..bf5ef49
--- /dev/null
+++ b/src/backtest/backtest.py
@@ -0,0 +1,106 @@
+import bt
+import pandas as pd
+import numpy as np
+from xtquant import xtdata
+import matplotlib.pyplot as plt
+
+# 数据的列名
+columns = ['open', 'high', 'low', 'close', 'volume', 'amount', 'settelmentPrice',
+ 'openInterest', 'preClose', 'suspendFlag']
+
+
+# 获取本地数据并进行处理
+def get_local_data(field_list: list, stock_list: list, period: str, start_time: str, end_time: str,
+ count: int, dividend_type: str, fill_data: bool, data_dir: str):
+ result = xtdata.get_local_data(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time,
+ end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data,
+ data_dir=data_dir)
+ return data_processing(result)
+
+
+# 数据处理函数
+def data_processing(result_local):
+ # 初始化一个空的列表,用于存储每个股票的数据框
+ df_list = []
+
+ # 遍历字典中的 DataFrame
+ for stock_code, df in result_local.items():
+ # 确保 df 是一个 DataFrame
+ if isinstance(df, pd.DataFrame):
+ # 将时间戳转换为日期时间格式,只保留年-月-日
+ df['time'] = pd.to_datetime(df['time'], unit='ms').dt.date
+ # 将 'time' 列设置为索引,保留为日期格式
+ df.set_index('time', inplace=True)
+ # 指定列名
+ df.columns = columns
+ # 添加一列 'stock_code' 用于标识不同的股票
+ df['stock_code'] = stock_code
+ # 将 DataFrame 添加到列表中
+ df_list.append(df[['close']]) # 只保留 'close' 列
+ else:
+ print(f"数据格式错误: {stock_code} 不包含 DataFrame")
+
+ # 使用 pd.concat() 将所有 DataFrame 合并为一个大的 DataFrame
+ combined_df = pd.concat(df_list, axis=1)
+
+ # 确保返回的 DataFrame 索引是日期格式
+ combined_df.index = pd.to_datetime(combined_df.index)
+
+ # 打印最终的 DataFrame
+ print(combined_df)
+
+ return combined_df
+
+
+# 定义策略
+def moving_average_strategy(data, short_window=20, long_window=50):
+ data['Short_MA'] = data['close'].rolling(window=short_window, min_periods=1).mean()
+ data['Long_MA'] = data['close'].rolling(window=long_window, min_periods=1).mean()
+
+ data['Signal'] = 0
+ data.loc[data.index[short_window:], 'Signal'] = np.where(
+ data['Short_MA'][short_window:] > data['Long_MA'][short_window:], 1, 0)
+ data['Position'] = data['Signal'].diff()
+
+ return data
+
+
+# 定义策略函数
+def bt_strategy(data):
+ # 计算策略信号
+ # data = moving_average_strategy(data)
+
+ # 定义策略
+ dual_ma_strategy = bt.Strategy('Dual MA Strategy', [bt.algos.RunOnce(),
+ bt.algos.SelectAll(),
+ bt.algos.WeighEqually(),
+ bt.algos.Rebalance()])
+ return dual_ma_strategy
+
+
+# 运行回测
+def run_backtest():
+ # 生成数据
+ data = get_local_data(field_list=[], stock_list=["300391.SZ"], period='1d', start_time='', end_time='', count=-1,
+ dividend_type='none', fill_data=True, data_dir="")
+
+ # 创建策略
+ strategy = bt_strategy(data)
+
+ # 创建回测
+ portfolio = bt.Backtest(strategy, data)
+ result = bt.run(portfolio)
+
+ return result
+
+
+# 执行回测并显示结果
+if __name__ == "__main__":
+ result = run_backtest()
+ result.plot()
+ b = xtdata.get_sector_list()
+ print(b)
+ xtdata.download_sector_data()
+ a = result.stats
+ print(a)
+ plt.show()
diff --git a/src/backtest/bollinger.py b/src/backtest/bollinger.py
new file mode 100644
index 0000000..f6e020f
--- /dev/null
+++ b/src/backtest/bollinger.py
@@ -0,0 +1,79 @@
+import bt
+
+# 模拟从前端接收到的因子列表
+factors = [
+ {'name': 'SMA', 'period': 50},
+ {'name': 'EMA', 'period': 200},
+ {'name': 'RSI', 'period': 14},
+ {'name': 'BollingerBands', 'period': 20}
+]
+
+
+def create_indicator(name, **kwargs):
+ """
+ 根据名称和参数动态创建bt的技术指标。
+ """
+ if name == 'SMA':
+ return bt.indicators.SMA(kwargs['period'])
+ elif name == 'EMA':
+ return bt.indicators.EMA(kwargs['period'])
+ elif name == 'RSI':
+ return bt.indicators.RSI(kwargs['period'])
+ elif name == 'BollingerBands':
+ return bt.indicators.BollingerBands(kwargs['period'])
+
+ else:
+ raise ValueError(f"未知的指标名称: {name}")
+
+
+def create_dynamic_strategy(name, factors):
+ """
+ 根据传递的因子列表动态构建策略。
+ """
+ algos = [bt.algos.RunMonthly(), # 每月运行一次
+ bt.algos.SelectAll()] # 选择所有资产
+
+ # 动态生成指标选择逻辑
+ for factor in factors:
+ indicator = create_indicator(factor['name'], **factor)
+ if factor['name'] == 'RSI':
+ # RSI 特定的买入/卖出逻辑
+ buy_signal = bt.algos.SelectWhere(indicator < 30) # 超卖区域买入
+ sell_signal = bt.algos.SelectWhere(indicator > 70) # 超买区域卖出
+ algos.append(buy_signal)
+ algos.append(bt.algos.WeighTarget(1.0))
+ algos.append(bt.algos.Rebalance())
+ algos.append(sell_signal)
+ algos.append(bt.algos.WeighTarget(0.0))
+ algos.append(bt.algos.Rebalance())
+ elif factor['name'] == 'BollingerBands':
+ # 布林带特定的逻辑(买入接近下轨,卖出接近上轨)
+ buy_signal = bt.algos.SelectWhere(bt.data < indicator['lower'])
+ sell_signal = bt.algos.SelectWhere(bt.data > indicator['upper'])
+ algos.append(buy_signal)
+ algos.append(bt.algos.WeighTarget(1.0))
+ algos.append(bt.algos.Rebalance())
+ algos.append(sell_signal)
+ algos.append(bt.algos.WeighTarget(0.0))
+ algos.append(bt.algos.Rebalance())
+ # 可以为其他指标添加更多逻辑
+
+ return bt.Strategy(name, algos)
+
+
+# 创建动态策略
+dynamic_strategy = create_dynamic_strategy("Dynamic_Strategy", factors)
+
+# 获取数据
+data = bt.get('spy,agg', start='2020-01-01', end='2023-01-01')
+
+# 创建回测
+backtest = bt.Backtest(dynamic_strategy, data)
+
+# 运行回测
+result = bt.run(backtest)
+
+# 展示结果
+result.display()
+result.plot()
+
diff --git a/src/backtest/bollinger_bands.py b/src/backtest/bollinger_bands.py
new file mode 100644
index 0000000..576e26d
--- /dev/null
+++ b/src/backtest/bollinger_bands.py
@@ -0,0 +1,274 @@
+import asyncio
+import json
+import time
+from datetime import datetime
+
+import bt
+import numpy as np
+import pandas as pd
+from xtquant import xtdata
+import matplotlib.pyplot as plt
+
+from src.backtest.until import get_local_data, convert_pandas_to_json_serializable
+from src.models import wance_data_storage_backtest, wance_data_stock
+from src.tortoises_orm_config import init_tortoise
+
+
+# 布林带策略函数
+async def create_bollinger_bands_strategy(data, stock_code: str, bollingerMA: int = 50, std_dev: int = 200):
+ # 生成布林带策略信号
+ signal = await bollinger_bands_strategy(data, bollingerMA, std_dev)
+
+ # 使用bt框架构建策略
+ strategy = bt.Strategy(f'{stock_code} 布林带策略',
+ [bt.algos.RunDaily(),
+ bt.algos.SelectAll(), # 选择所有股票
+ bt.algos.WeighTarget(signal), # 根据信号调整权重
+ bt.algos.Rebalance()]) # 调仓
+ return strategy, signal
+
+
+async def bollinger_bands_strategy(df, window=20, num_std_dev=2):
+ """
+ 基于布林带策略生成买卖信号。
+
+ 参数:
+ df: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。
+ window: int, 计算布林带中轨线的窗口期。
+ num_std_dev: float, 标准差的倍数,用于计算上下轨。
+
+ 返回:
+ signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,0 表示卖出。
+ """
+ # 计算中轨线(移动平均)
+ middle_band = df.rolling(window=window, min_periods=1).mean()
+
+ # 计算滚动标准差
+ rolling_std = df.rolling(window=window, min_periods=1).std()
+
+ # 计算上轨线和下轨线
+ upper_band = middle_band + (rolling_std * num_std_dev)
+ lower_band = middle_band - (rolling_std * num_std_dev)
+
+ # 初始化信号 DataFrame
+ signal = pd.DataFrame(index=df.index, columns=df.columns)
+
+ # 生成买入信号:当价格突破下轨时
+ for column in df.columns:
+ signal[column] = np.where(df[column] < lower_band[column], 1, np.nan) # 买入信号
+
+ # 生成卖出信号:当价格突破上轨时
+ for column in df.columns:
+ signal[column] = np.where(df[column] > upper_band[column], 0, signal[column]) # 卖出信号
+
+ # 前向填充信号,持仓不变
+ signal = signal.ffill()
+
+ # 将剩余的 NaN 替换为 0
+ signal = signal.fillna(0)
+
+ return signal
+
+
+async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, bollingerMA,
+ std_dev):
+ await init_tortoise()
+
+ # 要存储的字段列表
+ fields_to_store = [
+ 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price',
+ 'price', 'returns', 'data_start_time', 'data_end_time',
+ 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr',
+ 'max_drawdown', 'calmar', 'mtd', 'three_month',
+ 'six_month', 'ytd', 'one_year', 'three_year',
+ 'five_year', 'ten_year', 'incep', 'daily_sharpe',
+ 'daily_sortino', 'daily_mean', 'daily_vol',
+ 'daily_skew', 'daily_kurt', 'best_day', 'worst_day',
+ 'monthly_sharpe', 'monthly_sortino', 'monthly_mean',
+ 'monthly_vol', 'monthly_skew', 'monthly_kurt',
+ 'best_month', 'worst_month', 'yearly_sharpe',
+ 'yearly_sortino', 'yearly_mean', 'yearly_vol',
+ 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year',
+ 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month',
+ 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc'
+ ]
+
+ # 准备要存储的数据
+ data_to_store = {
+ 'stock_code': stock_code,
+ 'strategy_name': "布林带策略",
+ 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign(
+ time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')),
+ 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices),
+ 'price': convert_pandas_to_json_serializable(result[source_column_name].prices),
+ 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)),
+ 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'),
+ 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'),
+ 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')),
+ 'position': convert_pandas_to_json_serializable(signal),
+ 'backtest_name': f'{stock_code} 布林带策略 MA{bollingerMA}-{std_dev}倍标准差',
+ 'indicator_type': 'Bollinger',
+ 'indicator_information': json.dumps({'bollingerMA': bollingerMA, 'std_dev': std_dev})
+ }
+
+ # 使用循环填充其他字段
+ for field in fields_to_store[12:]: # 从第10个字段开始
+ value = result.stats.loc[field].iloc[0]
+ data_to_store[field] = 0.0 if (isinstance(value, float) and np.isnan(value)) else value
+
+ # 检查是否存在该 backtest_name
+ existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=data_to_store['backtest_name']
+ ).first()
+
+ if existing_record:
+ # 如果存在,更新记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ id=existing_record.id
+ ).update(**data_to_store)
+ else:
+ # 如果不存在,创建新的记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store)
+
+ return data_to_store
+
+
+async def run_bollinger_backtest(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = 100,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ bollingerMA: int = 50,
+ std_dev: int = 200):
+ try:
+ # 初始化一个列表用于存储每只股票的回测结果字典
+ results_list = []
+
+ # 遍历每只股票的数据(每列代表一个股票的收盘价)
+ data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type,
+ fill_data,
+ data_dir)
+
+ for stock_code in stock_list:
+
+ data_column_name = f'close_{stock_code}'
+ source_column_name = f'{stock_code} 布林带策略'
+ backtest_name = f'{stock_code} 布林带策略 MA{bollingerMA}-{std_dev}倍标准差'
+ now_time = int(datetime.now().strftime('%Y%m%d'))
+ db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+ if db_result:
+ if db_result[0].backtest_end_time == now_time:
+ results_list.append({source_column_name: db_result[0]})
+
+ # elif data_column_name in data.columns:
+ if data_column_name in data.columns:
+ stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame
+ stock_data_series.columns = ['close'] # 重命名列为 'close'
+
+ # 创建布林带策略
+ strategy, signal = await create_bollinger_bands_strategy(stock_data_series, stock_code,
+ bollingerMA=bollingerMA,
+ std_dev=std_dev)
+ # 创建回测
+ backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000)
+ # 运行回测
+ result = bt.run(backtest)
+ # 存储回测结果
+ data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code,
+ stock_data_series,
+ bollingerMA, std_dev)
+ # # 绘制回测结果图表
+ # result.plot()
+ # # 绘制个别股票数据图表
+ # plt.figure(figsize=(12, 6))
+ # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price')
+ # plt.title(f'Stock Price for {stock_code}')
+ # plt.xlabel('Date')
+ # plt.ylabel('Price')
+ # plt.legend()
+ # plt.grid(True)
+ # plt.show()
+ # 将结果存储为字典并添加到列表中
+ results_list.append({source_column_name: data_to_store})
+
+ else:
+ print(f"数据中缺少列: {data_column_name}")
+
+ return results_list # 返回结果列表
+
+ except Exception as e:
+ print(f"Error occurred: {e}")
+
+
+async def start_bollinger_backtest_service(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = -1,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ bollingerMA: int = 50,
+ std_dev: int = 200):
+ for stock_code in stock_list:
+ backtest_name = f'{stock_code} 布林带策略 MA{bollingerMA}-{std_dev}倍标准差'
+ db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+ now_time = int(datetime.now().strftime('%Y%m%d'))
+
+ if db_result and db_result[0].backtest_end_time == now_time:
+ return db_result
+ else:
+ # 执行回测
+ result = await run_bollinger_backtest(
+ field_list=field_list,
+ stock_list=stock_list,
+ period=period,
+ start_time=start_time,
+ end_time=end_time,
+ count=count,
+ dividend_type=dividend_type,
+ fill_data=fill_data,
+ data_dir=data_dir,
+ bollingerMA=bollingerMA,
+ std_dev=std_dev,
+ )
+ return result
+
+
+async def init_backtest_db():
+ bollinger_list = [{"bollingerMA": 20, "std_dev": 2}, {"bollingerMA": 30, "std_dev": 2},
+ {"bollingerMA": 70, "std_dev": 2}, {"bollingerMA": 5, "std_dev": 1},
+ {"bollingerMA": 20, "std_dev": 3}, {"bollingerMA": 50, "std_dev": 2.5}]
+ await init_tortoise()
+ wance_db = await wance_data_stock.WanceDataStock.all()
+ bollinger_list_lenght = len(bollinger_list)
+
+ for stock_code in wance_db:
+ for i in range(bollinger_list_lenght):
+ bollingerMA = bollinger_list[i]['bollingerMA']
+ std_dev = bollinger_list[i]['std_dev']
+ source_column_name = f'{stock_code} 布林带策略 MA{bollingerMA}-{std_dev}倍标准差'
+ result = await run_bollinger_backtest(field_list=['close', 'time'],
+ stock_list=[stock_code.stock_code],
+ bollingerMA=bollingerMA,
+ std_dev=std_dev)
+
+ print(f"回测成功 {source_column_name}")
+
+
+if __name__ == '__main__':
+ # 测试类的回测
+ asyncio.run(run_bollinger_backtest(field_list=['close', 'time'],
+ stock_list=['601222.SH', '601677.SH'],
+ bollingerMA=20,
+ std_dev=2))
+
+ # # 初始化数据库表
+ # asyncio.run(init_backtest_db())
diff --git a/src/backtest/dual_moving_average.py b/src/backtest/dual_moving_average.py
new file mode 100644
index 0000000..d5c274f
--- /dev/null
+++ b/src/backtest/dual_moving_average.py
@@ -0,0 +1,267 @@
+import asyncio
+import json
+import time
+from datetime import datetime
+
+import bt
+import numpy as np
+import pandas as pd
+from xtquant import xtdata
+import matplotlib.pyplot as plt
+
+from src.backtest.until import get_local_data, convert_pandas_to_json_serializable
+from src.models import wance_data_storage_backtest, wance_data_stock
+from src.tortoises_orm_config import init_tortoise
+
+
+# 双均线策略函数
+async def create_dual_ma_strategy(data, stock_code: str, short_window: int = 50, long_window: int = 200):
+ # 生成双均线策略信号
+ signal = await dual_ma_strategy(data, short_window, long_window)
+
+ # 使用bt框架构建策略
+ strategy = bt.Strategy(f'{stock_code} 双均线策略',
+ [bt.algos.RunDaily(),
+ bt.algos.SelectAll(), # 选择所有股票
+ bt.algos.WeighTarget(signal), # 根据信号调整权重
+ bt.algos.Rebalance()]) # 调仓
+ return strategy, signal
+
+
+async def dual_ma_strategy(df, short_window=20, long_window=50):
+ """
+ 基于双均线策略生成买卖信号。
+
+ 参数:
+ df: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。
+ short_window: int, 短期均线窗口期。
+ long_window: int, 长期均线窗口期。
+
+ 返回:
+ signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,0 表示卖出。
+ """
+ # 计算短期均线和长期均线
+ short_ma = df.rolling(window=short_window, min_periods=1).mean()
+ long_ma = df.rolling(window=long_window, min_periods=1).mean()
+
+ # 生成买入信号: 当短期均线从下方穿过长期均线
+ buy_signal = np.where(short_ma > long_ma, 1, np.nan)
+
+ # 生成卖出信号: 当短期均线从上方穿过长期均线
+ sell_signal = np.where(short_ma < long_ma, 0, np.nan)
+
+ # 合并买卖信号
+ signal = pd.DataFrame(buy_signal, index=df.index, columns=df.columns)
+ signal = np.where(short_ma < long_ma, 0, signal)
+
+ # 前向填充信号,持仓不变
+ signal = pd.DataFrame(signal, index=df.index, columns=df.columns).ffill()
+
+ # 将剩余的 NaN 替换为 0
+ signal = signal.fillna(0)
+
+ return signal
+
+
+async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, short_window,
+ long_window):
+ await init_tortoise()
+
+ # 要存储的字段列表
+ fields_to_store = [
+ 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price',
+ 'price', 'returns', 'data_start_time', 'data_end_time',
+ 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr',
+ 'max_drawdown', 'calmar', 'mtd', 'three_month',
+ 'six_month', 'ytd', 'one_year', 'three_year',
+ 'five_year', 'ten_year', 'incep', 'daily_sharpe',
+ 'daily_sortino', 'daily_mean', 'daily_vol',
+ 'daily_skew', 'daily_kurt', 'best_day', 'worst_day',
+ 'monthly_sharpe', 'monthly_sortino', 'monthly_mean',
+ 'monthly_vol', 'monthly_skew', 'monthly_kurt',
+ 'best_month', 'worst_month', 'yearly_sharpe',
+ 'yearly_sortino', 'yearly_mean', 'yearly_vol',
+ 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year',
+ 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month',
+ 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc'
+ ]
+
+ # 准备要存储的数据
+ data_to_store = {
+ 'stock_code': stock_code,
+ 'strategy_name': "双均线策略",
+ 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign(
+ time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')),
+ 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices),
+ 'price': convert_pandas_to_json_serializable(result[source_column_name].prices),
+ 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)),
+ 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'),
+ 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'),
+ 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')),
+ 'position': convert_pandas_to_json_serializable(signal),
+ 'backtest_name': f'{stock_code} 双均线策略 MA{short_window}-{long_window}日',
+ 'indicator_type': 'SMA',
+ 'indicator_information': json.dumps({'short_window': short_window, 'long_window': long_window})
+ }
+
+ # 使用循环填充其他字段
+ for field in fields_to_store[12:]: # 从第10个字段开始
+ value = result.stats.loc[field].iloc[0]
+ data_to_store[field] = 0.0 if (isinstance(value, float) and np.isnan(value)) else value
+
+ # 检查是否存在该 backtest_name
+ existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=data_to_store['backtest_name']
+ ).first()
+
+ if existing_record:
+ # 如果存在,更新记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ id=existing_record.id
+ ).update(**data_to_store)
+ else:
+ # 如果不存在,创建新的记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store)
+
+ return data_to_store
+
+
+async def run_sma_backtest(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = 100,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ short_window: int = 50,
+ long_window: int = 200):
+ try:
+ # 初始化一个列表用于存储每只股票的回测结果字典
+ results_list = []
+
+ # 遍历每只股票的数据(每列代表一个股票的收盘价)
+ data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type,
+ fill_data,
+ data_dir)
+
+ for stock_code in stock_list:
+
+ data_column_name = f'close_{stock_code}'
+ source_column_name = f'{stock_code} 双均线策略'
+ backtest_name = f'{stock_code} 双均线策略 MA{short_window}-{long_window}日'
+ now_data = int(datetime.now().strftime('%Y%m%d'))
+ db_result_data = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+
+ if db_result_data:
+ if db_result_data[0].backtest_end_time == now_data:
+ results_list.append({source_column_name: db_result_data[0]})
+
+ if data_column_name in data.columns:
+ stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame
+ stock_data_series.columns = ['close'] # 重命名列为 'close'
+
+ # 创建双均线策略
+ strategy, signal = await create_dual_ma_strategy(stock_data_series, stock_code,
+ short_window=short_window,
+ long_window=long_window)
+ # 创建回测
+ backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000)
+ # 运行回测
+ result = bt.run(backtest)
+ # 存储回测结果
+ data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code,
+ stock_data_series,
+ short_window, long_window)
+ # # 绘制回测结果图表
+ # result.plot()
+ # # 绘制个别股票数据图表
+ # plt.figure(figsize=(12, 6))
+ # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price')
+ # plt.title(f'Stock Price for {stock_code}')
+ # plt.xlabel('Date')
+ # plt.ylabel('Price')
+ # plt.legend()
+ # plt.grid(True)
+ # plt.show()
+ # 将结果存储为字典并添加到列表中
+ results_list.append({source_column_name: data_to_store})
+
+ else:
+ print(f"数据中缺少列: {data_column_name}")
+
+ return results_list # 返回结果列表
+
+ except Exception as e:
+ print(f"Error occurred: {e}")
+
+
+async def start_sma_backtest_service(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = -1,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ short_window: int = 50,
+ long_window: int = 200):
+ for stock_code in stock_list:
+ backtest_name = f'{stock_code} 双均线策略 MA{short_window}-{long_window}日'
+ db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+ now_time = int(datetime.now().strftime('%Y%m%d'))
+
+ if db_result and db_result[0].backtest_end_time == now_time:
+ return db_result
+ else:
+ # 执行回测
+ result = await run_sma_backtest(
+ field_list=field_list,
+ stock_list=stock_list,
+ period=period,
+ start_time=start_time,
+ end_time=end_time,
+ count=count,
+ dividend_type=dividend_type,
+ fill_data=fill_data,
+ data_dir=data_dir,
+ short_window=short_window,
+ long_window=long_window,
+ )
+ return result
+
+
+async def init_backtest_db():
+ sma_list = [{"short_window": 5, "long_window": 10}, {"short_window": 10, "long_window": 30},
+ {"short_window": 30, "long_window": 60}, {"short_window": 30, "long_window": 90},
+ {"short_window": 70, "long_window": 140}, {"short_window": 120, "long_window": 250}]
+ await init_tortoise()
+ wance_db = await wance_data_stock.WanceDataStock.all()
+ sma_list_lenght = len(sma_list)
+
+ for stock_code in wance_db:
+ for i in range(sma_list_lenght):
+ short_window = sma_list[i]['short_window']
+ long_window = sma_list[i]['long_window']
+ source_column_name = f'{stock_code} 双均线策略 MA{short_window}-{long_window}日'
+ result = await run_sma_backtest(field_list=['close', 'time'],
+ stock_list=[stock_code.stock_code],
+ short_window=short_window,
+ long_window=long_window)
+
+ print(f"回测成功 {source_column_name}")
+
+
+if __name__ == '__main__':
+ # 测试类的回测
+ # asyncio.run(run_sma_backtest(field_list=['close', 'time'],
+ # stock_list=['601222.SH', '601677.SH'],
+ # short_window=10,
+ # long_window=30))
+
+ # 初始化数据库表
+ asyncio.run(init_backtest_db())
diff --git a/src/backtest/living_backtesting.py b/src/backtest/living_backtesting.py
new file mode 100644
index 0000000..6da418c
--- /dev/null
+++ b/src/backtest/living_backtesting.py
@@ -0,0 +1,92 @@
+import bt
+import pandas as pd
+import numpy as np
+from xtquant import xtdata
+import matplotlib.pyplot as plt
+
+# 数据的列名
+columns = ['open', 'high', 'low', 'close', 'volume', 'amount', 'settelmentPrice',
+ 'openInterest', 'preClose', 'suspendFlag']
+
+
+def get_local_data(field_list: list, stock_list: list, period: str, start_time: str, end_time: str,
+ count: int, dividend_type: str, fill_data: bool, data_dir: str):
+ result = xtdata.get_local_data(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time,
+ end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data,
+ data_dir=data_dir)
+ return data_processing(result)
+
+
+# 数据处理函数
+def data_processing(result_local):
+ # 初始化一个空的列表,用于存储每个股票的数据框
+ df_list = []
+
+ # 遍历字典中的 DataFrame
+ for stock_code, df in result_local.items():
+ # 确保 df 是一个 DataFrame
+ if isinstance(df, pd.DataFrame):
+ # 将时间戳转换为日期时间格式,只保留年-月-日
+ df['time'] = pd.to_datetime(df['time'], unit='ms').dt.date
+ # 将 'time' 列设置为索引,保留为日期格式
+ df.set_index('time', inplace=True)
+ # 指定列名
+ df.columns = columns
+ # 添加一列 'stock_code' 用于标识不同的股票
+ df['stock_code'] = stock_code
+ # 将 DataFrame 添加到列表中
+ df_list.append(df[['close']]) # 只保留 'close' 列
+ else:
+ print(f"数据格式错误: {stock_code} 不包含 DataFrame")
+
+ # 使用 pd.concat() 将所有 DataFrame 合并为一个大的 DataFrame
+ combined_df = pd.concat(df_list, axis=1)
+
+ # 确保返回的 DataFrame 索引是日期格式
+ combined_df.index = pd.to_datetime(combined_df.index)
+
+ # 打印最终的 DataFrame
+ print(combined_df)
+
+ return combined_df
+
+
+def bt_strategy(data):
+
+ # 计算策略信号
+ # data = moving_average_strategy(data)
+
+ # 定义策略
+ dual_ma_strategy = bt.Strategy('Dual MA Strategy', [bt.algos.RunOnce(),
+ bt.algos.SelectAll(),
+ bt.algos.WeighEqually(),
+ bt.algos.Rebalance()])
+ return dual_ma_strategy
+
+
+# 运行回测
+def run_backtest():
+ # 生成数据
+ data = get_local_data(field_list=[], stock_list=["300391.SZ"], period='1d', start_time='', end_time='', count=-1,
+ dividend_type='none', fill_data=True, data_dir="")
+
+ # 创建策略
+ strategy = bt_strategy(data)
+
+ # 创建回测
+ portfolio = bt.Backtest(strategy, data)
+ result = bt.run(portfolio)
+
+ return result
+
+
+# 执行回测并显示结果
+if __name__ == "__main__":
+ result = run_backtest()
+ result.plot()
+ b = xtdata.get_sector_list()
+ print(b)
+ xtdata.download_sector_data()
+ a = result.stats
+ print(a)
+ plt.show()
diff --git a/src/backtest/macd_strategy.py b/src/backtest/macd_strategy.py
new file mode 100644
index 0000000..7d23359
--- /dev/null
+++ b/src/backtest/macd_strategy.py
@@ -0,0 +1,271 @@
+import asyncio
+import json
+import time
+from datetime import datetime
+
+import bt
+import numpy as np
+import pandas as pd
+from xtquant import xtdata
+import matplotlib.pyplot as plt
+
+from src.backtest.until import get_local_data, convert_pandas_to_json_serializable
+from src.models import wance_data_storage_backtest, wance_data_stock
+from src.tortoises_orm_config import init_tortoise
+
+
+# MACD策略函数
+async def create_dual_ma_strategy(data, stock_code: str, short_window: int = 50, long_window: int = 200):
+ # 生成MACD策略信号
+ signal = await macd_strategy(data, short_window, long_window)
+
+ # 使用bt框架构建策略
+ strategy = bt.Strategy(f'{stock_code} MACD策略',
+ [bt.algos.RunDaily(),
+ bt.algos.SelectAll(), # 选择所有股票
+ bt.algos.WeighTarget(signal), # 根据信号调整权重
+ bt.algos.Rebalance()]) # 调仓
+ return strategy, signal
+
+
+# 定义 MACD 策略的函数
+def macd_strategy(data, short_window=12, long_window=26, signal_window=9):
+ """
+ MACD 策略,当 MACD 线穿过信号线时买入,反之卖出。
+
+ 参数:
+ data: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。
+ short_window: int, 短期 EMA 的窗口期。
+ long_window: int, 长期 EMA 的窗口期。
+ signal_window: int, 信号线 EMA 的窗口期。
+
+ 返回:
+ signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,-1 表示卖出。
+ """
+ # 计算短期和长期的 EMA
+ short_ema = data.ewm(span=short_window, adjust=False).mean()
+ long_ema = data.ewm(span=long_window, adjust=False).mean()
+
+ # 计算 MACD 线
+ macd_line = short_ema - long_ema
+
+ # 计算信号线
+ signal_line = macd_line.ewm(span=signal_window, adjust=False).mean()
+
+ # 生成买入和卖出信号
+ signal = pd.DataFrame(index=data.index, columns=data.columns)
+ for column in data.columns:
+ signal[column] = 0 # 初始化信号为 0
+ # 买入信号:MACD 线从下方穿过信号线
+ signal[column] = (macd_line[column] > signal_line[column]) & (macd_line[column].shift(1) <= signal_line[column].shift(1)).astype(int)
+ # 卖出信号:MACD 线从上方穿过信号线
+ signal[column] = (macd_line[column] < signal_line[column]) & (macd_line[column].shift(1) >= signal_line[column].shift(1)).astype(int) * -1 + signal[column]
+
+ # 前向填充信号,保持持仓不变
+ signal = signal.ffill().fillna(0)
+
+ return signal
+
+
+async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, short_window,
+ long_window):
+ await init_tortoise()
+
+ # 要存储的字段列表
+ fields_to_store = [
+ 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price',
+ 'price', 'returns', 'data_start_time', 'data_end_time',
+ 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr',
+ 'max_drawdown', 'calmar', 'mtd', 'three_month',
+ 'six_month', 'ytd', 'one_year', 'three_year',
+ 'five_year', 'ten_year', 'incep', 'daily_sharpe',
+ 'daily_sortino', 'daily_mean', 'daily_vol',
+ 'daily_skew', 'daily_kurt', 'best_day', 'worst_day',
+ 'monthly_sharpe', 'monthly_sortino', 'monthly_mean',
+ 'monthly_vol', 'monthly_skew', 'monthly_kurt',
+ 'best_month', 'worst_month', 'yearly_sharpe',
+ 'yearly_sortino', 'yearly_mean', 'yearly_vol',
+ 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year',
+ 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month',
+ 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc'
+ ]
+
+ # 准备要存储的数据
+ data_to_store = {
+ 'stock_code': stock_code,
+ 'strategy_name': "MACD策略",
+ 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign(
+ time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')),
+ 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices),
+ 'price': convert_pandas_to_json_serializable(result[source_column_name].prices),
+ 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)),
+ 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'),
+ 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'),
+ 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')),
+ 'position': convert_pandas_to_json_serializable(signal),
+ 'backtest_name': f'{stock_code} MACD策略 MA{short_window}-{long_window}日',
+ 'indicator_type': 'MACD',
+ 'indicator_information': json.dumps({'short_window': short_window, 'long_window': long_window})
+ }
+
+ # 使用循环填充其他字段
+ for field in fields_to_store[12:]: # 从第10个字段开始
+ value = result.stats.loc[field].iloc[0]
+ data_to_store[field] = 0.0 if (isinstance(value, float) and np.isnan(value)) else value
+
+ # 检查是否存在该 backtest_name
+ existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=data_to_store['backtest_name']
+ ).first()
+
+ if existing_record:
+ # 如果存在,更新记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ id=existing_record.id
+ ).update(**data_to_store)
+ else:
+ # 如果不存在,创建新的记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store)
+
+ return data_to_store
+
+
+async def run_macd_backtest(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = 100,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ short_window: int = 50,
+ long_window: int = 200):
+ try:
+ # 初始化一个列表用于存储每只股票的回测结果字典
+ results_list = []
+
+ # 遍历每只股票的数据(每列代表一个股票的收盘价)
+ data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type,
+ fill_data,
+ data_dir)
+
+ for stock_code in stock_list:
+
+ data_column_name = f'close_{stock_code}'
+ source_column_name = f'{stock_code} MACD策略'
+ backtest_name = f'{stock_code} MACD策略 MA{short_window}-{long_window}日'
+ now_data = int(datetime.now().strftime('%Y%m%d'))
+ db_result_data = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+
+ if db_result_data:
+ if db_result_data[0].backtest_end_time == now_data:
+ results_list.append({source_column_name: db_result_data[0]})
+
+ elif data_column_name in data.columns:
+ stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame
+ stock_data_series.columns = ['close'] # 重命名列为 'close'
+
+ # 创建MACD策略
+ strategy, signal = await create_dual_ma_strategy(stock_data_series, stock_code,
+ short_window=short_window,
+ long_window=long_window)
+ # 创建回测
+ backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000)
+ # 运行回测
+ result = bt.run(backtest)
+ # 存储回测结果
+ data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code,
+ stock_data_series,
+ short_window, long_window)
+ # # 绘制回测结果图表
+ # result.plot()
+ # # 绘制个别股票数据图表
+ # plt.figure(figsize=(12, 6))
+ # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price')
+ # plt.title(f'Stock Price for {stock_code}')
+ # plt.xlabel('Date')
+ # plt.ylabel('Price')
+ # plt.legend()
+ # plt.grid(True)
+ # plt.show()
+ # 将结果存储为字典并添加到列表中
+ results_list.append({source_column_name: data_to_store})
+
+ else:
+ print(f"数据中缺少列: {data_column_name}")
+
+ return results_list # 返回结果列表
+
+ except Exception as e:
+ print(f"Error occurred: {e}")
+
+
+async def start_macd_backtest_service(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = -1,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ short_window: int = 50,
+ long_window: int = 200):
+ for stock_code in stock_list:
+ backtest_name = f'{stock_code} MACD策略 MA{short_window}-{long_window}日'
+ db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+ now_time = int(datetime.now().strftime('%Y%m%d'))
+
+ if db_result and db_result[0].backtest_end_time == now_time:
+ return db_result
+ else:
+ # 执行回测
+ result = await run_macd_backtest(
+ field_list=field_list,
+ stock_list=stock_list,
+ period=period,
+ start_time=start_time,
+ end_time=end_time,
+ count=count,
+ dividend_type=dividend_type,
+ fill_data=fill_data,
+ data_dir=data_dir,
+ short_window=short_window,
+ long_window=long_window,
+ )
+ return result
+
+
+async def init_backtest_db():
+ MACD_list = [{"short_window": 5, "long_window": 10}, {"short_window": 10, "long_window": 30},
+ {"short_window": 30, "long_window": 60}, {"short_window": 30, "long_window": 90},
+ {"short_window": 70, "long_window": 140}, {"short_window": 120, "long_window": 250}]
+ await init_tortoise()
+ wance_db = await wance_data_stock.WanceDataStock.all()
+ MACD_list_lenght = len(MACD_list)
+
+ for stock_code in wance_db:
+ for i in range(MACD_list_lenght):
+ short_window = MACD_list[i]['short_window']
+ long_window = MACD_list[i]['long_window']
+ source_column_name = f'{stock_code} MACD策略 MA{short_window}-{long_window}日'
+ result = await run_macd_backtest(field_list=['close', 'time'],
+ stock_list=[stock_code.stock_code],
+ short_window=short_window,
+ long_window=long_window)
+
+ print(f"回测成功 {source_column_name}")
+
+
+if __name__ == '__main__':
+ # 测试类的回测
+ # asyncio.run(run_macd_backtest(field_list=['close', 'time'],
+ # stock_list=['601222.SH', '601677.SH'],
+ # short_window=10,
+ # long_window=30))
+
+ # 初始化数据库表
+ asyncio.run(init_backtest_db())
diff --git a/src/backtest/reverse_dual_ma_strategy.py b/src/backtest/reverse_dual_ma_strategy.py
new file mode 100644
index 0000000..2417c53
--- /dev/null
+++ b/src/backtest/reverse_dual_ma_strategy.py
@@ -0,0 +1,264 @@
+import asyncio
+import json
+import time
+from datetime import datetime
+
+import bt
+import numpy as np
+import pandas as pd
+from xtquant import xtdata
+import matplotlib.pyplot as plt
+
+from src.backtest.until import get_local_data, convert_pandas_to_json_serializable
+from src.models import wance_data_storage_backtest, wance_data_stock
+from src.tortoises_orm_config import init_tortoise
+
+
+# 反双均线策略函数
+async def create_dual_ma_strategy(data, stock_code: str, short_window: int = 50, long_window: int = 200):
+ # 生成反双均线策略信号
+ signal = await reverse_dual_ma_strategy(data, short_window, long_window)
+
+ # 使用bt框架构建策略
+ strategy = bt.Strategy(f'{stock_code} 反双均线策略',
+ [bt.algos.RunDaily(),
+ bt.algos.SelectAll(), # 选择所有股票
+ bt.algos.WeighTarget(signal), # 根据信号调整权重
+ bt.algos.Rebalance()]) # 调仓
+ return strategy, signal
+
+
+
+# 定义反反双均线策略的函数
+def reverse_dual_ma_strategy(data, short_window=50, long_window=200):
+ """
+ 反反双均线策略,当短期均线跌破长期均线时买入,穿过长期均线时卖出。
+
+ 参数:
+ data: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。
+ short_window: int, 短期均线的窗口期。
+ long_window: int, 长期均线的窗口期。
+
+ 返回:
+ signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,0 表示卖出。
+ """
+ # 计算短期均线和长期均线
+ short_ma = data.rolling(window=short_window).mean()
+ long_ma = data.rolling(window=long_window).mean()
+
+ # 初始化信号 DataFrame
+ signal = pd.DataFrame(index=data.index, columns=data.columns)
+
+ # 生成买入信号:短期均线从上往下穿过长期均线
+ for column in data.columns:
+ signal[column] = (short_ma[column] < long_ma[column]).astype(int) # 跌破时买入,信号为1
+ signal[column] = (short_ma[column] > long_ma[column]).astype(int) * -1 + signal[column] # 穿过时卖出,信号为0
+
+ # 前向填充信号,保持持仓不变
+ signal = signal.ffill()
+
+ return signal
+
+
+async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, short_window,
+ long_window):
+ await init_tortoise()
+
+ # 要存储的字段列表
+ fields_to_store = [
+ 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price',
+ 'price', 'returns', 'data_start_time', 'data_end_time',
+ 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr',
+ 'max_drawdown', 'calmar', 'mtd', 'three_month',
+ 'six_month', 'ytd', 'one_year', 'three_year',
+ 'five_year', 'ten_year', 'incep', 'daily_sharpe',
+ 'daily_sortino', 'daily_mean', 'daily_vol',
+ 'daily_skew', 'daily_kurt', 'best_day', 'worst_day',
+ 'monthly_sharpe', 'monthly_sortino', 'monthly_mean',
+ 'monthly_vol', 'monthly_skew', 'monthly_kurt',
+ 'best_month', 'worst_month', 'yearly_sharpe',
+ 'yearly_sortino', 'yearly_mean', 'yearly_vol',
+ 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year',
+ 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month',
+ 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc'
+ ]
+
+ # 准备要存储的数据
+ data_to_store = {
+ 'stock_code': stock_code,
+ 'strategy_name': "反双均线策略",
+ 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign(
+ time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')),
+ 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices),
+ 'price': convert_pandas_to_json_serializable(result[source_column_name].prices),
+ 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)),
+ 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'),
+ 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'),
+ 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')),
+ 'position': convert_pandas_to_json_serializable(signal),
+ 'backtest_name': f'{stock_code} 反双均线策略 MA{short_window}-{long_window}日',
+ 'indicator_type': 'reverse_SMA',
+ 'indicator_information': json.dumps({'short_window': short_window, 'long_window': long_window})
+ }
+
+ # 使用循环填充其他字段
+ for field in fields_to_store[12:]: # 从第10个字段开始
+ value = result.stats.loc[field].iloc[0]
+ data_to_store[field] = 0.0 if (isinstance(value, float) and np.isnan(value)) else value
+
+ # 检查是否存在该 backtest_name
+ existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=data_to_store['backtest_name']
+ ).first()
+
+ if existing_record:
+ # 如果存在,更新记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ id=existing_record.id
+ ).update(**data_to_store)
+ else:
+ # 如果不存在,创建新的记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store)
+
+ return data_to_store
+
+
+async def run_reverse_reverse_SMA_backtest(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = 100,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ short_window: int = 50,
+ long_window: int = 200):
+ try:
+ # 初始化一个列表用于存储每只股票的回测结果字典
+ results_list = []
+
+ # 遍历每只股票的数据(每列代表一个股票的收盘价)
+ data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type,
+ fill_data,
+ data_dir)
+
+ for stock_code in stock_list:
+
+ data_column_name = f'close_{stock_code}'
+ source_column_name = f'{stock_code} 反双均线策略'
+ backtest_name = f'{stock_code} 反双均线策略 MA{short_window}-{long_window}日'
+ now_data = int(datetime.now().strftime('%Y%m%d'))
+ db_result_data = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+
+ if db_result_data:
+ if db_result_data[0].backtest_end_time == now_data:
+ results_list.append({source_column_name: db_result_data[0]})
+
+ elif data_column_name in data.columns:
+ stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame
+ stock_data_series.columns = ['close'] # 重命名列为 'close'
+
+ # 创建反双均线策略
+ strategy, signal = await create_dual_ma_strategy(stock_data_series, stock_code,
+ short_window=short_window,
+ long_window=long_window)
+ # 创建回测
+ backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000)
+ # 运行回测
+ result = bt.run(backtest)
+ # 存储回测结果
+ data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code,
+ stock_data_series,
+ short_window, long_window)
+ # # 绘制回测结果图表
+ # result.plot()
+ # # 绘制个别股票数据图表
+ # plt.figure(figsize=(12, 6))
+ # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price')
+ # plt.title(f'Stock Price for {stock_code}')
+ # plt.xlabel('Date')
+ # plt.ylabel('Price')
+ # plt.legend()
+ # plt.grid(True)
+ # plt.show()
+ # 将结果存储为字典并添加到列表中
+ results_list.append({source_column_name: data_to_store})
+
+ else:
+ print(f"数据中缺少列: {data_column_name}")
+
+ return results_list # 返回结果列表
+
+ except Exception as e:
+ print(f"Error occurred: {e}")
+
+
+async def start_reverse_SMA_backtest_service(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = -1,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ short_window: int = 50,
+ long_window: int = 200):
+ for stock_code in stock_list:
+ backtest_name = f'{stock_code} 反双均线策略 MA{short_window}-{long_window}日'
+ db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+ now_time = int(datetime.now().strftime('%Y%m%d'))
+
+ if db_result and db_result[0].backtest_end_time == now_time:
+ return db_result
+ else:
+ # 执行回测
+ result = await run_reverse_reverse_SMA_backtest(
+ field_list=field_list,
+ stock_list=stock_list,
+ period=period,
+ start_time=start_time,
+ end_time=end_time,
+ count=count,
+ dividend_type=dividend_type,
+ fill_data=fill_data,
+ data_dir=data_dir,
+ short_window=short_window,
+ long_window=long_window,
+ )
+ return result
+
+
+async def init_backtest_db():
+ reverse_SMA_list = [{"short_window": 5, "long_window": 10}, {"short_window": 10, "long_window": 30},
+ {"short_window": 30, "long_window": 60}, {"short_window": 30, "long_window": 90},
+ {"short_window": 70, "long_window": 140}, {"short_window": 120, "long_window": 250}]
+ await init_tortoise()
+ wance_db = await wance_data_stock.WanceDataStock.all()
+ reverse_SMA_list_lenght = len(reverse_SMA_list)
+
+ for stock_code in wance_db:
+ for i in range(reverse_SMA_list_lenght):
+ short_window = reverse_SMA_list[i]['short_window']
+ long_window = reverse_SMA_list[i]['long_window']
+ source_column_name = f'{stock_code} 反双均线策略 MA{short_window}-{long_window}日'
+ result = await run_reverse_reverse_SMA_backtest(field_list=['close', 'time'],
+ stock_list=[stock_code.stock_code],
+ short_window=short_window,
+ long_window=long_window)
+
+ print(f"回测成功 {source_column_name}")
+
+
+if __name__ == '__main__':
+ # 测试类的回测
+ # asyncio.run(run_reverse_SMA_backtest(field_list=['close', 'time'],
+ # stock_list=['601222.SH', '601677.SH'],
+ # short_window=10,
+ # long_window=30))
+
+ # 初始化数据库表
+ asyncio.run(init_backtest_db())
diff --git a/src/backtest/router.py b/src/backtest/router.py
new file mode 100644
index 0000000..4eed6ea
--- /dev/null
+++ b/src/backtest/router.py
@@ -0,0 +1,23 @@
+from fastapi import APIRouter, HTTPException # 从 FastAPI 中导入 APIRouter,用于创建 API 路由器
+
+from src.backtest.service import start_backtest_service
+from src.pydantic.backtest_request import BackRequest
+
+router = APIRouter() # 创建一个 FastAPI 路由器实例
+
+
+@router.get("/start_backtest")
+async def start_backtest(request: BackRequest):
+ result = await start_backtest_service(field_list=['close', 'time'],
+ stock_list=request.stock_list,
+ period=request.period,
+ start_time=request.start_time,
+ end_time=request.end_time,
+ count=request.count,
+ dividend_type=request.dividend_type,
+ fill_data=request.fill_data,
+ ma_type=request.ma_type,
+ short_window=request.short_window,
+ long_window=request.long_window
+ )
+ return result
diff --git a/src/backtest/rsi_strategy.py b/src/backtest/rsi_strategy.py
new file mode 100644
index 0000000..9832db2
--- /dev/null
+++ b/src/backtest/rsi_strategy.py
@@ -0,0 +1,296 @@
+import asyncio
+import json
+import time
+from datetime import datetime
+
+import bt
+import numpy as np
+import pandas as pd
+from xtquant import xtdata
+import matplotlib.pyplot as plt
+
+from src.backtest.until import get_local_data, convert_pandas_to_json_serializable
+from src.models import wance_data_storage_backtest, wance_data_stock
+from src.tortoises_orm_config import init_tortoise
+
+
+# RSI策略函数
+async def create_dual_ma_strategy(data, stock_code: str, short_window: int = 50, long_window: int = 200,
+ overbought: int = 70, oversold: int = 30):
+ # 生成RSI策略信号
+ signal = await rsi_strategy(data, short_window, long_window, overbought, oversold)
+
+ # 使用bt框架构建策略
+ strategy = bt.Strategy(f'{stock_code} RSI策略',
+ [bt.algos.RunDaily(),
+ bt.algos.SelectAll(), # 选择所有股票
+ bt.algos.WeighTarget(signal), # 根据信号调整权重
+ bt.algos.Rebalance()]) # 调仓
+ return strategy, signal
+
+
+async def rsi_strategy(df, short_window=14, long_window=28, overbought=70, oversold=30):
+ """
+ 基于RSI的策略生成买卖信号。
+
+ 参数:
+ df: pd.DataFrame, 股票的价格数据,行索引为日期,列为股票代码。
+ short_window: int, 短期RSI的窗口期。
+ long_window: int, 长期RSI的窗口期。
+ overbought: int, 超买水平。
+ oversold: int, 超卖水平。
+
+ 返回:
+ signal: pd.DataFrame, 每只股票的买卖信号,1 表示买入,0 表示卖出。
+ """
+ delta = df.diff().fillna(0)
+
+ gain = (delta.where(delta > 0, 0).rolling(window=short_window).mean()).fillna(0)
+ loss = (-delta.where(delta < 0, 0).rolling(window=short_window).mean()).fillna(0)
+
+ short_rsi = (100 - (100 / (1 + (gain / loss)))).fillna(0)
+
+ long_gain = (delta.where(delta > 0, 0).rolling(window=long_window).mean()).fillna(0)
+ long_loss = (-delta.where(delta < 0, 0).rolling(window=long_window).mean()).fillna(0)
+
+ long_rsi = (100 - (100 / (1 + (long_gain / long_loss)))).fillna(0)
+
+ signal = pd.DataFrame(index=df.index, columns=df.columns)
+
+ for column in df.columns:
+ signal[column] = np.where((short_rsi[column] < 30) & (long_rsi[column] < 30) & (short_rsi[column] != 0) & (long_rsi[column] != 0), 1, 0)
+ signal[column] = np.where((short_rsi[column] > 70) & (long_rsi[column] > 70) & (short_rsi[column] != 0) & (long_rsi[column] != 0), 0, signal[column])
+
+ return signal.ffill().fillna(0)
+
+
+async def storage_backtest_data(source_column_name, result, signal, stock_code, stock_data_series, short_window: int,
+ long_window: int, overbought: int = 70,
+ oversold: int = 30):
+ await init_tortoise()
+
+ # 要存储的字段列表
+ fields_to_store = [
+ 'stock_code', 'strategy_name', 'stock_close_price', 'daily_price',
+ 'price', 'returns', 'data_start_time', 'data_end_time',
+ 'backtest_end_time', 'position', 'backtest_name', 'rf', 'total_return', 'cagr',
+ 'max_drawdown', 'calmar', 'mtd', 'three_month',
+ 'six_month', 'ytd', 'one_year', 'three_year',
+ 'five_year', 'ten_year', 'incep', 'daily_sharpe',
+ 'daily_sortino', 'daily_mean', 'daily_vol',
+ 'daily_skew', 'daily_kurt', 'best_day', 'worst_day',
+ 'monthly_sharpe', 'monthly_sortino', 'monthly_mean',
+ 'monthly_vol', 'monthly_skew', 'monthly_kurt',
+ 'best_month', 'worst_month', 'yearly_sharpe',
+ 'yearly_sortino', 'yearly_mean', 'yearly_vol',
+ 'yearly_skew', 'yearly_kurt', 'best_year', 'worst_year',
+ 'avg_drawdown', 'avg_drawdown_days', 'avg_up_month',
+ 'avg_down_month', 'win_year_perc', 'twelve_month_win_perc'
+ ]
+
+ # 准备要存储的数据
+ data_to_store = {
+ 'stock_code': stock_code,
+ 'strategy_name': "RSI策略",
+ 'stock_close_price': json.dumps(stock_data_series.fillna(0).rename_axis('time').reset_index().assign(
+ time=stock_data_series.index.strftime('%Y%m%d')).set_index('time').to_dict(orient='index')),
+ 'daily_price': convert_pandas_to_json_serializable(result[source_column_name].daily_prices),
+ 'price': convert_pandas_to_json_serializable(result[source_column_name].prices),
+ 'returns': convert_pandas_to_json_serializable(result[source_column_name].returns.fillna(0)),
+ 'data_start_time': pd.to_datetime(result.stats.loc["start"].iloc[0]).strftime('%Y%m%d'),
+ 'data_end_time': pd.to_datetime(result.stats.loc["end"].iloc[0]).strftime('%Y%m%d'),
+ 'backtest_end_time': int(datetime.now().strftime('%Y%m%d')),
+ 'position': convert_pandas_to_json_serializable(signal),
+ 'backtest_name': f'{stock_code} RSI策略 RSI{short_window}-RSI{long_window}-overbought{overbought}-oversold{oversold}',
+ 'indicator_type': 'RSI',
+ 'indicator_information': json.dumps(
+ {'short_window': short_window, 'long_window': long_window, 'overbought': overbought, 'oversold': oversold})
+ }
+
+ # 使用循环填充其他字段
+ for field in fields_to_store[12:]: # 从第12个字段开始
+ value = result.stats.loc[field].iloc[0]
+
+ if isinstance(value, float):
+ if np.isnan(value):
+ data_to_store[field] = 0.0 # NaN 处理为 0
+ elif np.isinf(value): # 判断是否为无穷大或无穷小
+ if value > 0:
+ data_to_store[field] = 99999.9999 # 正无穷处理
+ else:
+ data_to_store[field] = -99999.9999 # 负无穷处理
+ else:
+ data_to_store[field] = value # 正常的浮点值
+ else:
+ data_to_store[field] = value # 非浮点类型保持不变
+
+ # 检查是否存在该 backtest_name
+ existing_record = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=data_to_store['backtest_name']
+ ).first()
+
+ if existing_record:
+ # 如果存在,更新记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ id=existing_record.id
+ ).update(**data_to_store)
+ else:
+ # 如果不存在,创建新的记录
+ await wance_data_storage_backtest.WanceDataStorageBacktest.create(**data_to_store)
+
+ return data_to_store
+
+
+async def run_rsi_backtest(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = 100,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ short_window: int = 50,
+ long_window: int = 200,
+ overbought: int = 70,
+ oversold: int = 30
+ ):
+ try:
+ # 初始化一个列表用于存储每只股票的回测结果字典
+ results_list = []
+
+ # 遍历每只股票的数据(每列代表一个股票的收盘价)
+ data = await get_local_data(field_list, stock_list, period, start_time, end_time, count, dividend_type,
+ fill_data,
+ data_dir)
+
+ for stock_code in stock_list:
+
+ data_column_name = f'close_{stock_code}'
+ source_column_name = f'{stock_code} RSI策略'
+ backtest_name = f'{stock_code} RSI策略 RSI{short_window}-RSI{long_window}'
+ now_data = int(datetime.now().strftime('%Y%m%d'))
+ db_result_data = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+
+ if db_result_data:
+ if db_result_data[0].backtest_end_time == now_data:
+ results_list.append({source_column_name: db_result_data[0]})
+
+ # elif data_column_name in data.columns:
+ if data_column_name in data.columns:
+ stock_data_series = data[[data_column_name]] # 提取该股票的收盘价 DataFrame
+ stock_data_series.columns = ['close'] # 重命名列为 'close'
+
+ # 创建RSI策略
+ strategy, signal = await create_dual_ma_strategy(stock_data_series, stock_code,
+ short_window=short_window, long_window=long_window)
+ # 创建回测
+ backtest = bt.Backtest(strategy=strategy, data=stock_data_series, initial_capital=100000)
+ # 运行回测
+ result = bt.run(backtest)
+ # 存储回测结果
+ data_to_store = await storage_backtest_data(source_column_name, result, signal, stock_code,
+ stock_data_series, short_window, long_window, overbought,
+ oversold)
+ # # 绘制回测结果图表
+ # result.plot()
+ # # 绘制个别股票数据图表
+ # plt.figure(figsize=(12, 6))
+ # plt.plot(stock_data_series.index, stock_data_series['close'], label='Stock Price')
+ # plt.title(f'Stock Price for {stock_code}')
+ # plt.xlabel('Date')
+ # plt.ylabel('Price')
+ # plt.legend()
+ # plt.grid(True)
+ # plt.show()
+ # 将结果存储为字典并添加到列表中
+ results_list.append({source_column_name: data_to_store})
+
+ else:
+ print(f"数据中缺少列: {data_column_name}")
+
+ return results_list # 返回结果列表
+
+ except Exception as e:
+ print(f"Error occurred: {e}")
+
+
+async def start_rsi_backtest_service(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = -1,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ short_window: int = 50,
+ long_window: int = 200,
+ overbought: int = 70,
+ oversold: int = 30
+ ):
+ for stock_code in stock_list:
+ backtest_name = f'{stock_code} RSI策略 RSI{short_window}-RSI{long_window}'
+ db_result = await wance_data_storage_backtest.WanceDataStorageBacktest.filter(
+ backtest_name=backtest_name)
+ now_time = int(datetime.now().strftime('%Y%m%d'))
+
+ if db_result and db_result[0].backtest_end_time == now_time:
+ return db_result
+ else:
+ # 执行回测
+ result = await run_rsi_backtest(
+ field_list=field_list,
+ stock_list=stock_list,
+ period=period,
+ start_time=start_time,
+ end_time=end_time,
+ count=count,
+ dividend_type=dividend_type,
+ fill_data=fill_data,
+ data_dir=data_dir,
+ short_window=short_window,
+ long_window=long_window,
+ overbought=overbought,
+ oversold=oversold
+ )
+ return result
+
+
+async def init_backtest_db():
+ sma_list = [{"short_window": 3, "long_window": 6}, {"short_window": 6, "long_window": 12},
+ {"short_window": 12, "long_window": 24}, {"short_window": 14, "long_window": 18},
+ {"short_window": 15, "long_window": 10}]
+ await init_tortoise()
+ wance_db = await wance_data_stock.WanceDataStock.all()
+ sma_list_lenght = len(sma_list)
+
+ for stock_code in wance_db:
+ for i in range(sma_list_lenght):
+ short_window = sma_list[i]['short_window']
+ long_window = sma_list[i]['long_window']
+ source_column_name = f'{stock_code} RSI策略 RSI{short_window}-RSI{long_window}'
+ result = await start_rsi_backtest_service(field_list=['close', 'time'],
+ stock_list=[stock_code.stock_code],
+ short_window=short_window,
+ long_window=long_window,
+ overbought=70,
+ oversold=30)
+
+ print(f"回测成功 {source_column_name}")
+
+
+if __name__ == '__main__':
+ # 测试类的回测
+ asyncio.run(run_rsi_backtest(field_list=['close', 'time'],
+ stock_list=['601222.SH', '601677.SH'],
+ count=-1,
+ short_window=10,
+ long_window=30,
+ overbought=70,
+ oversold=30
+ ))
+
+ # # 初始化数据库表
+ # asyncio.run(init_backtest_db())
diff --git a/src/backtest/service.py b/src/backtest/service.py
new file mode 100644
index 0000000..e07e9b8
--- /dev/null
+++ b/src/backtest/service.py
@@ -0,0 +1,74 @@
+from src.backtest.bollinger_bands import run_bollinger_backtest, start_bollinger_backtest_service
+from src.backtest.dual_moving_average import run_sma_backtest, start_sma_backtest_service
+from src.backtest.reverse_dual_ma_strategy import start_reverse_SMA_backtest_service
+from src.backtest.rsi_strategy import start_rsi_backtest_service
+from src.backtest.until import data_check
+
+
+async def start_backtest_service(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = -1,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '',
+ ma_type: str = 'SMA',
+ short_window: int = 50,
+ long_window: int = 200,
+ bollingerMA: int = 200,
+ std_dev: int = 200,
+ overbought: int = 70,
+ oversold: int = 30,
+ signal_window: int = 9):
+ # 数据检查
+ await data_check(field_list=field_list,
+ stock_list=stock_list,
+ period=period,
+ start_time=start_time,
+ end_time=end_time,
+ count=count,
+ dividend_type=dividend_type,
+ fill_data=fill_data,
+ data_dir=data_dir)
+
+ # 策略映射
+ strategies = {
+ 'SMA': start_sma_backtest_service,
+ 'Bollinger': start_bollinger_backtest_service,
+ 'RSI': start_rsi_backtest_service,
+ 'RESMA': start_reverse_SMA_backtest_service,
+ 'MACD': start_rsi_backtest_service
+ }
+
+ # 通用参数
+ base_params = {
+ 'field_list': field_list,
+ 'stock_list': stock_list,
+ 'period': period,
+ 'start_time': start_time,
+ 'end_time': end_time,
+ 'count': count,
+ 'dividend_type': dividend_type,
+ 'fill_data': fill_data,
+ 'data_dir': data_dir,
+ }
+
+ # 特定策略参数
+ strategy_params = {
+ 'SMA': {'short_window': short_window, 'long_window': long_window},
+ 'Bollinger': {'bollingerMA': bollingerMA, 'std_dev': std_dev},
+ 'RSI': {'short_window': short_window, 'long_window': long_window, 'overbought': overbought,
+ 'oversold': oversold},
+ 'RESMA': {'short_window': short_window, 'long_window': long_window},
+ 'MACD': {'short_window': short_window, 'long_window': signal_window}
+ }
+
+ # 选择策略并执行
+ strategy_func = strategies.get(ma_type)
+ if strategy_func:
+ result = await strategy_func(**base_params, **strategy_params[ma_type])
+ return result
+ else:
+ return None
diff --git a/src/backtest/until.py b/src/backtest/until.py
new file mode 100644
index 0000000..253919c
--- /dev/null
+++ b/src/backtest/until.py
@@ -0,0 +1,99 @@
+import json
+from datetime import datetime
+
+import numpy as np
+from xtquant import xtdata
+import pandas as pd
+
+# 数据的列名
+columns = ['open', 'high', 'low', 'close', 'volume', 'amount', 'settelmentPrice',
+ 'openInterest', 'preClose', 'suspendFlag']
+
+
+# 获取本地数据并进行处理
+async def get_local_data(field_list: list, stock_list: list, period: str, start_time: str, end_time: str,
+ count: int, dividend_type: str, fill_data: bool, data_dir: str):
+ result = xtdata.get_local_data(field_list=field_list, stock_list=stock_list, period=period, start_time=start_time,
+ end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data,
+ data_dir=data_dir)
+ return await data_processing(result)
+
+
+async def data_processing(result_local):
+ # 初始化一个空的列表,用于存储每个股票的数据框
+ df_list = []
+
+ # 遍历字典中的 DataFrame
+ for stock_code, df in result_local.items():
+ # 确保 df 是一个 DataFrame
+ if isinstance(df, pd.DataFrame):
+ # 将时间戳转换为日期时间格式,只保留年-月-日
+ df['time'] = pd.to_datetime(df['time'], unit='ms').dt.date
+ # 将 'time' 列设置为索引,保留为日期格式
+ df.set_index('time', inplace=True)
+ # 将 'close' 列重命名为 'close_股票代码'
+ df.rename(columns={'close': f'close_{stock_code}'}, inplace=True)
+ # 将 DataFrame 添加到列表中
+ df_list.append(df[[f'close_{stock_code}']]) # 只保留 'close_股票代码' 列
+ else:
+ print(f"数据格式错误: {stock_code} 不包含 DataFrame")
+
+ # 使用 pd.concat() 将所有 DataFrame 合并为一个大的 DataFrame
+ combined_df = pd.concat(df_list, axis=1)
+
+ # 确保返回的 DataFrame 索引是日期格式
+ combined_df.index = pd.to_datetime(combined_df.index)
+
+ return combined_df
+
+
+def convert_pandas_to_json_serializable(data: pd.Series) -> str:
+ """
+ 将 Pandas Series 或 DataFrame 中的 Timestamp 索引转换为字符串,并返回 JSON 可序列化的结果。
+
+ 参数:
+ data: pd.Series 或 pd.DataFrame, 带有时间戳索引的 pandas 数据
+
+ 返回:
+ JSON 字符串,键为日期字符串,值为原数据的值。
+ """
+ # 判断数据类型
+ if isinstance(data, (pd.Series, pd.DataFrame)):
+ # 如果索引是时间戳类型,则转换为 YYYYMMDD 格式
+ if isinstance(data.index, pd.DatetimeIndex):
+ data.index = data.index.strftime('%Y%m%d')
+
+ # 处理 NaN 和 None 的情况,替换为 0 或其他合适的默认值
+ data = data.replace([np.nan, None], 0)
+
+ # 将索引重置为普通列,然后转换为字典
+ json_serializable_data = data.rename_axis('date').reset_index().to_dict(orient='records')
+
+ # 将字典转换为 JSON 格式字符串
+ json_string = json.dumps(json_serializable_data)
+ return json_string
+ else:
+ raise ValueError("输入必须为 Pandas Series 或 DataFrame")
+
+
+async def data_check(field_list: list,
+ stock_list: list,
+ period: str = '1d',
+ start_time: str = '',
+ end_time: str = '',
+ count: int = -1,
+ dividend_type: str = 'none',
+ fill_data: bool = True,
+ data_dir: str = '', ):
+ result_data = xtdata.get_local_data(field_list=[], stock_list=stock_list, period=period, start_time=start_time,
+ end_time=end_time, count=count, dividend_type=dividend_type, fill_data=fill_data,
+ data_dir=data_dir)
+ time_now = int(datetime.now().strftime('%Y%m%d'))
+ for i in stock_list:
+ close = int(result_data.get(i).index[-1])
+ if close != 0 and close < time_now:
+ xtdata.download_history_data(stock_code=i,
+ period='1d',
+ start_time='',
+ end_time='',
+ incrementally=True)
diff --git a/src/constants.py b/src/constants.py
new file mode 100644
index 0000000..e1eab81
--- /dev/null
+++ b/src/constants.py
@@ -0,0 +1,43 @@
+from enum import Enum
+
+THISPOLICYNAME = ''
+def setThisPolicyName(name):
+ THISPOLICYNAME = name
+
+def getThisPolicyName():
+ return THISPOLICYNAME
+
+DB_NAMING_CONVENTION = {
+ "ix": "%(column_0_label)s_idx",
+ "uq": "%(table_name)s_%(column_0_name)s_key",
+ "ck": "%(table_name)s_%(constraint_name)s_check",
+ "fk": "%(table_name)s_%(column_0_name)s_fkey",
+ "pk": "%(table_name)s_pkey",
+}
+
+
+class RedisKeyConstants:
+ SMS_CODE_KEY = "sms:code:"
+ TOKEN_KEY = "Youcailogin:"
+ ACCESS_TOKEN_KEY = "applet_access_token_youcai"
+ SESSION_KEY = "session_id_youcai:"
+ OFFICIAL_ACCESS_TOKEN_KEY = "official_access_token_youcai"
+
+
+class Environment(str, Enum):
+ LOCAL = "LOCAL"
+ STAGING = "STAGING"
+ TESTING = "TESTING"
+ PRODUCTION = "PRODUCTION"
+
+ @property
+ def is_debug(self):
+ return self in (self.LOCAL, self.STAGING, self.TESTING)
+
+ @property
+ def is_testing(self):
+ return self == self.TESTING
+
+ @property
+ def is_deployed(self) -> bool:
+ return self in (self.STAGING, self.PRODUCTION)
diff --git a/readme.md b/src/data_processing/__init__.py
similarity index 100%
rename from readme.md
rename to src/data_processing/__init__.py
diff --git a/src/data_processing/history_data_processing.py b/src/data_processing/history_data_processing.py
new file mode 100644
index 0000000..63c1531
--- /dev/null
+++ b/src/data_processing/history_data_processing.py
@@ -0,0 +1,412 @@
+import asyncio
+from datetime import datetime
+
+import akshare as ak
+import pandas as pd
+from xtquant import xtdata
+
+from src.models import wance_data_stock
+from src.tortoises_orm_config import init_tortoise
+from src.utils.history_data_processing_utils import translation_dict, get_best_match, safe_get_value, on_progress
+from src.utils.split_stock_utils import split_stock_code, join_stock_code, percent_to_float
+from src.xtdata.service import download_history_data_service, get_full_tick_keys_service, download_history_data2_service
+
+# period - 周期,用于表示要获取的周期和具体数据类型
+period_list = ["1d", "1h", "30m", "15m", "5m", "1m", "tick", "1w", "1mon", "1q", "1hy", "1y"]
+# 数据的列名
+columns = ['open', 'high', 'low', 'close', 'volume', 'amount', 'settelmentPrice',
+ 'openInterest', 'preClose', 'suspendFlag']
+
+
+def processing_data(field_list: list, stock_list: list, period: str, start_time: str, end_time: str, count: int,
+ dividend_type: str, fill_data: bool):
+ """
+
+ :param field_list: []
+ :param stock_list: ["186511.SH","173312.SH","231720.SH","173709.SH","019523.SH"]
+ :param period: "1d"
+ :param start_time: "20240506"
+ :param end_time: ""
+ :param count: -1
+ :param dividend_type: "none"
+ :param fill_data: False
+ :return:
+ """
+ try:
+ # 获取本地数据
+ result_local = xtdata.get_local_data(field_list=field_list,
+ stock_list=stock_list,
+ period=period,
+ start_time=start_time,
+ end_time=end_time,
+ count=count,
+ dividend_type=dividend_type,
+ fill_data=fill_data,
+ data_dir=""
+ )
+
+ # 初始化一个空的列表,用于存储每个股票的数据框
+ df_list = []
+ # 遍历字典中的 DataFrame
+ for stock_code, df in result_local.items():
+ # 确保 df 是一个 DataFrame
+ if isinstance(df, pd.DataFrame):
+ # 将时间戳转换为日期时间格式,并格式化为字符串 'YYYYMMDD'
+ df['time'] = pd.to_datetime(df['time'], unit='ms').dt.strftime('%Y%m%d')
+ # 将 'time' 列设置为索引
+ df.set_index('time', inplace=True)
+ # 指定列名
+ df.columns = columns
+ # 添加一列 'stock_code' 用于标识不同的股票
+ df['stock_code'] = stock_code
+ # 将 DataFrame 添加到列表中
+ df_list.append(df)
+ else:
+ print(f"数据格式错误: {stock_code} 不包含 DataFrame")
+
+ # 使用 pd.concat() 将所有 DataFrame 合并为一个大的 DataFrame
+ combined_df = pd.concat(df_list)
+
+ # 打印合并后的 DataFrame
+ print(combined_df)
+
+ print(f"开始获取股票数据{result_local}")
+ except Exception as e:
+ print(f"处理数据发生错误: {str(e)}")
+
+
+def history_data_processing():
+ """
+ 本地路径 D:\\e海方舟-量化交易版\\userdata_mini\\datadir
+ :return:
+ """
+
+ try:
+ result = xtdata.get_full_tick(code_list=['SH', 'SZ'])
+ result = list(result.keys())
+ # for key in result:
+ # xtdata.download_history_data(stock_code=key, period="1d", start_time="", end_time="", incrementally="")
+ datas = xtdata.get_local_data(field_list=[], stock_list=result, period='1d', start_time='', end_time='',
+ count=-1,
+ dividend_type='none', fill_data=True)
+ # datas = xtdata.download_history_data2(stock_list=result, period="1d", start_time="", end_time="",
+ # callback=on_progress)
+ print(datas, "这里是返回的数据")
+ except Exception as e:
+ print(f"处理数据发生错误: {str(e)}")
+
+
+async def init_indicator():
+ await init_tortoise()
+ # 从数据库中获取股票列表
+ stock_list = await wance_data_stock.WanceDataStock.filter(stock_type__contains=["stock"]).all()
+ stock_zh_a_spot_em_df = ak.stock_zh_a_spot_em()
+
+ # 遍历股票列表拿到股票实体
+ for stock in stock_list:
+ try:
+ # 使用 akshare 获取股票指标数据
+ stock_code = stock.stock_code # 假设 stock_code 是股票代码的字段
+ stock_code_suffix = split_stock_code(stock_code) # 提取股票代码部分
+ stock_code_xq = join_stock_code(stock_code_suffix)
+ stock_code_front = stock_code_suffix[0]
+ # 筛选匹配的行
+ match = stock_zh_a_spot_em_df.loc[stock_zh_a_spot_em_df['代码'] == stock_code_front, '涨跌幅']
+
+ # 从akshare中获取数据
+ stock_a_indicator_lg_df = ak.stock_a_indicator_lg(symbol=stock_code_front) # 乐咕乐股-A 股个股指标: 市盈率, 市净率, 股息率
+ stock_financial_abstract_ths_df = ak.stock_financial_abstract_ths(symbol=stock_code_front,
+ indicator="按报告期") # 同花顺-财务指标-主要指标
+ stock_financial_abstract_df = ak.stock_financial_abstract(symbol=stock_code_front) # 新浪财经-财务报表-关键指标
+ stock_individual_spot_xq_df = ak.stock_individual_spot_xq(symbol=stock_code_xq) # 雪球-行情中心-个股
+ stock_zh_valuation_baidu_df = ak.stock_zh_valuation_baidu(symbol=stock_code_front, indicator="市现率",
+ period="近一年") # 百度股市通-A 股-财务报表-估值数据
+ stock_fhps_detail_em_df = ak.stock_fhps_detail_em(symbol=stock_code_front) # 东方财富网-数据中心-分红送配-分红送配详情
+
+ # 查询数据库中是否已有该股票的数据
+ existing_record = await wance_data_stock.WanceDataStock.filter(stock_code=stock_code).first()
+
+ if existing_record is None:
+ print(f"未找到股票记录: {stock_code}")
+ continue
+
+ # 处理并插入数据到数据库
+ last_indicator_row = stock_a_indicator_lg_df.iloc[-1]
+ last_abstract_row = stock_financial_abstract_ths_df.iloc[0]
+ last_financial_row = stock_financial_abstract_df.iloc[:, 2]
+ last_spot_xq_row = stock_individual_spot_xq_df.iloc[:, 1]
+ last_baidu_df_row = stock_zh_valuation_baidu_df.iloc[-1]
+ last_detail_em_row = stock_fhps_detail_em_df.iloc[-1]
+
+ # 更新字段的对应数据
+ # 每股指标模块
+ existing_record.financial_dividend = safe_get_value(last_detail_em_row.get('现金分红-现金分红比例'))
+ existing_record.financial_ex_gratia = safe_get_value(last_spot_xq_row[8])
+ existing_record.financial_cash_flow = safe_get_value(last_financial_row[10])
+ existing_record.financial_asset_value = safe_get_value(last_financial_row[9])
+ existing_record.financial_reserve_per = safe_get_value(last_abstract_row.get('每股资本公积金'))
+ existing_record.financial_undistributed_profit = safe_get_value(last_abstract_row.get('每股未分配利润'))
+
+ # 盈利能力模块
+ existing_record.profit_asset_value = safe_get_value(last_financial_row.iloc[33])
+ existing_record.profit_sale_ratio = safe_get_value(last_financial_row.iloc[43])
+ existing_record.profit_gross_rate = safe_get_value(last_financial_row.iloc[44])
+ existing_record.profit_business_increase = safe_get_value(last_financial_row.iloc[53])
+ existing_record.profit_dividend_rate = safe_get_value(last_spot_xq_row.iloc[26])
+
+ # 成长能力
+ existing_record.growth_Income_rate = percent_to_float(
+ safe_get_value(last_abstract_row.get('营业总收入同比增长率', 0.0)))
+ existing_record.growth_growth_rate = safe_get_value(last_financial_row.iloc[46])
+ existing_record.growth_nonnet_profit = percent_to_float(
+ safe_get_value(last_abstract_row.get('扣非净利润同比增长率', 0.0)))
+ existing_record.growth_attributable_rate = safe_get_value(last_financial_row.iloc[54])
+
+ # 估值指标
+ existing_record.valuation_PEGTTM_ratio = safe_get_value(last_indicator_row.get('pe_ttm'))
+ existing_record.valuation_PEG_percentile = safe_get_value(last_indicator_row.get('pe'))
+ existing_record.valuation_PB_TTM = safe_get_value(last_indicator_row.get('ps'))
+ existing_record.valuation_PB_percentile = safe_get_value(last_indicator_row.get('pb'))
+ existing_record.valuation_PTS_TTM = safe_get_value(last_indicator_row.get('dv_ratio'))
+ existing_record.valuation_PTS_percentile = safe_get_value(last_indicator_row.get('ps_ttm'))
+ existing_record.valuation_market_TTM = safe_get_value(last_baidu_df_row[-1])
+ existing_record.valuation_market_percentile = safe_get_value(1 / last_baidu_df_row[-1] * 100) if \
+ last_baidu_df_row[-1] != 0 else 0
+
+ # 行情指标
+ existing_record.market_indicator = safe_get_value(match.values[0]) if not match.empty else 0
+
+ # 保存更改
+ await existing_record.save()
+
+ print(f"更新股票指标成功!: {stock_code}")
+
+ except Exception as e:
+ print(f"处理股票 {stock_code} 时发生错误: {e}")
+ continue # 继续处理下一个股票
+
+
+async def init_stock_pool(incremental: bool = False):
+ """
+ 初始化股票池参数,包括股票名、股票代码、股票上市时间、股票板块等信息。
+ @param incremental: 是否执行增量下载
+ @type incremental: bool
+ """
+ await init_tortoise()
+
+ # 获取所有现有股票代码
+ existing_stocks = set()
+ if incremental:
+ existing_stocks = {stock.stock_code for stock in await wance_data_stock.WanceDataStock.all()}
+
+ # 初始化股票池
+ tick_result = xtdata.get_full_tick(['SH', 'SZ'])
+ stocks_to_create = [] # 使用一个列表批量创建
+ for key in tick_result.keys():
+ if incremental and key in existing_stocks:
+ print(f"股票代码 {key} 已经存在,跳过...\n")
+ continue
+
+ detail_result = xtdata.get_instrument_detail(key, False)
+ InstrumentName_result = detail_result.get("InstrumentName")
+ start_time = detail_result.get("OpenDate") or detail_result.get("CreateDate")
+ end_time = datetime.now().strftime('%Y%m%d')
+ time_expire = detail_result.get("ExpireDate")
+ type_dict = xtdata.get_instrument_type(key)
+ type_list = []
+ if type_dict:
+ for i in type_dict.keys():
+ type_list.append(i)
+
+ # 只在 type_list 包含 "stock" 时继续执行
+ if "stock" in type_list:
+ # 检查股票是否已存在
+ existing_stock = await wance_data_stock.WanceDataStock.filter(stock_code=key).first()
+ if not existing_stock:
+ stocks_to_create.append(
+ wance_data_stock.WanceDataStock(
+ stock_code=key,
+ stock_name=InstrumentName_result,
+ stock_sector=[], # 初始化为空列表,后续会加入板块
+ stock_type=type_list,
+ time_start=start_time,
+ time_end=end_time,
+ time_expire=time_expire,
+ market_sector=key.rsplit('.', 1)[-1]
+ )
+ )
+ print(f"加载成功 股票名称 {InstrumentName_result} 股票代码 {key} 股票类型 {type_list} \n")
+ else:
+ print(f"股票代码 {key} 已经存在,跳过...\n")
+ else:
+ print(f"跳过非股票类型:{key} 类型:{type_list} \n")
+
+ # 如果有新的股票,批量创建所有新股票记录
+ if stocks_to_create:
+ bulk_db_result = await wance_data_stock.WanceDataStock.bulk_create(stocks_to_create)
+ print(bulk_db_result, "股票池创建完成 \n")
+ else:
+ print("没有新的股票需要创建 \n")
+
+ # 获取并更新sector模块
+ sector_list = xtdata.get_sector_list()
+
+ for sector in sector_list:
+ # 使用模糊匹配找到最佳的中文板块匹配
+ best_match = get_best_match(sector, translation_dict)
+ if best_match:
+ translated_sector = translation_dict[best_match] # 获取对应的英文名称
+ else:
+ print(f"没有找到合适的板块匹配:{sector} \n")
+ continue # 如果没有找到匹配,跳过该板块
+
+ # 获取板块对应的股票列表
+ sector_stock = xtdata.get_stock_list_in_sector(sector)
+ # 获取所有相关股票
+ stocks_to_update = await wance_data_stock.WanceDataStock.filter(stock_code__in=sector_stock)
+
+ # 遍历并更新每个股票的sector,避免重复添加相同的英文板块
+ for stock in stocks_to_update:
+ if translated_sector not in stock.stock_sector: # 检查是否已经存在该英文板块
+ stock.stock_sector.append(translated_sector)
+ await stock.save() # 保存更新后的数据
+ else:
+ print(f"{stock.stock_code} 已经包含板块 {translated_sector}, 跳过重复添加 \n")
+
+ print(f"更新板块完成 {sector}: {translated_sector} \n")
+
+ print(f"所有股票已经加载完成 \n")
+
+
+ """
+ # 初始化股票池参数 股票名、股票代码、股票上市时间、股票板块、股票最后回测时间、股票退市时间、股票所属市场
+ @param incremental:是否执行增量下载
+ @type incremental:bool
+ @return:
+ @rtype:
+ """
+
+
+"""
+async def init_stock_pool(incremental: bool = False):
+ # 这行代码存在一个问题就是,存入的板块信息,是中文的,在查询的时候应为是中文的数据所以没有办法被选中
+
+ await init_tortoise()
+
+ # 获取所有现有股票代码
+ existing_stocks = set()
+ if incremental:
+ existing_stocks = {stock.stock_code for stock in await wance_data_stock.WanceDataStock.all()}
+
+ # 初始化股票池
+ tick_result = xtdata.get_full_tick(['SH', 'SZ'])
+ stocks_to_create = [] # 使用一个列表批量创建
+ for key in tick_result.keys():
+ # 如果是增量更新,且该股票已经存在,则跳过
+ if incremental and key in existing_stocks:
+ continue
+
+ detail_result = xtdata.get_instrument_detail(key, False)
+ InstrumentName_result = detail_result.get("InstrumentName")
+ start_time = detail_result.get("OpenDate") or detail_result.get("CreateDate")
+ end_time = datetime.now().strftime('%Y%m%d')
+ time_expire = detail_result.get("ExporeDate")
+ type_dict = xtdata.get_instrument_type(key)
+ type_list = []
+ if type_dict:
+ for i in type_dict.keys():
+ type_list.append(i)
+
+ stocks_to_create.append(
+ wance_data_stock.WanceDataStock(
+ stock_code=key,
+ stock_name=InstrumentName_result,
+ stock_sector=[],
+ stock_type=type_list,
+ time_start=start_time,
+ time_end=end_time,
+ time_expire=time_expire,
+ market_sector=key.rsplit('.', 1)[-1]
+ )
+ )
+ print(f"加载成功 股票名称 {InstrumentName_result} 股票代码 {key} 股票类型 {type_list} \n")
+
+ # 如果有新的股票,批量创建所有新股票记录
+ if stocks_to_create:
+ bulk_db_result = await wance_data_stock.WanceDataStock.bulk_create(stocks_to_create)
+ print(bulk_db_result, "股票池创建完成 \n")
+ else:
+ print("没有新的股票需要创建 \n")
+
+ # 获取并更新sector模块
+ sector_list = xtdata.get_sector_list()
+ if incremental:
+ # 获取已经更新过的sector
+ updated_sectors = set()
+ # 获取所有已经存在的股票及其相关板块
+ existing_stock_sectors = await wance_data_stock.WanceDataStock.all().values('stock_code', 'stock_sector')
+ for stock_info in existing_stock_sectors:
+ for sector in stock_info['stock_sector']:
+ updated_sectors.add(sector)
+
+ # 过滤出没有更新过的sector
+ sector_list = [sector for sector in sector_list if sector not in updated_sectors]
+
+ for sector in sector_list:
+ sector_stock = xtdata.get_stock_list_in_sector(sector)
+ # 获取所有相关股票
+ stocks_to_update = await wance_data_stock.WanceDataStock.filter(stock_code__in=sector_stock)
+
+ # 遍历并更新每个股票的sector,避免重复添加
+ for stock in stocks_to_update:
+ if sector not in stock.stock_sector:
+ stock.stock_sector.append(sector)
+ await stock.save() # 保存更新后的数据
+ print(f"更新板块完成 {stock.stock_code}: {stock.stock_sector} \n")
+
+ print(f"所有股票已经加载完成 \n")
+"""
+
+"""
+# 每次都会操作数据库
+async def init_stock_pool():
+ await init_tortoise()
+
+ # 初始化股票池
+ tick_result = xtdata.get_full_tick(['SH', 'SZ'])
+ tick_keys = list(tick_result.keys())
+ for key in tick_keys:
+ detail_result = xtdata.get_instrument_detail(key, False)
+ InstrumentName_result = detail_result.get("InstrumentName")
+ type_result = xtdata.get_instrument_type(key)
+ # 创建新的股票记录
+ await wance_data_stock.WanceDataStock.create(
+ stock_code=key,
+ stock_name=InstrumentName_result,
+ stock_sector="",
+ stock_type=type_result
+ )
+
+ # 获取并更新sector模块
+ sector_list = xtdata.get_sector_list()
+ for sector in sector_list:
+ sector_stock = xtdata.get_stock_list_in_sector(sector)
+ for stock in sector_stock:
+ # 更新已有股票的sector
+ await wance_data_stock.WanceDataStock.filter(stock_code=stock).update(stock_sector=sector)
+"""
+
+if __name__ == '__main__':
+ # processing_data(field_list=[],
+ # stock_list=["000062.SZ","600611.SH"],
+ # period="1d",
+ # start_time="20240506",
+ # end_time="",
+ # count=-1,
+ # dividend_type="none",
+ # fill_data=False,
+ # )
+ # history_data_processing()
+ asyncio.run(init_stock_pool(False))
+ asyncio.run(init_indicator())
+# xtdata.run()
diff --git a/src/data_processing/response_factor.py b/src/data_processing/response_factor.py
new file mode 100644
index 0000000..6f6f000
--- /dev/null
+++ b/src/data_processing/response_factor.py
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/exceptions.py b/src/exceptions.py
new file mode 100644
index 0000000..0e45d3f
--- /dev/null
+++ b/src/exceptions.py
@@ -0,0 +1,92 @@
+from typing import Any
+
+from fastapi import FastAPI, HTTPException, Request, status
+from fastapi.encoders import jsonable_encoder
+from fastapi.exceptions import RequestValidationError
+from fastapi.responses import JSONResponse
+from starlette.exceptions import HTTPException as StarletteHTTPException
+
+from src.utils.helpers import first
+
+
+def register_exception_handler(app: FastAPI):
+ @app.exception_handler(RequestValidationError)
+ async def validation_exception_handler(request: Request, exc: RequestValidationError):
+ """请求参数验证错误处理"""
+ error = first(exc.errors())
+ field = error.get('loc')[1] or ''
+ return JSONResponse(
+ status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
+ content=jsonable_encoder({
+ "status_code": status.HTTP_422_UNPROCESSABLE_ENTITY,
+ "message": "{} param {}".format(field, error.get('msg')),
+ "detail": exc.errors()}),
+ )
+
+ @app.exception_handler(StarletteHTTPException)
+ async def http_exception_handler(request: Request, exc: StarletteHTTPException):
+ """Http 异常处理"""
+ return JSONResponse(
+ status_code=exc.status_code,
+ content=jsonable_encoder({
+ "status_code": exc.status_code,
+ "message": exc.detail}),
+ )
+
+ @app.exception_handler(Exception)
+ async def exception_callback(request: Request, exc: Exception):
+
+ """其他异常处理,遇到其他问题再自定义异常"""
+ return JSONResponse(
+ status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
+ content=jsonable_encoder({
+ "status_code": status.HTTP_500_INTERNAL_SERVER_ERROR,
+ "message": "Internal Server Error",
+ # "detail": ''.join(exc.args)
+ }),
+ )
+
+
+class CommonHttpException(HTTPException):
+ def __init__(self, detail, status_code, **kwargs: dict[str, Any]) -> None:
+ super().__init__(status_code=status_code, detail=detail, **kwargs)
+
+
+class DetailedHTTPException(HTTPException):
+ STATUS_CODE = status.HTTP_500_INTERNAL_SERVER_ERROR
+ DETAIL = "Server error"
+
+ def __init__(self, **kwargs: dict[str, Any]) -> None:
+ super().__init__(status_code=self.STATUS_CODE, detail=self.DETAIL, **kwargs)
+
+
+class PermissionDenied(DetailedHTTPException):
+ STATUS_CODE = status.HTTP_403_FORBIDDEN
+ DETAIL = "Permission denied"
+
+
+class NotFound(DetailedHTTPException):
+ STATUS_CODE = status.HTTP_404_NOT_FOUND
+
+
+class BadRequest(DetailedHTTPException):
+ STATUS_CODE = status.HTTP_400_BAD_REQUEST
+ DETAIL = "Bad Request"
+
+
+class UnprocessableEntity(DetailedHTTPException):
+ STATUS_CODE = status.HTTP_422_UNPROCESSABLE_ENTITY
+ DETAIL = "Unprocessable entity"
+
+
+class NotAuthenticated(DetailedHTTPException):
+ STATUS_CODE = status.HTTP_401_UNAUTHORIZED
+ DETAIL = "User not authenticated"
+
+
+class WxResponseError(DetailedHTTPException):
+ STATUS_CODE = status.HTTP_400_BAD_REQUEST
+ DETAIL = "请求微信异常"
+
+ def __init__(self) -> None:
+ super().__init__(headers={"WWW-Authenticate": "Bearer"})
diff --git a/src/main.py b/src/main.py
new file mode 100644
index 0000000..9f9cac5
--- /dev/null
+++ b/src/main.py
@@ -0,0 +1,52 @@
+import sentry_sdk
+import uvicorn
+from fastapi import FastAPI
+from starlette.middleware.cors import CORSMiddleware
+
+from src.exceptions import register_exception_handler
+from src.tortoises import register_tortoise_orm
+from src.xtdata.router import router as xtdata_router
+from src.backtest.router import router as backtest_router
+
+from xtquant import xtdata
+from src.settings.config import app_configs, settings
+
+
+
+
+app = FastAPI(**app_configs)
+
+register_tortoise_orm(app)
+
+register_exception_handler(app)
+
+app.include_router(xtdata_router, prefix="/getwancedata", tags=["盘口数据"])
+app.include_router(backtest_router, prefix="/backtest", tags=["盘口数据"])
+
+
+if settings.ENVIRONMENT.is_deployed:
+ sentry_sdk.init(
+ dsn=settings.SENTRY_DSN,
+ environment=settings.ENVIRONMENT,
+ )
+
+app.add_middleware(
+ CORSMiddleware,
+ allow_origins=settings.CORS_ORIGINS,
+ allow_origin_regex=settings.CORS_ORIGINS_REGEX,
+ allow_credentials=True,
+ allow_methods=("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"),
+ allow_headers=settings.CORS_HEADERS,
+)
+
+
+
+
+@app.get("/")
+async def root():
+ return {"message": "Hello, FastAPI!"}
+
+
+
+if __name__ == "__main__":
+ uvicorn.run('src.main:app', host="0.0.0.0", port=8011, reload=True)
diff --git a/src/models/__init__.py b/src/models/__init__.py
new file mode 100644
index 0000000..508eccf
--- /dev/null
+++ b/src/models/__init__.py
@@ -0,0 +1,292 @@
+from datetime import datetime
+from decimal import Decimal
+from enum import Enum, IntEnum
+from typing import Optional
+
+from pydantic import BaseModel
+
+from src.settings.config import settings
+
+
+def with_table_name(table_name: str):
+ return f"{settings.DATABASE_PREFIX}{table_name}"
+
+
+class Decimals:
+ MAX_DIGITS = 15
+ DECIMAL_PLACES = 3
+ DECIMAL_PLACES_MORE = 6
+
+
+class SecurityPriceCode(Enum):
+ TRANSFER_FEE = "fee_1" # 过户费
+
+ STAMP_TAX = "fee_2" # 印花税
+
+ TOTAL_COMMISSION = "csf_fee" # 总证券佣金
+
+
+class UserSecurityExtraReminded(BaseModel):
+ """
+ reminded_target_buy_price: 最近提醒目标买入价
+ reminded_target_buy_time: 目标买入价最近提醒时间
+ reminded_buy_price: 最近提醒买入价
+ reminded_buy_fluctuation: 最近提醒买入波动
+ reminded_buy_time: 买入最近提醒时间
+ reminded_sell_price: 最近提醒目标卖出价
+ reminded_sell_fluctuation: 最近提醒卖出波动
+ reminded_sell_time: 卖出最近提醒时间
+ buy_up_first_interval: 买入上涨初次提醒波动间隔
+ buy_up_continue_interval: 买入继续上涨提醒波动间隔
+ buy_down_first_interval: 买入下跌初次提醒波动间隔
+ buy_down_continue_interval: 买入继续下跌提醒波动间隔
+ sell_down_first_interval: 卖出下跌初次提醒波动间隔
+ sell_down_continue_interval: 卖出继续下跌提醒波动间隔
+ browse_msg_alarm_flag: 浏览器消息提醒通知设置
+ wx_msg_alarm_flag: 微信公众号消息提醒通知设置
+ """
+ reminded_target_buy_price: int = 0
+ reminded_target_buy_time: Optional[datetime] = None
+ reminded_buy_price: Decimal = 0.000
+ reminded_buy_fluctuation: int = 0
+ reminded_buy_time: Optional[datetime] = None
+ reminded_sell_price: Decimal = 0.000
+ reminded_sell_fluctuation: int = 0
+ reminded_sell_time: Optional[datetime] = None
+ buy_up_first_interval: int = 1
+ buy_up_continue_interval: int = 1
+ buy_down_first_interval: int = -1
+ buy_down_continue_interval: int = -1
+ sell_down_first_interval: int = -1
+ sell_down_continue_interval: int = -1
+ browse_msg_alarm_flag: bool = True
+ wx_msg_alarm_flag: bool = True
+
+ class Config:
+ json_encoders = {Decimal: str}
+
+
+class Status(IntEnum):
+ """
+ 状态
+ 0-正常
+ 1-删除
+ 2-禁用
+ 3-退市
+ """
+ # 正常
+ NORMAL = 0
+ STOP = 1
+ DISABLED = 2
+ DELISTED = 3
+
+
+class StockType(str, Enum):
+ """
+ 股票类型
+ """
+ SH = "sh"
+ SZ = "sz"
+
+
+class UserSecurityAccountType(IntEnum):
+ """
+ 用户中国证券账户账户类型
+ """
+
+ NORMAL = 0 # 普通账户
+
+ MARGIN = 1 # 融资账户
+
+
+class ExchangeType(str, Enum):
+ """
+ 交易类型
+ """
+ SH = "SH"
+ SZ = "SZ"
+ NQ = "NQ"
+ BJ = "BJ"
+
+
+class SellReferType(IntEnum):
+ """
+ 参考卖出类型
+ highest-最高卖出价
+ latest-最近卖出价
+ """
+ HIGHEST = 0 # 最高卖出价
+ LATEST = 1 # 最近卖出价
+
+
+class InvestmentSource(IntEnum):
+ """
+ 投资资金来源
+ """
+ CASH = 0 # 现金
+ FINANCING = 1 # 场内融资
+ LOANED = 2 # 场外借贷
+
+
+class SecuritySource(IntEnum):
+ """
+ 证券获得方式
+ """
+ BUY = 0 # 正常买入
+ BONUS_SHARES = 1 # 送股
+ CONVERT_SHARES = 2 # 转增股
+
+
+class StatusSell(IntEnum):
+ """
+ 售出状态
+ """
+
+ UNSOLD = 0 # 未售出
+ PARTIAL_SOLD = 1 # 部分售出
+ ALL_SOLD = 2 # 全部售出
+
+
+class BuyAndSellType(IntEnum):
+ """
+ 卖出卖出记录类型
+ """
+ COMMISSION = 0 # 委托单
+ CONDITION = 1 # 条件单
+ CONTRACT = 2 # 成交单
+
+
+class SecurityType(IntEnum):
+ """
+ 证券类型
+ """
+
+ security_stock_a = 0 # A 股股票
+
+ security_stock_b = 1 # B 股股票
+
+ security_etf = 2 # 场内基金-ETF
+
+ security_lof = 3 # 场内基金-LOF
+
+
+class PushConfigModel(BaseModel):
+ """
+ 驼峰转下线
+ """
+ device_id: int = None
+ device_name: str = None
+ eable_flag: str = None
+ update_at: Optional[datetime] = None
+
+
+class CashFlowType(IntEnum):
+ INTO = 0 # 转入
+ OUT = 1 # 转出
+
+
+class AdminStatus(str, Enum):
+ """
+ 管理员账户状态
+ """
+
+ DELETE = "delete" # 删除
+ NORMAL = "normal" # 正常
+ FORBIDDEN = "forbidden" # 禁用
+ UNAUTHORIZED = "unauthorized" # 未验证
+
+
+class CustomerSecurityAccountSecurityAdminStatus(str, Enum):
+ """
+ 客户的证券账户中的券商状态
+ """
+ JOIN_GROUP = "join_group" # 新入群
+ ACCOUNT_READY_NOT_DEPOSIT = "account_ready_not_deposit" # 已开户但未入金
+ ACCOUNT_SUBSTANDARD = "account_substandard" # 非有效户(20个交易日里平均市值1万)
+ ACCOUNT_UNSETTLED = "account_unsettled" # 待结算
+ ACCOUNT_SETTLED = "account_settled" # 已结算
+
+
+class CustomerSecurityAccountVipStatus(str, Enum):
+ """
+ 客户的证券账户中的vip状态
+ """
+ REWARDED = "rewarded" # 已奖励
+ UNREWARD = "unreward" # 未奖励
+
+
+class UserBenefitStatus(IntEnum):
+ """
+ 用户福利领取状态
+ """
+ UNRECEIVED = 0 # 未领取
+ RECEIVED = 1 # 已领取
+
+
+class SecurityAdminTransferStatus(IntEnum):
+ """
+ 券商转账的状态
+ """
+ PAYED = 1 # 已支付
+ CONFIRMED = 2 # 已确认
+ DISAGREED = 3 # 有异议
+
+
+class YuanMaTalkQuestionPublicStatus(IntEnum):
+ """
+ 问题是否公开
+ """
+ PUBLIC = 1 # 公开
+ PRIVATE = 0 # 私密
+
+
+class YuanMaTalkQuestionStatus(str, Enum):
+ """
+ 问题状态
+ """
+ NEW = "new" # 新问题
+ REPLIED = "replied" # 已回复
+ WAITING = "waiting" # 待回复
+ CLOSED = "closed" # 关闭
+ DELETED = "deleted" # 删除
+ FORBIDDEN = "forbidden" # 禁止
+
+
+class LastReplySourceType(str, Enum):
+ """
+ 问题状态
+ """
+ ADMIN = "admin" # 管理后台
+ USER = "user" # 用户端
+
+
+class AccountType(IntEnum):
+ """
+ 账户类型
+ """
+ LIANGRONG = 1 # 两融账户
+ ORDINARY = 0 # 普通用户
+
+
+class supervisionFee(IntEnum):
+ """
+ 深市是否申请监管费
+ """
+ NOT_CHARGE = 1 # 不收
+ CHARGE = 0 # 收
+
+
+class transferFee(IntEnum):
+ """
+ 账户类型
+ """
+ NOT_CHARGE = 1 # 不收
+ CHARGE = 0 # 收
+
+
+class IsType(IntEnum):
+ """
+ 是否默认
+ """
+ YES = 1 # 是
+ NO = 0 # 否
diff --git a/src/models/__pycache__/__init__.cpython-311.pyc b/src/models/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c0f0082b4c28670ceba057c2b05393c2267f34c6
GIT binary patch
literal 12289
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