diff --git a/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl b/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl new file mode 100644 index 0000000000..ec0e7061b4 Binary files /dev/null and b/TA_Lib-0.4.32-cp310-cp310-win_amd64.whl differ diff --git a/instock/config/trade_client.json b/instock/config/trade_client.json index ad788c93b3..37d0336fe7 100644 --- a/instock/config/trade_client.json +++ b/instock/config/trade_client.json @@ -1,5 +1,5 @@ { - "user": "888888888888", + "user": "029000797211", "password": "888888", - "exe_path": "C:/gfzqrzrq/xiadan.exe" + "exe_path": "F:\\东吴证券金融终端独立下单/xiadan.exe" } \ No newline at end of file diff --git a/instock/core/backtest/Chan.py b/instock/core/backtest/Chan.py new file mode 100644 index 0000000000..2e9393842d --- /dev/null +++ b/instock/core/backtest/Chan.py @@ -0,0 +1,274 @@ +import os +import random +import sys +from datetime import datetime, timedelta + +import backtrader as bt +import pandas as pd +import matplotlib + +from instock.core.backtest.base_strategy import BaseStrategy +from instock.core.singleton_stock import stock_hist_data, stock_data +from instock.lib.singleton_type import singleton_type +import instock.core.tablestructure as tbs + +cpath_current = os.path.dirname(os.path.dirname(__file__)) +cpath = os.path.abspath(os.path.join(cpath_current, os.pardir)) +sys.path.append(cpath) + +class ChanKline: + def __init__(self, open, high, low, close, date, volume): + self.open = open + self.high = high + self.low = low + self.close = close + self.date = date + self.volume = volume + +class Segment: + def __init__(self, start, end, direction): + self.start = start + self.end = end + self.direction = direction # 1 for up, -1 for down + +class Pivot: + def __init__(self, kline, type): + self.kline = kline + self.type = type # 'high' or 'low' + +class CentralZone: + def __init__(self, start, end): + self.start = start + self.end = end + self.high = max(start.high, end.high) + self.low = min(start.low, end.low) + +class ChanIndicator(bt.Indicator): + lines = ('buy_signal', 'sell_signal') + params = ( + ('period', 20), + ('atr_period', 14), + ('atr_multiplier', 2), + ) + + def __init__(self): + self.addminperiod(self.params.period) + self.merged_klines = [] + self.segments = [] + self.pivots = [] + self.central_zones = [] + + # 添加技术指标 + self.atr = bt.indicators.ATR(self.data, period=self.params.atr_period) + self.rsi = bt.indicators.RSI(self.data, period=14) + self.macd = bt.indicators.MACD(self.data) + self.volume_ma = bt.indicators.SMA(self.data.volume, period=20) + + def next(self): + current_kline = ChanKline(self.data.open[0], self.data.high[0], self.data.low[0], + self.data.close[0], self.data.datetime.date(0), self.data.volume[0]) + self.merge_klines(current_kline) + self.identify_pivots() + self.identify_segments() + self.identify_central_zones() + + self.lines.buy_signal[0] = self.is_buy_point() + self.lines.sell_signal[0] = self.is_sell_point() + + def merge_klines(self, new_kline): + # 保持原有的K线合并逻辑 + if not self.merged_klines: + self.merged_klines.append(new_kline) + return + + last_kline = self.merged_klines[-1] + if (new_kline.high > last_kline.high and new_kline.low < last_kline.low) or \ + (new_kline.high < last_kline.high and new_kline.low > last_kline.low): + merged_kline = ChanKline( + last_kline.open, + max(last_kline.high, new_kline.high), + min(last_kline.low, new_kline.low), + new_kline.close, + new_kline.date, + last_kline.volume + new_kline.volume + ) + self.merged_klines[-1] = merged_kline + else: + self.merged_klines.append(new_kline) + + def identify_pivots(self): + # 保持原有的顶底点识别逻辑 + if len(self.merged_klines) < 3: + return + + for i in range(1, len(self.merged_klines) - 1): + prev, curr, next = self.merged_klines[i-1:i+2] + if curr.high > prev.high and curr.high > next.high: + self.pivots.append(Pivot(curr, 'high')) + elif curr.low < prev.low and curr.low < next.low: + self.pivots.append(Pivot(curr, 'low')) + + def identify_segments(self): + # 保持原有的线段识别逻辑 + if len(self.pivots) < 2: + return + + for i in range(len(self.pivots) - 1): + start, end = self.pivots[i], self.pivots[i+1] + if start.type != end.type: + direction = 1 if start.type == 'low' else -1 + self.segments.append(Segment(start.kline, end.kline, direction)) + + def identify_central_zones(self): + # 优化中枢识别逻辑 + if len(self.segments) < 3: + return + + for i in range(len(self.segments) - 2): + seg1, seg2, seg3 = self.segments[i:i+3] + if seg1.direction != seg3.direction: + overlap_high = min(seg1.end.high, seg2.end.high, seg3.start.high) + overlap_low = max(seg1.end.low, seg2.end.low, seg3.start.low) + if overlap_high > overlap_low: + zone = CentralZone(seg1.end, seg3.start) + if not self.central_zones or zone.low > self.central_zones[-1].high or zone.high < self.central_zones[-1].low: + self.central_zones.append(zone) + + def is_buy_point(self): + if not self.central_zones: + return 0 + + last_zone = self.central_zones[-1] + last_kline = self.merged_klines[-1] + prev_kline = self.merged_klines[-2] + + # 趋势判断 + trend = self.judge_trend() + + # 第一类买点:突破中枢上沿 + if prev_kline.close <= last_zone.high and last_kline.close > last_zone.high and trend == 1: + return 1 + + # 第二类买点:回调不破中枢 + if self.lines.buy_signal[-1] == 1 and last_kline.low > last_zone.low and trend == 1: + return 2 + + # 第三类买点:中枢震荡突破 + if last_zone.low < last_kline.close < last_zone.high and self.is_volume_breakout() and trend == 1: + return 3 + + return 0 + + def is_sell_point(self): + if not self.central_zones: + return 0 + + last_zone = self.central_zones[-1] + last_kline = self.merged_klines[-1] + + # 趋势判断 + trend = self.judge_trend() + + # 第一类卖点:跌破中枢下沿 + if last_kline.close < last_zone.low and self.merged_klines[-2].close >= last_zone.low and trend == -1: + return 1 + + # 第二类卖点:反弹不破中枢 + if self.lines.sell_signal[-1] == 1 and last_kline.high < last_zone.high and trend == -1: + return 2 + + # 第三类卖点:中枢震荡突破下沿 + if last_zone.low < last_kline.close < last_zone.high and self.is_volume_breakout() and trend == -1: + return 3 + + return 0 + + def judge_trend(self): + # 使用MACD判断趋势 + if self.macd.macd[0] > self.macd.signal[0] and self.macd.macd[0] > 0: + return 1 # 上升趋势 + elif self.macd.macd[0] < self.macd.signal[0] and self.macd.macd[0] < 0: + return -1 # 下降趋势 + else: + return 0 # 震荡 + + def is_volume_breakout(self): + # 判断是否出现放量 + return self.data.volume[0] > self.volume_ma[0] * 1.5 + + def is_divergence(self, seg1, seg2): + # 判断背驰 + price_change1 = abs(seg1.end.close - seg1.start.close) + price_change2 = abs(seg2.end.close - seg2.start.close) + volume1 = sum([k.volume for k in self.merged_klines[self.merged_klines.index(seg1.start):self.merged_klines.index(seg1.end)+1]]) + volume2 = sum([k.volume for k in self.merged_klines[self.merged_klines.index(seg2.start):self.merged_klines.index(seg2.end)+1]]) + return (seg1.direction == seg2.direction) and (price_change2 > price_change1) and (volume2 < volume1) + +class ImprovedChanStrategy(BaseStrategy): + params = ( + ('atr_period', 14), + ('atr_multiplier', 2), + ) + + def __init__(self): + super().__init__() + self.chan_indicators = {} + for d in self.datas: + self.chan_indicators[d] = ChanIndicator(d, period=20, atr_period=self.params.atr_period, atr_multiplier=self.params.atr_multiplier) + + def next(self): + for data in self.datas: + self.process_data(data) + + def process_data(self, data): + if self.orders.get(data): + return + + position = self.getposition(data) + buy_signal = self.chan_indicators[data].buy_signal[0] + sell_signal = self.chan_indicators[data].sell_signal[0] + + if not position: + if buy_signal > 0: + self.log(f'创建买入订单: {data._name} (买点类型: {buy_signal}), 价格: {data.close[0]}') + self.buy_stock(data) + else: + if sell_signal > 0: + self.log(f'创建卖出订单: {data._name} (卖点类型: {sell_signal}), 价格: {data.close[0]}') + self.close(data) + + self.check_stop_loss(data) + + def check_stop_loss(self, data): + position = self.getposition(data) + if not position: + return + + atr = self.chan_indicators[data].atr[0] + current_stop = self.buyprice[data] - self.params.atr_multiplier * atr + + if data.close[0] < current_stop: + self.log(f'触发动态止损: {data._name}, 价格: {data.close[0]}') + self.close(data) + + def buy_stock(self, data, size=None): + if size is None: + size = self.calculate_buy_size(data) + if size > 0: + self.orders[data] = self.buy(data=data, size=size) + self.position_value[data] = size * data.close[0] + self.buyprice[data] = data.close[0] + else: + self.log(f'可用资金不足,无法买入: {data._name}, 可用资金: {self.broker.getvalue() - sum(self.position_value.values())}') + + def calculate_buy_size(self, data): + available_cash = self.broker.getvalue() - sum(self.position_value.values()) + if available_cash <= 0: + return 0 + risk_per_trade = self.broker.getvalue() * 0.01 # 风险控制:每次交易最多损失账户1%的资金 + atr = self.chan_indicators[data].atr[0] + stop_loss = data.close[0] - self.params.atr_multiplier * atr + shares = int(risk_per_trade / (data.close[0] - stop_loss)) + return min(shares, int(available_cash / data.close[0])) + + diff --git a/instock/core/backtest/back_trader.py b/instock/core/backtest/back_trader.py new file mode 100644 index 0000000000..2a544089b6 --- /dev/null +++ b/instock/core/backtest/back_trader.py @@ -0,0 +1,81 @@ +import os +import random +import sys +from datetime import datetime, timedelta + +import backtrader as bt +import pandas as pd +import matplotlib +from instock.core.singleton_stock import stock_hist_data, stock_data +from instock.lib.singleton_type import singleton_type +import instock.core.tablestructure as tbs + +cpath_current = os.path.dirname(os.path.dirname(__file__)) +cpath = os.path.abspath(os.path.join(cpath_current, os.pardir)) +sys.path.append(cpath) + + +class CustomStrategy(bt.Strategy): + params = ( + ('maperiod5', 5), + ('maperiod13', 13), + ) + + def __init__(self): + self.dataclose = self.datas[0].close + self.order = None + self.buyprice = None + self.buycomm = None + self.m5 = bt.indicators.SimpleMovingAverage( + self.datas[0], period=self.params.maperiod5) + self.m13 = bt.indicators.SimpleMovingAverage( + self.datas[0], period=self.params.maperiod13) + + def next(self): + if self.order: + return + + if not self.position: + if self.m5[0] > self.m13[0]: + self.order = self.buy(price=self.m5[0]) + else: + if self.dataclose[0] < self.m5[0]: + self.order = self.sell(price=self.m13[0]) + +class back_test(metaclass=singleton_type): + def bt_strategy(self, date=None): + if date is None: + date = datetime.now() - timedelta(days=-1) + data = self.get_data(date, rand_num=5) + cerebro = bt.Cerebro() + cerebro.addstrategy(CustomStrategy) + cerebro.broker.set_cash(100000.0) + cerebro.addsizer(bt.sizers.PercentSizer, percents=50) + cerebro.broker.setcommission(commission=0.001) + for d in data: + cerebro.adddata(d) + cerebro.run() + cerebro.plot() + + def get_data(self, date, rand_num=100): + columns_ = stock_data(date).get_data()[list(tbs.TABLE_CN_STOCK_FOREIGN_KEY['columns'])] + stocks = [tuple(x) for x in columns_.values] + random_stocks = random.sample(stocks, rand_num) + data_list = [] + df = stock_hist_data(date=date, stocks=random_stocks).get_data() + for idx, row in df.items(): + if len(row) <= 100: + continue + row['datetime'] = pd.to_datetime(row['date']) + row.set_index('datetime', inplace=True) + data = bt.feeds.PandasData( + dataname=row + ) + data_list.append(data) + + return data_list + + +if __name__ == '__main__': + test = back_test() + test.bt_strategy() diff --git a/instock/core/backtest/backtrader_manager.py b/instock/core/backtest/backtrader_manager.py new file mode 100644 index 0000000000..ee495626d2 --- /dev/null +++ b/instock/core/backtest/backtrader_manager.py @@ -0,0 +1,148 @@ +import os +import random +import sys +from datetime import datetime, timedelta + +import backtrader as bt +import matplotlib +import matplotlib.dates as mdates +import numpy as np +import pandas as pd +from matplotlib import pyplot as plt + +import instock.core.tablestructure as tbs +from instock.core.backtest.Chan import ImprovedChanStrategy +from instock.core.backtest.volume_break_strategy import VolumeBreakStrategy +from instock.core.singleton_stock import stock_hist_data, stock_data +from instock.lib.singleton_type import singleton_type + +cpath_current = os.path.dirname(os.path.dirname(__file__)) +cpath = os.path.abspath(os.path.join(cpath_current, os.pardir)) +sys.path.append(cpath) + + +class back_test(metaclass=singleton_type): + def bt_strategy(self, date=None, strategy=None,rand_num=2): + if date is None: + date = datetime.now() - timedelta(days=1) + if strategy is None: + raise Exception("策略类为空") + data = self.get_data(date=date, rand_num=rand_num) + cerebro = bt.Cerebro() + cerebro.addstrategy(strategy) + cerebro.broker.set_cash(100000.0) + cerebro.addsizer(bt.sizers.PercentSizer, percents=50) + cerebro.broker.setcommission(commission=0.0001) + for d in data: + cerebro.adddata(d) + + cerebro.run() + + # 计算收益率 + initial_value = 100000.0 + final_value = cerebro.broker.getvalue() + profit = final_value - initial_value + profit_rate = (profit / initial_value) * 100 + + print(f'总收益: ¥{profit:.2f}') + print(f'收益率: {profit_rate:.2f}%') + # 设置中文字体 + matplotlib.rcParams['font.sans-serif'] = ['Arial Unicode MS'] + matplotlib.rcParams['axes.unicode_minus'] = False + figs = cerebro.plot(volume=True, style='candle', + barup='red', bardown='green', + volup='red', voldown='green', + subplot=False, # 不使用子图 + voloverlay=False, # 成交量不覆盖在K线上 + volscaling=0.3, # 成交量高度占比 + volpushup=1.0) + # 绘制图表,包括K线图 + fig = figs[0][0] + fig.set_size_inches(40, 20) + fig.suptitle("策略回测结果", fontsize=16) + fig.grid(False) + ax1 = fig.axes[0] # 获取主要的坐标轴 + + # 显示图表 + # 设置x轴日期格式 + ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d')) + plt.setp(ax1.xaxis.get_majorticklabels(), rotation=45) + plt.tight_layout() + plt.show() + + def bt_strategy_multiple(self, date=None, strategy=None, iterations=10): + if date is None: + date = datetime.now() - timedelta(days=1) + if strategy is None: + raise Exception("策略类为空") + + results = [] + data = self.get_data(date, rand_num=iterations) + for i in range(len(data)): + cerebro = bt.Cerebro() + cerebro.addstrategy(strategy) + cerebro.broker.set_cash(100000.0) + cerebro.addsizer(bt.sizers.PercentSizer, percents=50) + cerebro.broker.setcommission(commission=0.0001) + cerebro.adddata(data[i]) + + initial_value = cerebro.broker.getvalue() + cerebro.run() + final_value = cerebro.broker.getvalue() + profit = final_value - initial_value + profit_rate = (profit / initial_value) * 100 + + results.append({ + 'iteration': i + 1, + 'initial_value': initial_value, + 'final_value': final_value, + 'profit': profit, + 'profit_rate': profit_rate + }) + + print(f'迭代次数 {i + 1}: 利润: {profit:.2f}, 利润率: {profit_rate:.2f}%') + + + # 统计结果 + profits = [r['profit'] for r in results] + profit_rates = [r['profit_rate'] for r in results] + winning_trades = sum(1 for p in profits if p > 0) + + print("\n总结:") + print(f"总迭代次数: {iterations}") + print(f"盈利交易次数: {winning_trades}") + print(f"胜率: {winning_trades / iterations * 100:.2f}%") + print(f"平均利润: {np.mean(profits):.2f}") + print(f"平均利润率: {np.mean(profit_rates):.2f}%") + print(f"最大利润: {max(profits):.2f}") + print(f"最小利润: {min(profits):.2f}") + print(f"利润标准差: {np.std(profits):.2f}") + + return results + + def get_data(self, date, rand_num=100): + columns_ = stock_data(date).get_data()[list(tbs.TABLE_CN_STOCK_FOREIGN_KEY['columns'])] + stocks = [tuple(x) for x in columns_.values] + random_stocks = random.sample(stocks, rand_num) + data_list = [] + df = stock_hist_data(date=date, stocks=random_stocks).get_data() + + for idx, row in df.items(): + if len(row) <= 100: + continue + # row = row.iloc[-30:] + row['datetime'] = pd.to_datetime(row['date']) + row.set_index('datetime', inplace=True) + data = bt.feeds.PandasData( + dataname=row, + name=idx[1] + idx[2] + ) + data_list.append(data) + + return data_list + + +if __name__ == '__main__': + test = back_test() + test.bt_strategy_multiple(strategy=VolumeBreakStrategy, iterations=500) + # test.bt_strategy(strategy=VolumeBreakStrategy, rand_num=10) diff --git a/instock/core/backtest/base_strategy.py b/instock/core/backtest/base_strategy.py new file mode 100644 index 0000000000..08b84e7534 --- /dev/null +++ b/instock/core/backtest/base_strategy.py @@ -0,0 +1,141 @@ +import backtrader as bt + + +class BaseStrategy(bt.Strategy): + params = ( + ('stop_loss', 0.06), + ('take_profit', 0.30), + ) + + def __init__(self): + self.orders = {} + self.position_value = {} + self.buyprice = {} + self.buycomm = {} + self.stop_loss_price = {} + self.take_profit_price = {} + + def buy_stock(self, data, size=None): + if size is None: + size = self.calculate_buy_size(data) + if size > 0: + limit_up_price = self.calculate_limit_up_price(data) + if data.close[0] >= limit_up_price: + self.log(f'涨停无法买入: {data._name}, 价格: {data.close[0]}, 涨停价: {limit_up_price}') + return + + if data.close[0] > 0: + self.stop_loss_price[data] = max(data.close[0] * (1 - self.params.stop_loss), 0.01) + self.take_profit_price[data] = max(data.close[0] * (1 + self.params.take_profit), 0.01) + else: + self.log(f'警告: {data._name} 的收盘价为0,无法设置止损和止盈') + return + + self.orders[data] = self.buy(data=data, size=size) + self.position_value[data] = size * data.close[0] + else: + self.log(f'可用资金不足,无法买入: {data._name}, 可用资金: {self.broker.getvalue() - sum(self.position_value.values())}') + + def calculate_buy_size(self, data, ratio=None): + available_cash = self.broker.getvalue() - sum(self.position_value.values()) + if available_cash <= 0: + return 0 + if ratio is None: + if self.position: + ratio = 0.2 + else: + ratio = 0.5 + return int(round(available_cash * ratio / data.close[0] / 100) * 100) + + def check_sell_strategy(self, data): + position = self.getposition(data) + if position: + if data in self.stop_loss_price and data.close[0] <= self.stop_loss_price[data]: + self.log(f'触发止损: {data._name}, 价格: {data.close[0]}') + self.close(data) + return True + if data in self.take_profit_price and data.close[0] >= self.take_profit_price[data]: + if data.low[0] < data.low[-1] and data.high[0] < data.high[-1]: + self.log(f'触发最低点低于昨日最低点卖出: {data._name}, 价格: {data.close[0]}') + self.close(data) + return True + if data.low[0] < data.low[-1] < data.low[-2] and data.high[0] < data.high[-1] < data.high[-2]: + if position: + self.log(f'触发最高和最低点低于昨日卖出: {data._name}, 价格: {data.close[0]}') + self.close(data) + return True + + if data.volume[0] == max(data.volume.get(size=7) or [0]) and data.close[0] < data.low[-1]: + if position: + self.log(f'触发成交量和价格条件卖出: {data._name}, 价格: {data.close[0]}') + self.close(data) + return True + return False + + @staticmethod + def calculate_limit_up_price(data): + previous_close = data.close[-1] + stock_code = data._name[:6] + limit_up_ratio = 0.20 if stock_code.startswith('300') else 0.10 + base_limit_up_price = round(previous_close * (1 + limit_up_ratio), 2) + return base_limit_up_price * 1.001 + + def log(self, txt, dt=None): + dt = dt or self.datas[0].datetime.date(0) + print(f'{dt.isoformat()}, {txt}') + + def notify_order(self, order): + if order.status in [order.Submitted, order.Accepted]: + return + + if order.status in [order.Completed]: + if order.isbuy(): + self.buyprice[order.data] = order.executed.price + self.buycomm[order.data] = order.executed.comm + self.log( + f'买入执行: {order.data._name}, 价格: {order.executed.price:.2f}, ' + f'成本: {order.executed.value:.2f}, 佣金: {order.executed.comm:.2f}, ' + f'数量: {order.executed.size}, ' + f'止损价: {self.stop_loss_price.get(order.data, 0):.2f}, ' + f'止盈价: {self.take_profit_price.get(order.data, 0):.2f}' + ) + else: + buyprice = self.buyprice[order.data] + buycomm = self.buycomm[order.data] + sellprice = order.executed.price + sellcomm = order.executed.comm + profit = (sellprice - buyprice) * order.executed.size * -1 - buycomm - sellcomm + profit_pct = (profit / (buyprice * order.executed.size)) * 100 * -1 + self.log( + f'卖出执行: {order.data._name}, 价格: {sellprice:.2f}, ' + f'成本: {order.executed.value:.2f}, 佣金: {sellcomm:.2f}, ' + f'数量: {order.executed.size}, 利润: {profit:.2f}, ' + f'利润率: {profit_pct:.2f}%' + ) + self.position_value[order.data] = 0 + self.stop_loss_price.pop(order.data, None) + self.take_profit_price.pop(order.data, None) + + elif order.status in [order.Canceled, order.Margin, order.Rejected]: + self.log(f'订单被取消/保证金不足/拒绝: {order.data._name}') + + self.orders[order.data] = None + + def notify_trade(self, trade): + if not trade.isclosed: + return + self.log(f'交易利润: {trade.data._name}, 毛利: {trade.pnl:.2f}, 净利: {trade.pnlcomm:.2f}') + + def stop(self): + self.log(f'最终投资组合价值: {self.broker.getvalue():.2f}') + for data in self.datas: + position = self.getposition(data) + if position.size != 0: + unrealized_pnl = position.size * (data.close[0] - self.buyprice[data]) - self.buycomm[data] + unrealized_pnl_pct = (unrealized_pnl / (self.buyprice[data] * position.size)) * 100 + self.log( + f'最终持仓 {data._name}: {position.size} 股, ' + f'价值: {position.size * data.close[0]:.2f}, ' + f'未实现盈亏: {unrealized_pnl:.2f}, ' + f'未实现盈亏率: {unrealized_pnl_pct:.2f}%' + ) diff --git a/instock/core/backtest/strategy_group.py b/instock/core/backtest/strategy_group.py new file mode 100644 index 0000000000..217cd3104d --- /dev/null +++ b/instock/core/backtest/strategy_group.py @@ -0,0 +1,284 @@ +import backtrader as bt +import numpy as np + +class BacktraceMA250(bt.Strategy): + params = ( + ('period', 90), + ) + + def __init__(self): + self.ma250 = bt.indicators.SimpleMovingAverage(self.data.close, period=self.params.period) + self.highest = bt.indicators.Highest(self.data.close, period=60) + self.volume = bt.indicators.SimpleMovingAverage(self.data.volume, period=5) + + def next(self): + if len(self) < 60: + return + + if not self.position: + if (self.data.close[-60] < self.ma250[-60] and self.data.close[0] > self.ma250[0] and + self.data.close[0] >= self.highest[-1] and + self.data.volume[0] / self.volume[0] > 2 and + self.data.close[0] / self.highest[0] < 0.8): + self.buy() + + elif self.position: + if self.data.close[0] < self.ma250[0]: + self.sell() + +class BreakthroughPlatform(bt.Strategy): + params = ( + ('period', 60), + ) + + def __init__(self): + self.ma60 = bt.indicators.SimpleMovingAverage(self.data.close, period=self.params.period) + self.volume = bt.indicators.SimpleMovingAverage(self.data.volume, period=5) + + def next(self): + if len(self) < self.params.period: + return + + if not self.position: + if (self.data.open[0] < self.ma60[0] <= self.data.close[0] and + self.data.volume[0] > self.volume[0] * 2): + for i in range(1, self.params.period): + if not -0.05 < (self.ma60[-i] - self.data.close[-i]) / self.ma60[-i] < 0.2: + return + self.buy() + + elif self.position: + if self.data.close[0] < self.ma60[0]: + self.sell() + +class ClimaxLimitdown(bt.Strategy): + params = ( + ('volume_times', 4), + ('amount_threshold', 200000000), + ) + + def __init__(self): + self.volume_ma5 = bt.indicators.SimpleMovingAverage(self.data.volume, period=5) + + def next(self): + if not self.position: + if (self.data.close[0] / self.data.open[0] - 1 < -0.095 and + self.data.close[0] * self.data.volume[0] >= self.params.amount_threshold and + self.data.volume[0] >= self.volume_ma5[0] * self.params.volume_times): + self.buy() + + elif self.position: + if self.data.close[0] > self.data.open[0]: + self.sell() + + +class EnterStrategy(bt.Strategy): + params = ( + ('volume_times', 2), + ('amount_threshold', 200000000), + ) + + def __init__(self): + self.volume_ma5 = bt.indicators.SimpleMovingAverage(self.data.volume, period=5) + + def next(self): + if not self.position: + if (self.data.close[0] / self.data.open[0] - 1 > 0.02 and + self.data.close[0] * self.data.volume[0] >= self.params.amount_threshold and + self.data.volume[0] >= self.volume_ma5[0] * self.params.volume_times): + self.buy() + + elif self.position: + if self.data.close[0] < self.data.open[0]: + self.sell() + + +class HighTightFlag(bt.Strategy): + params = ( + ('lookback', 24), + ('threshold', 1.9), + ) + + def __init__(self): + self.order = None + + def next(self): + if self.order: + return + + if not self.position: + lookback_low = min(self.data.low.get(size=self.params.lookback)) + if self.data.close[0] / lookback_low >= self.params.threshold: + count = 0 + for i in range(1, self.params.lookback + 1): + if self.data.close[-i] / self.data.open[-i] - 1 >= 0.095: + count += 1 + else: + count = 0 + if count == 2: + self.order = self.buy() + break + + elif self.position: + if self.data.close[0] < self.data.open[0]: + self.order = self.sell() + +class KeepIncreasing(bt.Strategy): + params = ( + ('period', 30), + ('threshold', 1.2), + ) + + def __init__(self): + self.ma30 = bt.indicators.SimpleMovingAverage(self.data.close, period=self.params.period) + + def next(self): + if len(self) < self.params.period: + return + + if not self.position: + if (self.ma30[0] > self.ma30[-10] > self.ma30[-20] > self.ma30[-30] and + self.ma30[0] / self.ma30[-30] > self.params.threshold): + self.buy() + + elif self.position: + if self.data.close[0] < self.ma30[0]: + self.sell() + + +class LowATRGrowth(bt.Strategy): + params = ( + ('period', 10), + ('atr_threshold', 10), + ('growth_threshold', 1.1), + ) + + def __init__(self): + self.atr = bt.indicators.ATR(self.data, period=self.params.period) + + def next(self): + if len(self) < self.params.period: + return + + if not self.position: + highest = max(self.data.close.get(size=self.params.period)) + lowest = min(self.data.close.get(size=self.params.period)) + if (self.atr[0] <= self.params.atr_threshold and + highest / lowest >= self.params.growth_threshold): + self.buy() + + elif self.position: + if self.data.close[0] < self.data.open[0]: + self.sell() + + +class LowBacktraceIncrease(bt.Strategy): + params = ( + ('period', 60), + ('increase_threshold', 0.6), + ('decline_threshold', -0.07), + ('cumulative_threshold', -0.1), + ) + + def __init__(self): + self.order = None + + def next(self): + if self.order: + return + + if len(self) < self.params.period: + return + + if not self.position: + if self.data.close[0] / self.data.close[-self.params.period] - 1 >= self.params.increase_threshold: + for i in range(1, self.params.period + 1): + if (self.data.close[-i] / self.data.open[-i] - 1 <= self.params.decline_threshold or + self.data.close[-i] / self.data.close[-i-1] - 1 <= self.params.cumulative_threshold): + return + self.order = self.buy() + + elif self.position: + if self.data.close[0] < self.data.open[0]: + self.order = self.sell() + + +class ParkingApron(bt.Strategy): + params = ( + ('lookback', 15), + ('increase_threshold', 0.095), + ('consolidation_days', 3), + ('consolidation_threshold', 0.03), + ) + + def __init__(self): + self.order = None + + def next(self): + if self.order: + return + + if len(self) < self.params.lookback: + return + + if not self.position: + for i in range(1, self.params.lookback + 1): + if self.data.close[-i] / self.data.open[-i] - 1 > self.params.increase_threshold: + if i >= self.params.consolidation_days: + is_consolidating = True + for j in range(1, self.params.consolidation_days + 1): + if abs(self.data.close[-j] / self.data.open[-j] - 1) > self.params.consolidation_threshold: + is_consolidating = False + break + if is_consolidating: + self.order = self.buy() + break + + elif self.position: + if self.data.close[0] < self.data.open[0]: + self.order = self.sell() + + +class TurtleTrade(bt.Strategy): + params = ( + ('period', 60), + ) + + def __init__(self): + self.highest = bt.indicators.Highest(self.data.close, period=self.params.period) + + def next(self): + if len(self) < self.params.period: + return + + if not self.position: + if self.data.close[0] >= self.highest[-1]: + self.buy() + + elif self.position: + if self.data.close[0] < self.data.open[0]: + self.sell() + +class MACDStrategy(bt.Strategy): + params = ( + ('fast_period', 12), + ('slow_period', 26), + ('signal_period', 9), + ) + + def __init__(self): + self.macd = bt.indicators.MACD( + self.data.close, + period_me1=self.params.fast_period, + period_me2=self.params.slow_period, + period_signal=self.params.signal_period + ) + self.crossover = bt.indicators.CrossOver(self.macd.macd, self.macd.signal) + + def next(self): + if not self.position: + if self.crossover > 0: # 金叉买入信号 + self.buy() + else: + if self.crossover < 0: # 死叉卖出信号 + self.sell() \ No newline at end of file diff --git a/instock/core/backtest/strategy_tester.py b/instock/core/backtest/strategy_tester.py new file mode 100644 index 0000000000..7e7bff1d9d --- /dev/null +++ b/instock/core/backtest/strategy_tester.py @@ -0,0 +1,123 @@ +import backtrader as bt +import pandas as pd +from datetime import datetime, timedelta +import random +from instock.core.singleton_stock import stock_hist_data, stock_data +import instock.core.tablestructure as tbs +from instock.core.backtest.strategy_group import * + +class StrategyTester: + def __init__(self): + self.strategies = [ + BacktraceMA250, BreakthroughPlatform, ClimaxLimitdown, + EnterStrategy, HighTightFlag, KeepIncreasing, LowATRGrowth, + LowBacktraceIncrease, ParkingApron, TurtleTrade, MACDStrategy + ] + + def get_data(self, date, rand_num=10): + columns_ = stock_data(date).get_data()[list(tbs.TABLE_CN_STOCK_FOREIGN_KEY['columns'])] + stocks = [tuple(x) for x in columns_.values] + random_stocks = random.sample(stocks, rand_num) + data_list = [] + df = stock_hist_data(date=date, stocks=random_stocks).get_data() + for idx, row in df.items(): + if len(row) <= 100: + continue + row['datetime'] = pd.to_datetime(row['date']) + row.set_index('datetime', inplace=True) + data = bt.feeds.PandasData( + dataname=row, + name=idx[1] + idx[2] + ) + data_list.append(data) + return data_list + + def run_backtest(self, strategy, data): + cerebro = bt.Cerebro() + cerebro.addstrategy(strategy) + cerebro.adddata(data) + cerebro.broker.setcash(100000.0) + cerebro.broker.setcommission(commission=0.001) + cerebro.addsizer(bt.sizers.PercentSizer, percents=95) + + # 添加交易分析器 + cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades") + + initial_value = cerebro.broker.getvalue() + results = cerebro.run() + final_value = cerebro.broker.getvalue() + + return results[0], initial_value, final_value + + def evaluate_strategy(self, strategy, data): + try: + strategy_results, initial_value, final_value = self.run_backtest(strategy, data) + + # 获取交易分析结果 + trade_analyzer = strategy_results.analyzers.trades.get_analysis() + + # 计算总交易次数和盈利交易次数 + total_trades = trade_analyzer.total.closed + won_trades = trade_analyzer.won.total if hasattr(trade_analyzer, 'won') else 0 + + max_profit = final_value - initial_value + win_rate = won_trades / total_trades if total_trades > 0 else 0 + + return { + 'strategy': strategy.__name__, + 'max_profit': max_profit, + 'win_rate': win_rate + } + except Exception as e: + print(f"Error evaluating strategy {strategy.__name__}: {str(e)}") + return None + + def run_all_strategies(self, date=None, iterations=5): + if date is None: + date = datetime.now() - timedelta(days=1) + + all_results = [] + + for _ in range(iterations): + data_list = self.get_data(date) + for data in data_list: + for strategy in self.strategies: + result = self.evaluate_strategy(strategy, data) + if result: + all_results.append(result) + + return all_results + + def find_best_strategy(self, results): + strategy_performance = {} + for result in results: + strategy_name = result['strategy'] + if strategy_name not in strategy_performance: + strategy_performance[strategy_name] = { + 'total_profit': 0, + 'total_win_rate': 0, + 'count': 0 + } + strategy_performance[strategy_name]['total_profit'] += result['max_profit'] + strategy_performance[strategy_name]['total_win_rate'] += result['win_rate'] + strategy_performance[strategy_name]['count'] += 1 + + for strategy, performance in strategy_performance.items(): + performance['avg_profit'] = performance['total_profit'] / performance['count'] + performance['avg_win_rate'] = performance['total_win_rate'] / performance['count'] + + best_strategy = max(strategy_performance.items(), key=lambda x: x[1]['avg_profit']) + return best_strategy + +if __name__ == '__main__': + tester = StrategyTester() + results = tester.run_all_strategies(iterations=5) + + print("All Strategy Results:") + for result in results: + print(f"Strategy: {result['strategy']}, Max Profit: {result['max_profit']:.2f}, Win Rate: {result['win_rate']:.2f}") + + best_strategy, performance = tester.find_best_strategy(results) + print(f"\nBest Strategy: {best_strategy}") + print(f"Average Profit: {performance['avg_profit']:.2f}") + print(f"Average Win Rate: {performance['avg_win_rate']:.2f}") diff --git a/instock/core/backtest/volume_break_strategy.py b/instock/core/backtest/volume_break_strategy.py new file mode 100644 index 0000000000..ecb28d70a8 --- /dev/null +++ b/instock/core/backtest/volume_break_strategy.py @@ -0,0 +1,57 @@ +import backtrader as bt +import numpy as np +from instock.core.backtest.base_strategy import BaseStrategy + + +class VolumeBreakStrategy(BaseStrategy): + params = ( + ('volume_threshold', 1.5), # 成交量阈值 + ('price_drop_threshold', 0.10), # 价格下跌阈值 + ('lookback_period', 7), # 回看天数 + ) + + def __init__(self): + super().__init__() + self.volume_ma = bt.indicators.SimpleMovingAverage( + self.data.volume, period=self.params.lookback_period) + self.high_price = bt.indicators.Highest( + self.data.high, period=self.params.lookback_period) + self.low_price = bt.indicators.Lowest( + self.data.low, period=self.params.lookback_period) + self.max_volume = bt.indicators.Highest( + self.data.volume, period=self.params.lookback_period) + + def next(self): + for data in self.datas: + self.process_data(data) + + def process_data(self, data): + if self.orders.get(data): + return + + + position = self.getposition(data) + if self.check_sell_strategy(data): + return + if self.buy_condition(data): + self.log(f'买入信号: {data._name}, 价格: {data.close[0]}') + self.buy_stock(data) + return + + + def buy_condition(self, data): + # 条件1: 最近七天有一天成交量大于当天-7天成交量均值的150% + volume_condition = np.any(np.array(data.volume.get(size=self.params.lookback_period)) > + self.volume_ma * self.params.volume_threshold) + + # 条件2: 当天的收盘价低于最近七天的最高价的15% + price_condition = data.close[0] < self.high_price[0] * (1 - self.params.price_drop_threshold) + + # 条件3: 当天的成交量低于最高日的成交量 + current_volume_condition = data.volume[0] < self.max_volume[0] + + # 条件4: 当天的最低价格高于昨天的最低价 + low_price_condition = data.low[0] > data.low[-1] + + return (volume_condition and price_condition and + current_volume_condition and low_price_condition) diff --git a/instock/core/singleton_stock.py b/instock/core/singleton_stock.py index 996bc0bf0f..d0a06339e8 100644 --- a/instock/core/singleton_stock.py +++ b/instock/core/singleton_stock.py @@ -1,8 +1,8 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- - -import logging import concurrent.futures +import logging + import instock.core.stockfetch as stf import instock.core.tablestructure as tbs import instock.lib.trade_time as trd diff --git a/instock/core/strategy/common_sell_check.py b/instock/core/strategy/common_sell_check.py new file mode 100644 index 0000000000..ce90503ae4 --- /dev/null +++ b/instock/core/strategy/common_sell_check.py @@ -0,0 +1,63 @@ +#!/usr/local/bin/python +# -*- coding: utf-8 -*- + +import numpy as np +import talib as tl + +__author__ = 'myh ' +__date__ = '2023/3/10 ' + +# 止损止盈 +params = { + 'stop_loss': 0.06, + 'take_profit': 0.30, + 'volume_threshold': 1.5, + 'price_drop_threshold': 0.15, + 'lookback_period': 7 +} + +# 通用卖出策略 +def check(code_name, data, date=None, cost=None, threshold=60): + if date is None: + end_date = code_name[0] + else: + end_date = date.strftime("%Y-%m-%d") + if end_date is not None: + mask = (data['date'] <= end_date) + data = data.loc[mask].copy() + if cost is None: + return False + if len(data.index) < threshold: + return True + # 检查是否应该卖出 + if check_sell_signal(data, cost=cost, stop_loss=params['stop_loss'], take_profit=params['take_profit']): + return True # 如果应该卖出,则不考虑买入 + + return False + +def check_sell_signal(data, cost, stop_loss, take_profit): + latest_data = data.iloc[-1] + previous_data = data.iloc[-2] + + if cost: + # 检查止损 + if latest_data['close'] <= cost * (1 - stop_loss): + return True + + # 检查止盈 + if latest_data['close'] >= cost * (1 + take_profit): + if latest_data['low'] < previous_data['low'] and latest_data['high'] < previous_data['high']: + return True + + # 检查最高和最低点是否低于昨日 + if (latest_data['low'] < previous_data['low'] < data.iloc[-3]['low'] and + latest_data['high'] < previous_data['high'] < data.iloc[-3]['high']): + return True + + # 检查成交量和价格条件 + if (latest_data['volume'] == max(data['volume'].tail(params['lookback_period'])) and + latest_data['close'] < previous_data['low']): + return True + + return False + diff --git a/instock/core/strategy/volume_break.py b/instock/core/strategy/volume_break.py new file mode 100644 index 0000000000..9425c00311 --- /dev/null +++ b/instock/core/strategy/volume_break.py @@ -0,0 +1,90 @@ +#!/usr/local/bin/python +# -*- coding: utf-8 -*- + +import numpy as np +import talib as tl + +__author__ = 'myh ' +__date__ = '2023/3/10 ' + +# 止损止盈 +params = { + 'stop_loss': 0.06, + 'take_profit': 0.30, + 'volume_threshold': 1.5, + 'price_drop_threshold': 0.15, + 'lookback_period': 7 +} + +# 量价突破策略 +# 1.最近七天有一天成交量大于当天-7天成交量均值的150% +# 2.当天的收盘价低于最近七天的最高价的15% +# 3.当天的成交量低于最高日的成交量 +# 4.当天的最低价格高于昨天的最低价 +def check(code_name, data, date=None, threshold=60, cost=None): + if date is None: + end_date = code_name[0] + else: + end_date = date.strftime("%Y-%m-%d") + if end_date is not None: + mask = (data['date'] <= end_date) + data = data.loc[mask].copy() + if len(data.index) < threshold: + return False + + # 检查是否应该卖出 + if check_sell_signal(data, cost=cost, stop_loss=params['stop_loss'], take_profit=params['take_profit']): + return False # 如果应该卖出,则不考虑买入 + + # 计算需要的指标 + data['volume_ma'] = tl.MA(data['volume'].values, timeperiod=params['lookback_period']) + data['volume_ma'].values[np.isnan(data['volume_ma'].values)] = 0.0 + data['high_price'] = data['high'].rolling(window=params['lookback_period']).max() + data['max_volume'] = data['volume'].rolling(window=params['lookback_period']).max() + + # 获取最近的数据 + recent_data = data.tail(params['lookback_period']) + latest_data = recent_data.iloc[-1] + + # 条件1: 最近七天有一天成交量大于当天-7天成交量均值的150% + volume_condition = (recent_data['volume'] > recent_data['volume_ma'] * params['volume_threshold']).any() + + # 条件2: 当天的收盘价低于最近七天的最高价的15% + price_condition = latest_data['close'] < latest_data['high_price'] * (1 - params['price_drop_threshold']) + + # 条件3: 当天的成交量低于最高日的成交量 + current_volume_condition = latest_data['volume'] < latest_data['max_volume'] + + # 条件4: 当天的最低价格高于昨天的最低价 + low_price_condition = latest_data['low'] > data['low'].iloc[-2] + + # 所有条件都满足时返回True + return (volume_condition and price_condition and + current_volume_condition and low_price_condition) + +def check_sell_signal(data, cost, stop_loss, take_profit): + latest_data = data.iloc[-1] + previous_data = data.iloc[-2] + + if cost: + # 检查止损 + if latest_data['close'] <= cost * (1 - stop_loss): + return True + + # 检查止盈 + if latest_data['close'] >= cost * (1 + take_profit): + if latest_data['low'] < previous_data['low'] and latest_data['high'] < previous_data['high']: + return True + + # 检查最高和最低点是否低于昨日 + if (latest_data['low'] < previous_data['low'] < data.iloc[-3]['low'] and + latest_data['high'] < previous_data['high'] < data.iloc[-3]['high']): + return True + + # 检查成交量和价格条件 + if (latest_data['volume'] == max(data['volume'].tail(params['lookback_period'])) and + latest_data['close'] < previous_data['low']): + return True + + return False + diff --git a/instock/core/tablestructure.py b/instock/core/tablestructure.py index 44c841f00f..df1a4750c7 100644 --- a/instock/core/tablestructure.py +++ b/instock/core/tablestructure.py @@ -4,7 +4,7 @@ from sqlalchemy import DATE, VARCHAR, FLOAT, BIGINT, SmallInteger, DATETIME from sqlalchemy.dialects.mysql import BIT import talib as tl -from instock.core.strategy import enter +from instock.core.strategy import enter, volume_break, common_sell_check from instock.core.strategy import turtle_trade from instock.core.strategy import climax_limitdown from instock.core.strategy import low_atr @@ -156,47 +156,74 @@ CN_STOCK_SECTOR_FUND_FLOW = (('行业资金流', '概念资金流'), ({'name': 'stock_sector_fund_flow_rank', 'cn': '今日', - 'columns': {'name': {'type': VARCHAR(20, _COLLATE), 'cn': '名称', 'size': 70}, - 'change_rate': {'type': FLOAT, 'cn': '今日涨跌幅', 'size': 70}, - 'fund_amount': {'type': BIGINT, 'cn': '今日主力净流入-净额', 'size': 100}, - 'fund_rate': {'type': FLOAT, 'cn': '今日主力净流入-净占比', 'size': 70}, - 'fund_amount_super': {'type': BIGINT, 'cn': '今日超大单净流入-净额', 'size': 100}, - 'fund_rate_super': {'type': FLOAT, 'cn': '今日超大单净流入-净占比', 'size': 70}, - 'fund_amount_large': {'type': BIGINT, 'cn': '今日大单净流入-净额', 'size': 100}, - 'fund_rate_large': {'type': FLOAT, 'cn': '今日大单净流入-净占比', 'size': 70}, - 'fund_amount_medium': {'type': BIGINT, 'cn': '今日中单净流入-净额', 'size': 100}, - 'fund_rate_medium': {'type': FLOAT, 'cn': '今日中单净流入-净占比', 'size': 70}, - 'fund_amount_small': {'type': BIGINT, 'cn': '今日小单净流入-净额', 'size': 100}, - 'fund_rate_small': {'type': FLOAT, 'cn': '今日小单净流入-净占比', 'size': 70}, - 'stock_name': {'type': VARCHAR(20, _COLLATE), 'cn': '今日主力净流入最大股', 'size': 70}}}, - {'name': 'stock_individual_fund_flow_rank', 'cn': '5日', - 'columns': {'name': {'type': VARCHAR(20, _COLLATE), 'cn': '名称', 'size': 70}, - 'change_rate_5': {'type': FLOAT, 'cn': '5日涨跌幅', 'size': 70}, - 'fund_amount_5': {'type': BIGINT, 'cn': '5日主力净流入-净额', 'size': 100}, - 'fund_rate_5': {'type': FLOAT, 'cn': '5日主力净流入-净占比', 'size': 70}, - 'fund_amount_super_5': {'type': BIGINT, 'cn': '5日超大单净流入-净额', 'size': 100}, - 'fund_rate_super_5': {'type': FLOAT, 'cn': '5日超大单净流入-净占比', 'size': 70}, - 'fund_amount_large_5': {'type': BIGINT, 'cn': '5日大单净流入-净额', 'size': 100}, - 'fund_rate_large_5': {'type': FLOAT, 'cn': '5日大单净流入-净占比', 'size': 70}, - 'fund_amount_medium_5': {'type': BIGINT, 'cn': '5日中单净流入-净额', 'size': 100}, - 'fund_rate_medium_5': {'type': FLOAT, 'cn': '5日中单净流入-净占比', 'size': 70}, - 'fund_amount_small_5': {'type': BIGINT, 'cn': '5日小单净流入-净额', 'size': 100}, - 'fund_rate_small_5': {'type': FLOAT, 'cn': '5日小单净流入-净占比', 'size': 70}, - 'stock_name_5': {'type': VARCHAR(20, _COLLATE), 'cn': '5日主力净流入最大股', 'size': 70}}}, - {'name': 'stock_individual_fund_flow_rank', 'cn': '10日', - 'columns': {'name': {'type': VARCHAR(20, _COLLATE), 'cn': '名称', 'size': 70}, - 'change_rate_10': {'type': FLOAT, 'cn': '10日涨跌幅', 'size': 70}, - 'fund_amount_10': {'type': BIGINT, 'cn': '10日主力净流入-净额', 'size': 100}, - 'fund_rate_10': {'type': FLOAT, 'cn': '10日主力净流入-净占比', 'size': 70}, - 'fund_amount_super_10': {'type': BIGINT, 'cn': '10日超大单净流入-净额', 'size': 100}, - 'fund_rate_super_10': {'type': FLOAT, 'cn': '10日超大单净流入-净占比', 'size': 70}, - 'fund_amount_large_10': {'type': BIGINT, 'cn': '10日大单净流入-净额', 'size': 100}, - 'fund_rate_large_10': {'type': FLOAT, 'cn': '10日大单净流入-净占比', 'size': 70}, - 'fund_amount_medium_10': {'type': BIGINT, 'cn': '10日中单净流入-净额', 'size': 100}, - 'fund_rate_medium_10': {'type': FLOAT, 'cn': '10日中单净流入-净占比', 'size': 70}, - 'fund_amount_small_10': {'type': BIGINT, 'cn': '10日小单净流入-净额', 'size': 100}, - 'fund_rate_small_10': {'type': FLOAT, 'cn': '10日小单净流入-净占比', 'size': 70}, - 'stock_name_10': {'type': VARCHAR(20, _COLLATE), 'cn': '10日主力净流入最大股', 'size': 70}}})) + 'columns': {'name': {'type': NVARCHAR(20), 'cn': '名称', 'size': 70}, + 'change_rate': {'type': FLOAT, 'cn': '今日涨跌幅', 'size': 70}, + 'fund_amount': {'type': BIGINT, 'cn': '今日主力净流入-净额', 'size': 100}, + 'fund_rate': {'type': FLOAT, 'cn': '今日主力净流入-净占比', 'size': 70}, + 'fund_amount_super': {'type': BIGINT, 'cn': '今日超大单净流入-净额', + 'size': 100}, + 'fund_rate_super': {'type': FLOAT, 'cn': '今日超大单净流入-净占比', + 'size': 70}, + 'fund_amount_large': {'type': BIGINT, 'cn': '今日大单净流入-净额', + 'size': 100}, + 'fund_rate_large': {'type': FLOAT, 'cn': '今日大单净流入-净占比', + 'size': 70}, + 'fund_amount_medium': {'type': BIGINT, 'cn': '今日中单净流入-净额', + 'size': 100}, + 'fund_rate_medium': {'type': FLOAT, 'cn': '今日中单净流入-净占比', + 'size': 70}, + 'fund_amount_small': {'type': BIGINT, 'cn': '今日小单净流入-净额', + 'size': 100}, + 'fund_rate_small': {'type': FLOAT, 'cn': '今日小单净流入-净占比', + 'size': 70}, + 'stock_name': {'type': NVARCHAR(20), 'cn': '今日主力净流入最大股', + 'size': 70}}}, + {'name': 'stock_individual_fund_flow_rank', 'cn': '5日', + 'columns': {'name': {'type': NVARCHAR(20), 'cn': '名称', 'size': 70}, + 'change_rate_5': {'type': FLOAT, 'cn': '5日涨跌幅', 'size': 70}, + 'fund_amount_5': {'type': BIGINT, 'cn': '5日主力净流入-净额', 'size': 100}, + 'fund_rate_5': {'type': FLOAT, 'cn': '5日主力净流入-净占比', 'size': 70}, + 'fund_amount_super_5': {'type': BIGINT, 'cn': '5日超大单净流入-净额', + 'size': 100}, + 'fund_rate_super_5': {'type': FLOAT, 'cn': '5日超大单净流入-净占比', + 'size': 70}, + 'fund_amount_large_5': {'type': BIGINT, 'cn': '5日大单净流入-净额', + 'size': 100}, + 'fund_rate_large_5': {'type': FLOAT, 'cn': '5日大单净流入-净占比', + 'size': 70}, + 'fund_amount_medium_5': {'type': BIGINT, 'cn': '5日中单净流入-净额', + 'size': 100}, + 'fund_rate_medium_5': {'type': FLOAT, 'cn': '5日中单净流入-净占比', + 'size': 70}, + 'fund_amount_small_5': {'type': BIGINT, 'cn': '5日小单净流入-净额', + 'size': 100}, + 'fund_rate_small_5': {'type': FLOAT, 'cn': '5日小单净流入-净占比', + 'size': 70}, + 'stock_name_5': {'type': NVARCHAR(20), 'cn': '5日主力净流入最大股', + 'size': 70}}}, + {'name': 'stock_individual_fund_flow_rank', 'cn': '10日', + 'columns': {'name': {'type': NVARCHAR(20), 'cn': '名称', 'size': 70}, + 'change_rate_10': {'type': FLOAT, 'cn': '10日涨跌幅', 'size': 70}, + 'fund_amount_10': {'type': BIGINT, 'cn': '10日主力净流入-净额', 'size': 100}, + 'fund_rate_10': {'type': FLOAT, 'cn': '10日主力净流入-净占比', 'size': 70}, + 'fund_amount_super_10': {'type': BIGINT, 'cn': '10日超大单净流入-净额', + 'size': 100}, + 'fund_rate_super_10': {'type': FLOAT, 'cn': '10日超大单净流入-净占比', + 'size': 70}, + 'fund_amount_large_10': {'type': BIGINT, 'cn': '10日大单净流入-净额', + 'size': 100}, + 'fund_rate_large_10': {'type': FLOAT, 'cn': '10日大单净流入-净占比', + 'size': 70}, + 'fund_amount_medium_10': {'type': BIGINT, 'cn': '10日中单净流入-净额', + 'size': 100}, + 'fund_rate_medium_10': {'type': FLOAT, 'cn': '10日中单净流入-净占比', + 'size': 70}, + 'fund_amount_small_10': {'type': BIGINT, 'cn': '10日小单净流入-净额', + 'size': 100}, + 'fund_rate_small_10': {'type': FLOAT, 'cn': '10日小单净流入-净占比', + 'size': 70}, + 'stock_name_10': {'type': NVARCHAR(20), 'cn': '10日主力净流入最大股', + 'size': 70}}})) TABLE_CN_STOCK_FUND_FLOW_INDUSTRY = {'name': 'cn_stock_fund_flow_industry', 'cn': '行业资金流向', 'columns': {'date': {'type': DATE, 'cn': '日期', 'size': 0}}} @@ -366,31 +393,77 @@ 'columns': _tmp_columns} TABLE_CN_STOCK_STRATEGIES = [ - {'name': 'cn_stock_strategy_enter', 'cn': '放量上涨', 'size': 70, 'func': enter.check_volume, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_keep_increasing', 'cn': '均线多头', 'size': 70, 'func': keep_increasing.check, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_parking_apron', 'cn': '停机坪', 'size': 70, 'func': parking_apron.check, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_backtrace_ma250', 'cn': '回踩年线', 'size': 70, 'func': backtrace_ma250.check, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_breakthrough_platform', 'cn': '突破平台', 'size': 70, - 'func': breakthrough_platform.check, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_low_backtrace_increase', 'cn': '无大幅回撤', 'size': 70, - 'func': low_backtrace_increase.check, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_turtle_trade', 'cn': '海龟交易法则', 'size': 70, 'func': turtle_trade.check_enter, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_high_tight_flag', 'cn': '高而窄的旗形', 'size': 70, - 'func': high_tight_flag.check_high_tight, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_climax_limitdown', 'cn': '放量跌停', 'size': 70, 'func': climax_limitdown.check, - 'columns': _tmp_columns}, - {'name': 'cn_stock_strategy_low_atr', 'cn': '低ATR成长', 'size': 70, 'func': low_atr.check_low_increase, + # {'name': 'cn_stock_strategy_enter', 'cn': '放量上涨', 'size': 70, 'func': enter.check_volume, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_keep_increasing', 'cn': '均线多头', 'size': 70, 'func': keep_increasing.check, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_parking_apron', 'cn': '停机坪', 'size': 70, 'func': parking_apron.check, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_backtrace_ma250', 'cn': '回踩年线', 'size': 70, 'func': backtrace_ma250.check, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_breakthrough_platform', 'cn': '突破平台', 'size': 70, + # 'func': breakthrough_platform.check, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_low_backtrace_increase', 'cn': '无大幅回撤', 'size': 70, + # 'func': low_backtrace_increase.check, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_turtle_trade', 'cn': '海龟交易法则', 'size': 70, 'func': turtle_trade.check_enter, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_high_tight_flag', 'cn': '高而窄的旗形', 'size': 70, + # 'func': high_tight_flag.check_high_tight, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_climax_limitdown', 'cn': '放量跌停', 'size': 70, 'func': climax_limitdown.check, + # 'columns': _tmp_columns}, + # {'name': 'cn_stock_strategy_low_atr', 'cn': '低ATR成长', 'size': 70, 'func': low_atr.check_low_increase, + # 'columns': _tmp_columns}, + {'name': 'volume_break_strategy', 'cn': '回调选股', 'size': 60, 'func': volume_break.check, 'columns': _tmp_columns} ] +#持仓卖出策略 +TABLE_CN_STOCK_POSITION_SELL = [ + {'name': 'common_sell_strategy', 'cn': '通用卖出策略', 'size': 30, 'func': common_sell_check.check, + 'columns': TABLE_CN_STOCK_FOREIGN_KEY['columns'].copy()} +] + +#持仓买入策略 +TABLE_CN_STOCK_BUY_DATA = {'name': 'cn_stock_buy_data', 'cn': '持仓买入出策略', + 'columns': {'date': {'type': DATE, 'cn': '日期', 'size': 0}, + 'code': {'type': NVARCHAR(6), 'cn': '代码', 'size': 60}, + 'name': {'type': NVARCHAR(20), 'cn': '名称', 'size': 70}, + 'open': {'type': FLOAT, 'cn': '开盘'}, + 'close': {'type': FLOAT, 'cn': '收盘'}, + 'high': {'type': FLOAT, 'cn': '最高'}, + 'low': {'type': FLOAT, 'cn': '最低'}, + 'volume': {'type': FLOAT, 'cn': '成交量'}, + 'amount': {'type': FLOAT, 'cn': '成交额'}, + 'amplitude': {'type': FLOAT, 'cn': '振幅'}, + 'quote_change': {'type': FLOAT, 'cn': '涨跌幅'}, + 'ups_downs': {'type': FLOAT, 'cn': '涨跌额'}, + 'turnover': {'type': FLOAT, 'cn': '换手率'}, + 'p_change': {'type': FLOAT, 'cn': '换手率'}, + } + } + +#持仓卖出策略 +TABLE_CN_STOCK_SELL_DATA = {'name': 'cn_stock_sell_data', 'cn': '持仓卖出出策略', + 'columns': {'date': {'type': DATE, 'cn': '日期', 'size': 0}, + 'code': {'type': NVARCHAR(6), 'cn': '代码', 'size': 60}, + 'name': {'type': NVARCHAR(20), 'cn': '名称', 'size': 70}, + 'open': {'type': FLOAT, 'cn': '开盘'}, + 'close': {'type': FLOAT, 'cn': '收盘'}, + 'high': {'type': FLOAT, 'cn': '最高'}, + 'low': {'type': FLOAT, 'cn': '最低'}, + 'volume': {'type': FLOAT, 'cn': '成交量'}, + 'amount': {'type': FLOAT, 'cn': '成交额'}, + 'amplitude': {'type': FLOAT, 'cn': '振幅'}, + 'quote_change': {'type': FLOAT, 'cn': '涨跌幅'}, + 'ups_downs': {'type': FLOAT, 'cn': '涨跌额'}, + 'turnover': {'type': FLOAT, 'cn': '换手率'}, + 'p_change': {'type': FLOAT, 'cn': '换手率'}, + } + } + STOCK_KLINE_PATTERN_DATA = {'name': 'cn_stock_pattern_recognitions', 'cn': 'K线形态', 'columns': { 'tow_crows': {'type': SmallInteger, 'cn': '两只乌鸦', 'size': 70, 'func': tl.CDL2CROWS}, @@ -964,6 +1037,42 @@ 'LOAN_REPAY_VOL': {'type': FLOAT, 'cn': '融券还量'}, 'LOAN_BALANCE': {'type': FLOAT, 'cn': '融券余额'}}} +TABLE_CN_STOCK_POSITION = {'name': 'cn_stock_position', 'cn': '股票持仓', + 'columns': { + 'date': {'type': DATE, 'cn': '日期'}, + 'code': {'type': NVARCHAR(20), 'cn': '代码'}, + 'name': {'type': NVARCHAR(50), 'cn': '名称'}, + 'market': {'type': NVARCHAR(20), 'cn': '市场'}, + 'frozen_quantity': {'type': BIGINT, 'cn': '冻结数量'}, + 'unit_quantity': {'type': BIGINT, 'cn': '单位数量'}, + 'available_balance': {'type': FLOAT, 'cn': '可用余额'}, + 'actual_quantity': {'type': BIGINT, 'cn': '实际数量'}, + 'market_price': {'type': FLOAT, 'cn': '市价'}, + 'market_value': {'type': FLOAT, 'cn': '市值'}, + 'market_code': {'type': NVARCHAR(20), 'cn': '市场代码'}, + 'opening_date': {'type': DATE, 'cn': '开仓日期'}, + 'cost_price': {'type': FLOAT, 'cn': '成本价'}, + 'cost_amount': {'type': FLOAT, 'cn': '成本金额'}, + 'break_even_price': {'type': FLOAT, 'cn': '保本价'}, + 'circulation_type': {'type': NVARCHAR(20), 'cn': '流通类型'}, + 'profit_loss': {'type': FLOAT, 'cn': '盈亏'}, + 'profit_loss_ratio': {'type': FLOAT, 'cn': '盈亏比例'}, + 'account': {'type': NVARCHAR(20), 'cn': '账户'}, + 'stock_balance': {'type': BIGINT, 'cn': '股票余额'}, + 'stock_category': {'type': NVARCHAR(20), 'cn': '股票类别'}, + }} + +TABLE_CN_STOCK_ACCOUNT = {'name': 'cn_stock_account', 'cn': '股票账户', + 'columns': { + 'date': {'type': DATE, 'cn': '日期'}, + 'market_cap': {'type': NVARCHAR(20), 'cn': '参考市值'}, + 'available_funds': {'type': NVARCHAR(50), 'cn': '可用资金'}, + 'cost_price': {'type': FLOAT, 'cn': '币种'}, + 'total_assets': {'type': BIGINT, 'cn': '总资产'}, + 'profit_loss': {'type': FLOAT, 'cn': '股份参考盈亏'}, + 'balance': {'type': FLOAT, 'cn': '资金余额'}, + 'account_id': {'type': BIGINT, 'cn': '资金帐号'}, + }} def get_field_cn(key, table): f = table.get('columns').get(key) diff --git a/instock/job/execute_daily_job.py b/instock/job/execute_daily_job.py index cf32a33a14..2b848ca756 100644 --- a/instock/job/execute_daily_job.py +++ b/instock/job/execute_daily_job.py @@ -27,6 +27,7 @@ import backtest_data_daily_job as bdj import klinepattern_data_daily_job as kdj import selection_data_daily_job as sddj +import strategy_position_daily_job as spdj __author__ = 'myh ' __date__ = '2023/3/10 ' @@ -58,6 +59,9 @@ def main(): # # # # 第7步创建股票闭盘后才有的数据 acdj.main() + # # # # 第8步持仓股票数据分析 + spdj.main() + logging.info("######## 完成任务, 使用时间: %s 秒 #######" % (time.time() - start)) diff --git a/instock/job/indicators_data_daily_job.py b/instock/job/indicators_data_daily_job.py index b84756ffc0..e4d191a3f9 100644 --- a/instock/job/indicators_data_daily_job.py +++ b/instock/job/indicators_data_daily_job.py @@ -1,7 +1,6 @@ #!/usr/local/bin/python3 # -*- coding: utf-8 -*- - - +import datetime import logging import concurrent.futures import pandas as pd diff --git a/instock/job/strategy_data_daily_job.py b/instock/job/strategy_data_daily_job.py index f2af5c8f07..bcf0b5e66e 100644 --- a/instock/job/strategy_data_daily_job.py +++ b/instock/job/strategy_data_daily_job.py @@ -1,8 +1,10 @@ #!/usr/local/bin/python3 # -*- coding: utf-8 -*- - +import datetime import logging import concurrent.futures +import traceback + import pandas as pd import os.path import sys @@ -49,9 +51,46 @@ def prepare(date, strategy): if date.strftime("%Y-%m-%d") != data.iloc[0]['date']: data['date'] = date_str mdb.insert_db_from_df(data, table_name, cols_type, False, "`date`,`code`") + # 增加买入数据 + + + # 取出stocks_data中key为results中值的数据 + buy_data = {k:stocks_data[k] for k in results} + df_list = [] # 用于存储 DataFrame 行 + # 遍历 new_data,检查日期匹配 + # 遍历 buy_data,检查日期匹配 + for key, value in buy_data.items(): + date_key, code, name = key # 解包键 + # 遍历值中的日期 + for i, date_value in enumerate(value['date']): + if date_value == date_key: # 如果日期匹配 + # 使用字典解包简化行数据的创建 + row = { + **{k: value[k][i] for k in + ['open', 'close', 'low', 'volume', 'amount', 'amplitude', 'high', 'quote_change', + 'ups_downs', 'turnover', 'p_change']}, + 'date': date_value, + 'code': code, + 'name': name + } + df_list.append(row) # 添加到列表中 + df = pd.DataFrame(df_list) + + # 确保数据不为空 + + # 删除老数据。 + buy_data_name_ = tbs.TABLE_CN_STOCK_BUY_DATA['name'] + if mdb.checkTableIsExist(buy_data_name_): + del_sql = f"DELETE FROM `{buy_data_name_}` where `date` = '{date_str}'" + mdb.executeSql(del_sql) + cols_type = None + else: + cols_type = tbs.get_field_types(tbs.TABLE_CN_STOCK_BUY_DATA['columns']) + # 插入新数据 + mdb.insert_db_from_df(df, buy_data_name_, cols_type, False, "`date`,`code`") except Exception as e: - logging.error(f"strategy_data_daily_job.prepare处理异常:{strategy}策略{e}") + logging.error(f"strategy_data_daily_job.prepare处理异常:{strategy}策略{traceback.format_exc()}") def run_check(strategy_fun, table_name, stocks, date, workers=40): diff --git a/instock/job/strategy_position_daily_job.py b/instock/job/strategy_position_daily_job.py new file mode 100644 index 0000000000..31d3b486fa --- /dev/null +++ b/instock/job/strategy_position_daily_job.py @@ -0,0 +1,153 @@ +#!/usr/local/bin/python3 +# -*- coding: utf-8 -*- +import concurrent.futures +import logging +import pandas as pd +import os.path +import sys +import datetime + +from instock.core.stockfetch import fetch_stock_top_entity_data + +cpath_current = os.path.dirname(os.path.dirname(__file__)) +cpath = os.path.abspath(os.path.join(cpath_current, os.pardir)) +sys.path.append(cpath) + +import instock.core.tablestructure as tbs +import instock.lib.database as mdb +import instock.lib.run_template as runt +from instock.core.singleton_stock import stock_hist_data + +__author__ = 'your_name' +__date__ = '2023/current_date' + + +def prepare(date, strategy): + try: + if date is None: + date = datetime.date.today().strftime("%Y-%m-%d") + # 获取当前持仓 + positionTableName = tbs.TABLE_CN_STOCK_POSITION['name'] + if not mdb.checkTableIsExist(positionTableName): + logging.info(f"Table {positionTableName} does not exist. Creating it.") + pdata = pd.DataFrame(data=[['2024-12-09', '603078', '江化微', 18.775,1000, 18.560,18560.0, 0]] , columns=list(tbs.TABLE_CN_STOCK_POSITION['columns'])) + cols_type = tbs.get_field_types(tbs.TABLE_CN_STOCK_POSITION['columns']) + mdb.insert_db_from_df(pdata, positionTableName, cols_type, False, "`date`,`code`") + del_sql = f"DELETE FROM `{positionTableName}`" + mdb.executeSql(del_sql) + return + # 获取当前持仓 + result = mdb.executeSqlFetch(f"SELECT * FROM `{positionTableName}`") + + + if result is None: + logging.info("No positions found.") + return + pd_result = pd.DataFrame(result, columns=list(tbs.TABLE_CN_STOCK_POSITION['columns'])) + # 获取持仓股票代码列表 + # 获取历史数据 + stocks_data = stock_hist_data(date=date).get_data() + if stocks_data is None: + logging.error("Failed to get historical data.") + return + + sell_list = [] + + strategy_name = strategy['name'] + strategy_func = strategy['func'] + # 计算成本 + cost_dict = {row['code']: row['cost_price'] for _, row in pd_result.iterrows()} + + results = run_check(strategy_func, strategy_name, stocks_data, date, cost_dict) + # 遍历结果 + if results is None: + return + + table_name = strategy['name'] + if mdb.checkTableIsExist(table_name): + # 删除当日旧数据 + del_sql = f"DELETE FROM `{table_name}` WHERE `date` = '{date}'" + mdb.executeSql(del_sql) + cols_type = None + else: + cols_type = tbs.get_field_types(tbs.TABLE_CN_STOCK_BACKTEST_DATA['columns']) + data = pd.DataFrame(results) + columns = tuple(tbs.TABLE_CN_STOCK_FOREIGN_KEY['columns']) + data.columns = columns + # 单例,时间段循环必须改时间 + date_str = date.strftime("%Y-%m-%d") + if date.strftime("%Y-%m-%d") != data.iloc[0]['date']: + data['date'] = date_str + # 插入新数据 + mdb.insert_db_from_df(data, table_name, cols_type, False, "`date`,`code`") + + logging.info(f"Added {len(sell_list)} stocks to sell list.") + # 取出stocks_data中key为results中值的数据 + buy_data = {k: stocks_data[k] for k in results} + df_list = [] # 用于存储 DataFrame 行 + # 遍历 new_data,检查日期匹配 + # 遍历 buy_data,检查日期匹配 + for key, value in buy_data.items(): + date_key, code, name = key # 解包键 + # 遍历值中的日期 + for i, date_value in enumerate(value['date']): + if date_value == date_key: # 如果日期匹配 + # 使用字典解包简化行数据的创建 + row = { + **{k: value[k][i] for k in + ['open', 'close', 'low', 'volume', 'amount', 'amplitude', 'high', 'quote_change', + 'ups_downs', 'turnover', 'p_change']}, + 'date': date_value, + 'code': code, + 'name': name + } + df_list.append(row) # 添加到列表中 + df = pd.DataFrame(df_list) + + # 确保数据不为空 + + # 删除老数据。 + sell_data_name_ = tbs.TABLE_CN_STOCK_SELL_DATA['name'] + if mdb.checkTableIsExist(sell_data_name_): + del_sql = f"DELETE FROM `{sell_data_name_}` where `date` = '{date_str}'" + mdb.executeSql(del_sql) + cols_type = None + else: + cols_type = tbs.get_field_types(tbs.TABLE_CN_STOCK_SELL_DATA['columns']) + + # 插入新数据 + mdb.insert_db_from_df(df, sell_data_name_, cols_type, False, "`date`,`code`") + except Exception as e: + logging.error(f"Error in check_position_and_sell: {e}") + +def run_check(strategy_fun, strategy_name, stocks, date, code_price = None, workers=40): + + data = [] + try: + with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor: + future_to_data = {executor.submit(strategy_fun, k, stocks[k], date=date, cost=code_price.get(k[1])): k for k in stocks} + for future in concurrent.futures.as_completed(future_to_data): + stock = future_to_data[future] + try: + if future.result(): + data.append(stock) + except Exception as e: + logging.error(f"strategy_data_daily_job.run_check处理异常:{stock[1]}代码{e}策略{strategy_name}") + except Exception as e: + logging.error(f"strategy_data_daily_job.run_check处理异常:策略{strategy_name}", e) + if not data: + return None + else: + return data + + +def main(): + # 使用方法传递。 + with concurrent.futures.ThreadPoolExecutor() as executor: + for strategy in tbs.TABLE_CN_STOCK_POSITION_SELL: + executor.submit(runt.run_with_args, prepare, strategy) + + + +if __name__ == '__main__': + main() diff --git a/instock/lib/run_template.py b/instock/lib/run_template.py index ba7b935d33..be5845c186 100644 --- a/instock/lib/run_template.py +++ b/instock/lib/run_template.py @@ -7,6 +7,8 @@ import concurrent.futures import sys import time +import traceback + import instock.lib.trade_time as trd __author__ = 'myh ' @@ -30,7 +32,7 @@ def run_with_args(run_fun, *args): time.sleep(2) run_date += datetime.timedelta(days=1) except Exception as e: - logging.error(f"run_template.run_with_args处理异常:{run_fun}{sys.argv}{e}") + logging.error(f"run_template.run_with_args处理异常:{run_fun}{sys.argv}\n{traceback.format_exc()}") elif len(sys.argv) == 2: # N个时间作业 python xxx.py 2023-03-01,2023-03-02 dates = sys.argv[1].split(',') @@ -43,7 +45,7 @@ def run_with_args(run_fun, *args): executor.submit(run_fun, run_date, *args) time.sleep(2) except Exception as e: - logging.error(f"run_template.run_with_args处理异常:{run_fun}{sys.argv}{e}") + logging.error(f"run_template.run_with_args处理异常:{run_fun}{sys.argv}\n{traceback.format_exc()}") else: # 当前时间作业 python xxx.py try: @@ -55,4 +57,4 @@ def run_with_args(run_fun, *args): else: run_fun(run_date_nph, *args) except Exception as e: - logging.error(f"run_template.run_with_args处理异常:{run_fun}{sys.argv}{e}") + logging.error(f"run_template.run_with_args处理异常:{run_fun}{sys.argv}\n{traceback.format_exc()}") diff --git a/instock/lib/trade_time.py b/instock/lib/trade_time.py index b3b971eb00..fd1d88e897 100644 --- a/instock/lib/trade_time.py +++ b/instock/lib/trade_time.py @@ -113,7 +113,7 @@ def is_open(now_time): def get_trade_hist_interval(date): tmp_year, tmp_month, tmp_day = date.split("-") date_end = datetime.datetime(int(tmp_year), int(tmp_month), int(tmp_day)) - date_start = (date_end + datetime.timedelta(days=-(365 * 3))).strftime("%Y%m%d") + date_start = (date_end + datetime.timedelta(days=-(365 * 1))).strftime("%Y%m%d") now_time = datetime.datetime.now() now_date = now_time.date() diff --git a/instock/trade/robot/engine/main_engine.py b/instock/trade/robot/engine/main_engine.py index 4f7da40eee..6d38598fcd 100644 --- a/instock/trade/robot/engine/main_engine.py +++ b/instock/trade/robot/engine/main_engine.py @@ -39,7 +39,7 @@ def __init__(self, broker=None, need_data=None, log_handler=DefaultLogHandler(), else: self.user = None self.log.info('选择了无交易模式') - + self.log.info('当前账户信息%s' % self.user.balance) self.event_engine = EventEngine() self.clock_engine = ClockEngine(self.event_engine, tzinfo) diff --git a/instock/trade/strategies/buy_strategy.py b/instock/trade/strategies/buy_strategy.py new file mode 100644 index 0000000000..94f64483e6 --- /dev/null +++ b/instock/trade/strategies/buy_strategy.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os.path +import datetime as dt +import random + +from dateutil import tz +from instock.trade.robot.infrastructure.default_handler import DefaultLogHandler +from instock.trade.robot.infrastructure.strategy_template import StrategyTemplate +import instock.lib.database as mdb +import pandas as pd +import instock.core.tablestructure as tbs +__author__ = 'myh ' +__date__ = '2023/4/10 ' + +# 更新持仓 +class Strategy(StrategyTemplate): + + def init(self): + # 注册盘后事件 + # after_trading_moment = dt.time(15, 0, 0, tzinfo=tz.tzlocal()) + # self.clock_engine.register_moment(self.name, after_trading_moment) + return + + def strategy(self): + # [{'参考市值': 21642.0, + # '可用资金': 28494.21, + # '币种': '0', + # '总资产': 50136.21, + # '股份参考盈亏': -90.21, + # '资金余额': 28494.21, + # '资金帐号': 'xxx'}] + self.user.refresh() + balance = pd.DataFrame([self.user.balance]) + if len(balance) < 1: + return + + # 测试 + # 查询昨日生产的可以买入股票 + yeasterDay = dt.datetime.now() - dt.timedelta(days=1) + datetime = yeasterDay.strftime('%Y-%m-%d') + prepare_buy = [] + fetch = mdb.executeSqlFetch( + f"SELECT * FROM `{tbs.TABLE_CN_STOCK_BUY_DATA['name']}` WHERE `date`='{datetime}'") + pd_result = pd.DataFrame(fetch, columns=list(tbs.TABLE_CN_STOCK_BUY_DATA['columns'])) + cash = balance['可用金额'] + if cash < 10000: + return + if not pd_result.empty: + # 随机选一个 + random_row = pd_result.sample(n=1) + price = random_row['close'].values[0] + amount = int((cash / 2 / price) // 100) * 100 + prepare_buy = [(random_row['code'].values[0], float(price), amount)] + + if not prepare_buy or len(prepare_buy) < 1: + return + + # --------写交易策略--------- + for buy in prepare_buy: + self.user.buy(buy[0], price=buy[1], amount=buy[2]) + + self.log.info('检查持仓') + self.log.info(self.user.balance) + self.log.info('\n') + + + def clock(self, event): + """在交易时间会定时推送 clock 事件""" + if event.data.clock_event in ('open','continue', 'close'): + self.strategy() + + def log_handler(self): + """自定义 log 记录方式""" + cpath_current = os.path.dirname(os.path.dirname(__file__)) + cpath = os.path.abspath(os.path.join(cpath_current, os.pardir)) + log_filepath = os.path.join(cpath, 'log', f'{self.name}.log') + return DefaultLogHandler(self.name, log_type='file', filepath=log_filepath) + + def shutdown(self): + """关闭进程前的调用""" + self.log.info("假装在关闭前保存了策略数据") + + + + + diff --git a/instock/trade/strategies/check_strategy.py b/instock/trade/strategies/check_strategy.py new file mode 100644 index 0000000000..41d64b7da3 --- /dev/null +++ b/instock/trade/strategies/check_strategy.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import os.path +import datetime as dt +import random +from typing import List, Tuple + +from dateutil import tz +from instock.trade.robot.infrastructure.default_handler import DefaultLogHandler +from instock.trade.robot.infrastructure.strategy_template import StrategyTemplate +import instock.lib.database as mdb +import pandas as pd +import instock.core.tablestructure as tbs + +class Strategy(StrategyTemplate): + + def init(self): + self.buy_orders = [] + self.check_interval = dt.timedelta(minutes=5) + self.last_check_time = dt.datetime.now() + + def buy_strategy(self): + self.user.refresh() + balance = pd.DataFrame([self.user.balance]) + if balance.empty: + return + + cash = balance['可用金额'].values[0] + if cash < 10000: + return + + prepare_buy = self.get_stocks_to_buy() + if not prepare_buy: + return + + for code, price, amount in prepare_buy: + order = self.user.buy(code, price=price, amount=amount) + self.buy_orders.append(order) + + self.log.info('检查持仓') + self.log.info(self.user.balance) + self.log.info('\n') + + def get_stocks_to_buy(self) -> List[Tuple[str, float, int]]: + yesterday = dt.datetime.now() - dt.timedelta(days=1) + date_str = yesterday.strftime('%Y-%m-%d') + + fetch = mdb.executeSqlFetch( + f"SELECT * FROM `{tbs.TABLE_CN_STOCK_BUY_DATA['name']}` WHERE `date`='{date_str}'") + pd_result = pd.DataFrame(fetch, columns=list(tbs.TABLE_CN_STOCK_BUY_DATA['columns'])) + + if pd_result.empty: + return [] + + random_row = pd_result.sample(n=1) + price = random_row['close'].values[0] + cash = self.user.balance['可用金额'] + amount = int((cash / 2 / price) // 100) * 100 + return [(random_row['code'].values[0], float(price), amount)] + + def check_buy_orders(self): + current_time = dt.datetime.now() + if current_time - self.last_check_time >= self.check_interval: + self.last_check_time = current_time + completed_orders = [] + for order in self.buy_orders: + if order.is_completed(): + completed_orders.append(order) + self.log.info(f"买入订单完成: {order}") + + for completed_order in completed_orders: + self.buy_orders.remove(completed_order) + + if completed_orders: + self.user.refresh() + self.log.info("更新后的持仓信息:") + self.log.info(self.user.position) + + def clock(self, event): + if event.data.clock_event in ('open', 'continue', 'close'): + self.buy_strategy() + self.check_buy_orders() + + def log_handler(self): + cpath_current = os.path.dirname(os.path.dirname(__file__)) + cpath = os.path.abspath(os.path.join(cpath_current, os.pardir)) + log_filepath = os.path.join(cpath, 'log', f'{self.name}.log') + return DefaultLogHandler(self.name, log_type='file', filepath=log_filepath) + + def shutdown(self): + self.log.info("关闭前保存策略数据") diff --git a/instock/trade/strategies/stagging.py b/instock/trade/strategies/stagging.py index 11a316be84..66d17af548 100644 --- a/instock/trade/strategies/stagging.py +++ b/instock/trade/strategies/stagging.py @@ -25,10 +25,6 @@ def init(self): moment = dt.time(10, 0, 0, tzinfo=tz.tzlocal()) self.clock_engine.register_moment(clock_type, moment) - # 注册时钟间隔事件, 不在交易阶段也会触发, clock_type == minute_interval - minute_interval = 1.5 - self.clock_engine.register_interval(minute_interval, trading=False) - def strategy(self): self.log.info('打新股') self.user.auto_ipo() diff --git a/instock/trade/strategies/stratey1.py b/instock/trade/strategies/stratey1.py index 79a88f4462..9f53681bb7 100644 --- a/instock/trade/strategies/stratey1.py +++ b/instock/trade/strategies/stratey1.py @@ -6,51 +6,116 @@ from dateutil import tz from instock.trade.robot.infrastructure.default_handler import DefaultLogHandler from instock.trade.robot.infrastructure.strategy_template import StrategyTemplate - +import instock.lib.database as mdb +import pandas as pd +import instock.core.tablestructure as tbs __author__ = 'myh ' __date__ = '2023/4/10 ' - +# 更新持仓 class Strategy(StrategyTemplate): - name = 'stratey1' def init(self): - # 通过下面的方式来获取时间戳 - # now_dt = self.clock_engine.now_dt - # now = self.clock_engine.now - # now = time.time() - - # 注册时钟事件 - clock_type = self.name - moment = dt.time(14, 54, 30, tzinfo=tz.tzlocal()) - self.clock_engine.register_moment(clock_type, moment) + # 注册盘后事件 + # after_trading_moment = dt.time(15, 0, 0, tzinfo=tz.tzlocal()) + # self.clock_engine.register_moment(self.name, after_trading_moment) # 注册时钟间隔事件, 不在交易阶段也会触发, clock_type == minute_interval - minute_interval = 1.5 - self.clock_engine.register_interval(minute_interval, trading=False) + minute_interval = 3 + self.clock_engine.register_interval(minute_interval, trading=True) + return + + def save_position_to_db(self): + positions = self.user.position + if not positions: + self.log.info("没有持仓信息") + return + # Convert positions data to English and map columns + positions_en = [ + { + 'market': 'Shanghai A-share' if p['交易市场'] == '上海A股' else '深圳 A股', + 'frozen_quantity': p['冻结数量'], + 'unit_quantity': p['单位数量'], + 'available_balance': p['可用余额'], + 'actual_quantity': p['实际数量'], + 'market_price': p['市价'], + 'market_value': p['市值'], + 'market_code': p['市场代码'], + 'opening_date': p['开仓日期'], + 'cost_price': p['成本价'], + 'cost_amount': p['成本金额'], + 'break_even_price': p['摊薄保本价'], + 'circulation_type': 'Circulating' if p['流通类型'] == '流通' else p['流通类型'], + 'profit_loss': p['盈亏'], + 'profit_loss_ratio': p['盈亏比例(%)'], + 'account': p['股东帐户'].strip('="'), + 'stock_balance': p['股票余额'], + 'stock_category': p['股票类别'], + 'code': p['证券代码'].strip('="'), + 'name': p['证券名称'] + } for p in positions + ] + + # Convert to DataFrame + df = pd.DataFrame(positions_en) + + df = pd.DataFrame(positions) + data = df.rename(columns={ + '证券代码': 'code', + '证券名称': 'name', + '成本价': 'cost_price', + '可用余额': 'available_shares', + '市价': 'market_price', + '市值': 'market_value', + '买入冻结' : 'freeze', + }) + + table_name = tbs.TABLE_CN_STOCK_POSITION['name'] + # 删除老数据。 + if mdb.checkTableIsExist(table_name): + del_sql = f"DELETE FROM `{table_name}`" + mdb.executeSql(del_sql) + cols_type = None + else: + cols_type = tbs.get_field_types(tbs.TABLE_CN_STOCK_POSITION['columns']) + + df['date'] = dt.date.today() + mdb.insert_db_from_df(data, table_name, cols_type, False, "`date`,`code`") + self.log.info(f"持仓信息{data}已保存到数据库表 {table_name}") - def strategy(self): - buy_stock = [('000001', 1, 100)] - sell_stock = [('000001', 100, 100)] - # --------写交易策略--------- + def save_account_to_db(self): + balance = self.user.balance + if not balance: + self.log.info("没有用户信息") + return + df = pd.DataFrame([balance]) + data = df.rename(columns={ + '参考市值': 'market_cap', + '可用资金': 'available_funds', + '币种': 'cost_price', + '总资产': 'total_assets', + '股份参考盈亏': 'profit_loss', + '资金余额': 'balance', + '资金帐号': 'account_id', + }) - # --------写交易策略--------- - for buy in buy_stock: - self.user.buy(buy[0], price=buy[1], amount=buy[3]) - for sell in sell_stock: - self.user.sell(sell[0], price=sell[1], amount=sell[3]) + table_name = tbs.TABLE_CN_STOCK_ACCOUNT['name'] + # 删除老数据。 + if mdb.checkTableIsExist(table_name): + del_sql = f"DELETE FROM `{table_name}`" + mdb.executeSql(del_sql) + cols_type = None + else: + cols_type = tbs.get_field_types(tbs.TABLE_CN_STOCK_ACCOUNT['columns']) - self.log.info('检查持仓') - self.log.info(self.user.balance) - self.log.info('\n') + df['date'] = dt.date.today() + mdb.insert_db_from_df(data, table_name, cols_type, False, "`date`,`account_id`") + self.log.info(f"账户信息{data}已保存到数据库表 {table_name}") def clock(self, event): - """在交易时间会定时推送 clock 事件 - :param event: event.data.clock_event 为 [0.5, 1, 3, 5, 15, 30, 60] 单位为分钟, ['open', 'close'] 为开市、收市 - event.data.trading_state bool 是否处于交易时间 - """ - if event.data.clock_event == self.name: - self.strategy() + """在交易时间会定时推送 clock 事件""" + self.save_account_to_db() + self.save_position_to_db() def log_handler(self): """自定义 log 记录方式""" @@ -60,8 +125,5 @@ def log_handler(self): return DefaultLogHandler(self.name, log_type='file', filepath=log_filepath) def shutdown(self): - """ - 关闭进程前的调用 - :return: - """ + """关闭进程前的调用""" self.log.info("假装在关闭前保存了策略数据") diff --git a/instock/trade/trade_service.py b/instock/trade/trade_service.py index 4ec56fbe19..9665a1323d 100644 --- a/instock/trade/trade_service.py +++ b/instock/trade/trade_service.py @@ -17,7 +17,8 @@ def main(): - broker = 'gf_client' + broker = 'universal_client' + log_handler = DefaultLogHandler(name='交易服务', log_type='file', filepath=log_filepath) m = MainEngine(broker, need_data, log_handler) m.is_watch_strategy = True # 策略文件出现改动时,自动重载,不建议在生产环境下使用 diff --git a/instock/trade/usage.md b/instock/trade/usage.md index 559b70b1e8..8c36a2aba4 100644 --- a/instock/trade/usage.md +++ b/instock/trade/usage.md @@ -58,7 +58,7 @@ user = easytrader.use('xq') 请手动打开并登录客户端后,运用connect函数连接客户端。 ```python -user.connect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' +ect(r'客户端xiadan.exe路径') # 类似 r'C:\htzqzyb2\xiadan.exe' ``` ### (二)通用同花顺客户端 diff --git a/requirements.txt b/requirements.txt index de86e06ad2..c2dba3fc90 100644 --- a/requirements.txt +++ b/requirements.txt @@ -14,4 +14,7 @@ tqdm==4.67.1 easytrader==0.23.0 beautifulsoup4==4.12.3 pycryptodome==3.21.0 -python_dateutil==2.9.0.post0 \ No newline at end of file +python_dateutil==2.9.0.post0 +backtrader~=1.9.78.123 +matplotlib~=3.9.3 +python-dateutil~=2.9.0.post0 \ No newline at end of file diff --git a/tests/buy_test.py b/tests/buy_test.py new file mode 100644 index 0000000000..b9b454ab9b --- /dev/null +++ b/tests/buy_test.py @@ -0,0 +1,33 @@ +import os.path +import datetime as dt +import random +from typing import List, Tuple + +from dateutil import tz +from instock.trade.robot.infrastructure.default_handler import DefaultLogHandler +from instock.trade.robot.infrastructure.strategy_template import StrategyTemplate +import instock.lib.database as mdb +import pandas as pd +import instock.core.tablestructure as tbs + + +def test_buy(): + yesterday = dt.datetime.now() - dt.timedelta(days=1) + date_str = yesterday.strftime('%Y-%m-%d') + + fetch = mdb.executeSqlFetch( + f"SELECT * FROM `{tbs.TABLE_CN_STOCK_BUY_DATA['name']}` WHERE `date`='{date_str}'") + pd_result = pd.DataFrame(fetch, columns=list(tbs.TABLE_CN_STOCK_BUY_DATA['columns'])) + + if pd_result.empty: + return [] + cash = 100000 + random_row = pd_result.sample(n=1) + price = random_row['close'].values[0] + amount = int((cash / 2 / price) // 100) * 100 + return [(random_row['code'].values[0], float(price), amount)] + +if __name__ == '__main__': + buys = test_buy() + for buy in buys: + print(f'buy: {buy[0]} price:{buy[1]} amount: {buy[2]}') diff --git a/tests/table_in_test.py b/tests/table_in_test.py new file mode 100644 index 0000000000..8215c95779 --- /dev/null +++ b/tests/table_in_test.py @@ -0,0 +1,54 @@ +import os.path +import datetime as dt +from dateutil import tz +from instock.trade.robot.infrastructure.default_handler import DefaultLogHandler +from instock.trade.robot.infrastructure.strategy_template import StrategyTemplate +import instock.lib.database as mdb +import pandas as pd +import instock.core.tablestructure as tbs +__author__ = 'myh ' +__date__ = '2023/4/10 ' + +# 更新持仓 + +if __name__ == '__main__': + positions = [{'Unnamed: 24': '', '交易市场': '上海A股', '冻结数量': 0, '单位数量': 1, '历史成交': '', '可用余额': 1000, '实际数量': 1000, '市价': 18.9, '市值': 18900.0, '市场代码': 2, '开仓日期': 20241209, '当日买入成交数量': 0, '当日卖出成交数量': 0, '成本价': 18.775, '成本金额': 18775.19, '摊薄保本价': 18.79, '流通类型': '流通', '盈亏': 110.17, '盈亏比例(%)': 0.587, '股东帐户': '="A811175096"', '股票余额': 1000, '股票类别': 'A0', '证券代码': '="603078"', '证券名称': '江化微', '资讯': ''}, {'Unnamed: 24': '', '交易市场': '深圳A股', '冻结数量': 0, '单位数量': 1, '历史成交': '', '可用余额': 500, '实际数量': 500, '市价': 41.07, '市值': 20535.0, '市场代码': 1, '开仓日期': 20241210, '当日买入成交数量': 0, '当日卖出成交数量': 0, '成本价': 41.01, '成本金额': 20505.0, '摊薄保本价': 41.04, '流通类型': '流通', '盈亏': 14.73, '盈亏比例(%)': 0.072, '股东帐户': '="0241355428"', '股票余额': 500, '股票类别': 'A0', '证券代码': '="001229"', '证券名称': '魅视科技', '资讯': ''}, {'Unnamed: 24': '', '交易市场': '深圳A股', '冻结数量': 0, '单位数量': 1, '历史成交': '', '可用余额': 400, '实际数量': 400, '市价': 60.28, '市值': 24112.0, '市场代码': 1, '开仓日期': 20241120, '当日买入成交数量': 0, '当日卖出成交数量': 0, '成本价': 58.665, '成本金额': 23466.0, '摊薄保本价': 58.708, '流通类型': '流通', '盈亏': 628.26, '盈亏比例(%)': 2.677, '股东帐户': '="0241355428"', '股票余额': 400, '股票类别': 'CR', '证券代码': '="300917"', '证券名称': '特发服务', '资讯': ''}] + + positions_en = [ + { + 'market': '上海A股' if p['交易市场'] == '上海A股' else '深圳A股', + 'frozen_quantity': p['冻结数量'], + 'unit_quantity': p['单位数量'], + 'available_balance': p['可用余额'], + 'actual_quantity': p['实际数量'], + 'market_price': p['市价'], + 'market_value': p['市值'], + 'market_code': p['市场代码'], + 'opening_date': p['开仓日期'], + 'cost_price': p['成本价'], + 'cost_amount': p['成本金额'], + 'break_even_price': p['摊薄保本价'], + 'circulation_type': 'Circulating' if p['流通类型'] == '流通' else p['流通类型'], + 'profit_loss': p['盈亏'], + 'profit_loss_ratio': p['盈亏比例(%)'], + 'account': p['股东帐户'].strip('="'), + 'stock_balance': p['股票余额'], + 'stock_category': p['股票类别'], + 'code': p['证券代码'].strip('="'), + 'name': p['证券名称'] + } for p in positions + ] + + # Convert to DataFrame + df = pd.DataFrame(positions_en) + table_name = tbs.TABLE_CN_STOCK_POSITION['name'] + # 删除老数据。 + if mdb.checkTableIsExist(table_name): + del_sql = f"DELETE FROM `{table_name}`" + mdb.executeSql(del_sql) + cols_type = None + else: + cols_type = tbs.get_field_types(tbs.TABLE_CN_STOCK_POSITION['columns']) + + df['date'] = dt.date.today() + mdb.insert_db_from_df(df, table_name, cols_type, False, "`date`,`code`")