# 使用tushare库获取股票数据
import datetime
import os
import time
from datetime import date
import logging  # 日志输出

from sqlalchemy import create_engine, DATE, BOOLEAN, Float, QueuePool

import numpy as np
import pandas as pd
# import talib  # 下载不成功
import tushare as ts  # 股票数据下载
import yangke.common.config as config
from yangke.common.config import logger
import traceback
from yangke.dataset.YKSqlalchemy import SqlOperator, YkColumn, 修改时间记录表
from yangke.dataset.YKSqlalchemy import Base, Column, String, DATETIME
import baostock as bs
import akshare as ak


class StockData:
    """
    初始化tushare，填写token
    """

    def __init__(self, project=None):
        self.api = ts.pro_api('67e4202fb31f321f28929afdbe11780241cd35eb662f6f1947f7e800')
        self.code = None

    def __repr__(self):
        info = f"股票数据类"
        return info

    def __getstate__(self):
        """
        自定义的类型，实现该方法后，则可以使用pickle进行序列化与反序列化
        """
        return {"data_folder": self.data_folder, "code": self.code, "day": self.day, "storage": self.storage}

    def setCode(self, code: str):
        self.code = code

    def setDay(self, day: datetime.date):
        self.day = day

    def get_name(self, code):
        data = self.api.stock_basic(exchange='', list_status='L', fields='ts_code,symbol,name,area,industry,list_date')

        data_s = data[data['symbol'] == code]  # 选出‘symbol=600006'的那一行，成为1行n列的DataFrame数据

        name = data_s.iloc[0, 2]  # 选出第0行第2列，因为数据经过挑选只有一行，所以行号永远为0
        return name

    def get_exchange_of_symbol(self, symbol):
        """
        根据股票的symbol获取ts_code
        """
        res = self.sql.select_in_table(table_name=self.股票信息表,
                                       condition_dict={"symbol": symbol},
                                       result_col="exchange")
        if res is None:
            self.get_all_stock_basic_info()
            res = self.sql.select_in_table(table_name=self.股票信息表,
                                           condition_dict={"symbol": symbol},
                                           result_col="exchange")
            if res is None:
                logger.warning(f"不存在代码为{symbol}的股票，请检查！")
        return res

    def get_all_stock_basic_info(self) -> pd.DataFrame:
        """
        获得上海和深圳证券交易所目前上市的所有股票代码

        :return: 股票代码的列表
        """

        def get_exchange(symbol):
            symbol = str(symbol)
            if symbol.startswith('6'):
                return 'SH'
            elif symbol.startswith('0') or symbol.startswith('2') or symbol.startswith('3'):
                return "SZ"
            elif symbol.startswith('8') or symbol.startswith('4'):
                return 'BJ'

        def get_market(symbol):
            """
            获取股票所属的板块
            """
            symbol = str(symbol)
            if symbol.startswith('60'):
                return '主板'
            elif symbol.startswith('00'):
                return "主板"
            elif symbol.startswith('30'):
                return '创业板'
            elif symbol.startswith('68'):
                return '科创板'
            elif symbol.startswith('82'):
                return '优先股'
            elif symbol.startswith('83'):
                return '普通股'
            elif symbol.startswith('87'):
                return '普通股'
            elif symbol.startswith('4'):
                return '北交所'

        try:
            # tushare现在(2023年6月18日测试)对stock_basic接口限制太死，每分钟只能调用1次，每天5次，这里使用baostock请求数据
            inner_data = self.api.stock_basic(exchange='', list_status='L',
                                              fields=['ts_code', 'symbol', 'name', 'market'])
            # 数据示例
            # ts_code   symbol name    market exchange
            # 000001.SZ 000001 平安银行  主板    SZ
            inner_data['exchange'] = inner_data['ts_code'].str[7:9]
            inner_data = inner_data.drop('ts_code', axis=1)
        except:
            # akshare获取股票信息
            inner_data = ak.stock_zh_a_spot_em()  # 东方财富数据
            # stock_zh_a_spot_df = ak.stock_zh_a_spot()  # 新浪数据，速度很慢，貌似是爬虫实时爬取的数据，且数据不如东财全面
            inner_data = inner_data[['代码', '名称']]
            inner_data.rename({"代码": 'symbol', "名称": 'name'}, axis=1, inplace=True)
            inner_data['name'] = inner_data['name'].str.replace(" ", "")
            inner_data['exchange'] = inner_data['symbol'].apply(func=lambda x: get_exchange(x))
            inner_data['market'] = inner_data['symbol'].apply(func=lambda x: get_market(x))

        inner_data = inner_data.sort_values(by='symbol', ascending=True)
        # 保存到Sqlite/Mysql数据库，该方法可能比较耗时，尤其是使用网络数据库的时候，可以判断后只插入数据库中没有的记录
        if not self.sql.has_table(self.股票信息表):
            logger.debug(f"创建股票信息总表")
            self.sql.create_table(table_name=self.股票信息表, columns=[
                YkColumn('symbol', String(10), nullable=False, primary_key=True),
                YkColumn('name', String(20)),
                YkColumn('exchange', String(10)),
                YkColumn('market', String(10)),
            ])
        logger.debug(f"更新股票信息总表数据")
        self.sql.insert_dataframe(table_name=self.股票信息表, df=inner_data, if_exists="append", index=False)
        self.sql.update_update_time_of_table(self.股票信息表)

    @config.loggingTitleCall(title="查看触底反弹股")
    def get_bottom_out(self, in_recent_days: int = 10):  # 触底反弹股票，根据近30天内的数据进行判断
        """
        获得触底反弹股票

        :param in_recent_days:股票数据条数，即判断的时间周期
        :returns symbols 触底反弹股
                 symbols2 最后一天在增长的股票
        """
        df = self.download_daily(date.today())
        inner_symbols = []
        symbols_first = []
        # 首先获得满足如下3个条件的股票列表
        # 1. 当天开盘价小于收盘价，表明最后一天在增长
        # 2. 当天开盘价小于昨天收盘价，表明昨天到今天在下降，满足先降后生的特点，即触底反弹的特点
        # 3. 当天最高价等于收盘价，表明最后阶段没有出现下降的趋势
        for inner_index, inner_row in df.iterrows():
            if inner_row["open"] < inner_row["close"] and inner_row["open"] < inner_row["pre_close"] \
                    and inner_row["high"] == inner_row["close"]:
                inner_symbols.append(inner_row['ts_code'])
                symbols_first.append(inner_row['ts_code'])
        logger.debug("初筛触底反弹股列表:{}".format(symbols_first))
        logger.debug("进一步检查初筛列表...")
        # 进一步删选选出来的股票，剔除其中最低价没有出现在最后一天的股票，只有最后一天是最低价，才能算是触底反弹
        for inner_symbol in symbols_first:
            df = self.download(inner_symbol, append=True)
            df = df.iloc[0 - in_recent_days:]  # 截取最后n天的数据
            lowest_price = df['low'].min()  # 获得30天内的最低价
            lowest_date = np.array(df['date'][df['low'] == lowest_price])[0]  # 获得30天内最低价对应的日期，这里返回的lowest_date为str类型

            if lowest_date == df.iloc[-1]['date']:
                pass
            else:
                inner_symbols.remove(inner_symbol)
        logger.debug("触底反弹股列表:{}".format(inner_symbols))
        return inner_symbols, symbols_first

    def download_all_stocks(self, codes: list = None, num_works=64, exclude: list = None) -> list:
        """
        下载所有股票数据，如果传入的是列表，则只下载列表中股票代码对应的股票数据
        如果使用以下方法下载所有股票数据，则耗时巨大，实用性不强，本方法主要结合多线程进行下载。

        for stock_code in codes:
            self.download(stock_code)

        需要注意的是多线程并不会增加本地的处理速度，只是可以http并发请求数量，在高耗时的http连接上可以提高下载速度。

        测试该方法下载沪深所有股票数据需要7分钟左右。执行耗时与网速即硬件相关。

        如果本地数据是最新，则耗时大概10秒钟左右。

        :param codes: 股票代码列表
        :param num_works: 线程数
        :param exclude: 排除的股票代码列表
        :return: 存在有效数据的股票代码列表，由于有些股票连续停牌超过两年会导致没有数据，返回的股票列表会剔除这种股票
        """
        if codes is None:
            codes_df = self.get_all_stock_basic_info()
            codes_available = codes_df['symbol'].tolist()
            logger.debug("下载沪深所有股票数据，预计耗时10分钟左右（本地所有股票数据需要更新时）...")
        else:
            codes_available = codes
        if exclude is not None:
            codes_available = [code for code in codes_available if code not in exclude]

        # 如果当前时间是交易日的8点-下午3点之间，则拒绝下载所有股票数据
        if not self.is_holiday(day_datetime=datetime.datetime.today()):
            if 8 <= datetime.datetime.now().hour < 15:
                logger.debug(f"开市日早8点至下午15点之间，不能使用该方法")
                return codes_available

        update_time = self.sql.get_update_time_of_table('daily_all')
        if self.need_update(update_time):  # 判断需要更新的数据的数量
            # 如果更新时间是昨天下午4点以后，且当前时间超过了下午3点，则只用下载所有股票一天的数据
            last_working_day = self.get_last_n_working_day(1)
            if update_time > 10:
                ...
        else:
            return codes_available

        n = len(codes_available)
        for i, stock_code in enumerate(codes_available):
            logger.debug(f"-----------------------正在下载第{i + 1}/{n}个股票的数据---------------------")
            self.download(stock_code)
            logger.debug(f"-----------------------第{i + 1}/{n}个股票的数据下载完毕---------------------")
        self.sql.update_update_time_of_table('daily_all')  # 记录最后一次更新所有股票数据的时间
        return codes_available

    def download(self, code: str, append=True) -> pd.DataFrame:
        """
        下载或更新code对应的股票在近一年的数据，只能在交易时间之外进行。

        下载的股票数据列及其意义如下：

        date open high close low volume price_change p_change ma5 ...

        日期 开盘 最高 收盘 最低价 成交量 收盘价格变化 价格涨幅 滑动平均5 ...

        更新：下载股票数据时不会替换当前已经存在的数据，只是追加新数据

        :param code: 股票代码，可以带证交所后缀的股票代码：如601002.SH，也可以只是股票编号：601002
        :param append: 如果为True，则追加新数据到已有数据，而不是下载后替换已有数据
        :return:
        """
        code = str(code)
        logger.debug("准备下载股票数据，股票代码：" + code)
        if '.' in code:  # 如果传入的股票代码是类似于 000544.SZ 格式的，则去掉后面的 .SZ,只保留数字格式代码
            ts_code = code
            symbol = code[0:6]
        else:
            exchange = self.get_exchange_of_symbol(code)
            ts_code = f"{code}.{exchange}"
            symbol = code

        if self.storage == "Sqlite数据库" or self.storage == "Mysql数据库":
            update_time = self.sql.get_update_time_of_table(f"daily{symbol}")
            if not self.need_update(update_time):
                logger.debug(f"本地数据已是最新，使用本地股票数据（读取自{self.storage}）...")
                df = pd.read_sql(f"daily{symbol}", con=self.sql.connect)  # MySQL中con不能使用self.engine和self.sql.session
                return df

        # 如果需要更新，则开始更新
        logger.debug("本地数据需要更新，开始下载股票数据（%s）..." % code)
        success = False  # 标记是否成功下载
        try:
            df = self.api.query('daily', ts_code=ts_code)
            if df is None:  # 有时候下载失败，会导致df为None，如果出现这种情况，重复10次
                logger.debug(f"下载股票数据{symbol}失败，重试...")
                for i in range(10):
                    df: pd.DataFrame = self.api.query('daily', ts_code=ts_code)
                    if df is not None:
                        success = True
                        logger.debug(f"下载股票数据{symbol}失败，重试...，成功！")
                        break
            else:
                success = True
        except:  # 当网络故障的时候，ts.get_hist_data(code)会报错
            logger.error(f"获取股票{symbol}失败，请检查网络")
            return None
        if not success:
            logger.warning(f"经过10次尝试后，下载股票数据{code}一直失败，没有数据"
                           "保存到本地！停牌超过两年或预上市股票也会产生该警告！")
            return None
        df.sort_index(inplace=True)  # 反序排列数据，这样让更新的数据位于文件最下方
        df.reset_index(inplace=True)

        # vol: 成交量(手), amount: 成交额(千元)
        df = df[['trade_date', 'open', 'high', 'low', 'close', 'vol', 'amount']].copy()
        df['trade_date'] = pd.to_datetime(df["trade_date"])  # df中的数据转换成时间列
        if append:  # 如果需要追加数据
            if self.storage == "Sqlite数据库" or self.storage == "Mysql数据库":
                if not self.sql.has_table(f"daily{symbol}"):  # 不存在就创建
                    logger.debug(f"创建数据库表daily{symbol}")
                    self.sql.create_table(table_name=f"daily{symbol}",
                                          columns=[
                                              YkColumn('trade_date', DATE(), primary_key=True),
                                              YkColumn('open', Float()),
                                              YkColumn('high', Float()),
                                              YkColumn('low', Float()),
                                              YkColumn('close', Float()),
                                              YkColumn('vol', Float()),
                                              YkColumn('amount', Float()),
                                          ])
                    logger.debug(f"插入数据")
                    try:
                        df.to_sql(name=f"daily{symbol}", con=self.engine,
                                  index=False, if_exists="append",
                                  dtype={
                                      'trade_date': DATE,
                                  })
                        # self.sql.insert_dataframe(f"daily{symbol}", df=df, if_exists='append') # 会报table has changed错误
                    except:
                        logger.debug(f"{df=}")
                else:
                    self.sql.insert_dataframe(f"daily{symbol}", df=df, if_exists='append')

                # 手动更新mysql表的更新时间
                self.sql.update_update_time_of_table(f"daily{symbol}")
            else:
                # noinspection all
                logger.debug("不支持本地存储")

        return df

    def download_daily(self, day_datetime: datetime.date):
        """
        下载更新给定日期的所有股票数据

        :param day_datetime: 给定日期
        :return:
        """
        if self.is_holiday(day_datetime):
            day_datetime = self.get_working_day_before_day(day_datetime, 1)

        if day_datetime == date.today():  # 对于当天的股票数据，需要在16:00以后才会更新数据
            hour = time.strftime("%H")
            if int(hour) < 16:  # 更新时间：交易日每天15点～16点之间，所以需要16点以后调用保证正确性，16点以前调用获得的是上一个交易日的数据
                day_datetime = self.get_working_day_before_day(day_datetime, 1)
                logger.debug("正常交易日，16:00之前当日数据尚未更新，使用上一个交易日的数据！")
            else:
                logger.debug("正常交易日，使用本日数据！")
        else:
            logger.debug("今天休市，使用上一个交易日的数据！")

        day_str = day_datetime.strftime("%Y%m%d")
        logger.debug("准备交易日数据，日期：{}".format(day_datetime))
        # ------------创建“api_daily/日期.csv”用以保存数据-----------
        parent_path1 = os.path.join(self.data_folder, 'api_daily')
        if not os.path.exists(parent_path1):
            os.mkdir(parent_path1)
        data_file = os.path.join(parent_path1, day_str + '.csv')
        # ------------创建“api_daily/日期.csv”用以保存数据-----------
        if not self.need_update(data_file):
            df = pd.read_csv(data_file, encoding=self.encode)
            logger.debug("检查本地数据：存在数据文件且无需更新，使用本地数据")
        else:
            df = self.api.daily(trade_date=day_str)  # 真正获得数据
            df.to_csv(data_file, encoding=self.encode, index=False)
            logger.debug("检查本地数据：本地数据文件不存在或需要更新，下载交易日数据...")
        return df

    def get_data_of_day(self, code, day_datetime: date = date.today(), next_day: int = -1):
        """
        获得给定代码的股票在给定日期的开盘，收盘等等价格信息，如果给定的日期休市，默认返回其前一个工作日的数据。
        只能获得最近一年内的信息，更早的信息目前不支持

        :param code: 股票代码
        :param day_datetime: 日期datetime.date
        :param next_day: 如果给定日期股票停牌或不是交易日，是否获得其他天的股票数据，-1获得给定日期前一天，0返回空值，1返回给定日期后一天数据，如果为空，返回空值
        :return: DataFrame格式数据
        """
        df = self.download(code, True)  # 获得股票近一年的数据
        day_str = day_datetime.strftime("%Y-%m-%d")
        df = df.loc[df['date'] == day_str]  # 检索day_str对应的日期的股票数据
        if len(df) == 0:  # 即当天没有查到数据
            if next_day == 0:
                return df
            elif next_day == -1:
                day_datetime = self.get_working_day_before_day(day_datetime, 1)
                return self.get_data_of_day(code, day_datetime, -1)
            else:
                day_datetime = self.get_working_day_before_day(day_datetime, -1)  # 获得后一个工作日
                last_datetime = datetime.datetime.strptime(str(self.get_previous_working_day(include_time=True)),
                                                           '%Y-%m-%d')  # 目前为止最后一个有数据的交易日
                if day_datetime >= last_datetime:  # 如果后一个工作日在今天(有数据的股票交易日)之前
                    return self.get_data_of_day(code, day_datetime, 1)  # 因为这里day_datetime必然是工作日，因此第三个参数
                    # 一般是不重要的，给了1是为了防止指定股票在当日停牌，这种情况极少见
                else:
                    logger.error("指定日期{}的股票数据不存在，检查日期是否是未来的日期！".format(day_datetime))
                    # 这种情况目前不存在用例，将来在设计返回参数
        return df

    def get_last_n_working_day(self, n: int = 30, sort='ascend', last_date=date.today()) -> list:
        """
        获得最近n个交易日的日期列表；如果给定last_date，则获得last_date之前的n个交易日数据

        :param n:
        :param sort: 排序方式，'ascend'：从前到后，'descend'：从后到前；默认从前到后
        :param last_date: n个交易中最后一个日期
        :return: 返回长度为n的date列表,列表项为datetime.date类型数据
        """
        # <<<<<<<<<<<<<<<<<<<<<方法1-----------------------------------------
        dates = []
        day = self.get_previous_working_day(last_date, include_time=True)  # 获得指定日期前的最后一个交易日
        while len(dates) < n:
            dates.append(day)
            day = self.get_working_day_before_day(day)
        # ---------------------方法1>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
        if sort.lower() == 'ascend':
            dates = dates[::-1]  # 反序排列列表
        elif sort.lower() == 'descend':
            pass
        else:
            logger.error("参数sort未识别，sort只能取值'ascend'和'descend'!")
        return dates

    def need_update(self, data_file: str or datetime.datetime):
        """
        检查数据文件是否需要更新；如果传入的是时间，则检查该时间之后是否有股票数据更新。
        当上一个工作日4点以后更新过，且今天时间不到下午4点，则无需更新

        :param data_file:
        :return:
        """
        if isinstance(data_file, datetime.datetime):
            last_change_time = data_file
            last_change_date_str = last_change_time.strftime("%Y-%m-%d")
        elif data_file is None:  # 如果文件为空
            return True
        elif os.path.exists(data_file):  # 如果文件存在，判断文件是否需要更新
            last_change_time = os.stat(data_file).st_mtime
            last_change_time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(last_change_time))
            last_change_time = datetime.datetime.strptime(last_change_time_str, "%Y-%m-%d %H:%M:%S")
            last_change_date_str = last_change_time.strftime("%Y-%m-%d")
        else:  # 文件不存在，则需要更新
            return True

        now_time = datetime.datetime.now()  # 当前时间
        now_time_date_str = now_time.strftime("%Y-%m-%d")  # 用来和最后修改日期作比较

        if last_change_date_str == now_time_date_str:  # 如果是今天修改的
            if last_change_time.hour > 15:  # 最后修改日期是当天15点以后，则不用更新数据
                logger.debug("数据文件{}已经最新，无需更新！".format(data_file))
                return False  # 无需更新数据
            else:
                if now_time.hour < 16:  # 如果今天修改过且今天还没到16:00，也无须更新
                    return False
                # 如果今天时holiday
                if self.is_holiday(date.today()):  # 如果今天时休息日，今天更新过也不用更新
                    logger.debug("数据文件{}已经最新，无需更新！".format(data_file))
                    return False
                else:
                    return True
        else:
            last_working_day_str = str(self.get_previous_working_day(include_time=True)) + ' 16:00:00'
            last_working_datetime = datetime.datetime.strptime(last_working_day_str, '%Y-%m-%d %H:%M:%S')
            if last_change_time > last_working_datetime:
                # 如果最后修改日期在上一个工作日15点之后
                if isinstance(data_file, str):
                    logger.debug("数据文件{}已经最新，无需更新！".format(data_file))
                else:
                    logger.debug("数据文件更新于{}，无需更新！".format(data_file))
                return False  # 无需更新数据
            else:
                return True

    def get_open_limit(self, up_down='up', day_datetime: datetime.date = date.today()):
        """
        获得对应日期的开盘涨/跌停股
        如果对应日期休市，则查询昨天，以此类推，找最近一个交易日的数据

        :param up_down:
        :param day_datetime 日期
        :return 返回开盘跌停股的code列表
        """
        if up_down.lower() == 'up':
            title = "涨停"
        elif up_down.lower() == 'down':
            title = "跌停"
        else:
            logger.debug("参数未识别，up_down只能取值'up'或'down'！")
        config.loggingTitle("查找开盘{}股({})".format(title, day_datetime), "start")
        open_limit = self._get_limit(up_down=up_down, day_datetime=day_datetime)
        logger.debug("开盘{}股列表:{}".format(title, open_limit))
        config.loggingTitle("查找开盘{}股({})".format(title, day_datetime), "end")
        return open_limit

    def _get_limit(self, up_down: str = 'up', day_datetime: datetime.date = date.today()) -> list:
        """
        私有类，查询并获取指定日期的开盘涨停或跌停股，给外部接口函数get_open_limit_up/down调用，如果传入的日期是休息日，则获得
        传入日期上一个工作日的开盘涨停或跌停股。

        :param up_down: 涨停/跌停，可取值"up"和"down"
        :param day_datetime: 指定日期
        :return:
        """
        result = []
        if self.is_holiday(day_datetime):  # 如果今天是休息日
            day_datetime = self.get_working_day_before_day(day_datetime, 1)

        # ------------把day由"%Y-%m-%d"转为"%Y%m%d"格式-----------
        day_str = day_datetime.strftime("%Y%m%d")
        # ------------把day由"%Y-%m-%d"转为"%Y%m%d"格式-----------

        # ------------创建“api_daily/日期.csv”用以保存数据-----------
        parent_path1 = os.path.join(self.data_folder, 'api_daily')
        if not os.path.exists(parent_path1):
            os.mkdir(parent_path1)
        data_file = os.path.join(parent_path1, day_str + '.csv')
        # ------------创建“api_daily/日期.csv”用以保存数据-----------
        if os.path.exists(data_file):  # 因为data_file是以日期命名的，存在就不需要更新
            # 判断数据是否有变化
            df = pd.read_csv(data_file, encoding=self.encoding)
        else:
            df = self.api.daily(trade_date=day_str)  # 真正获得数据
            df.to_csv(data_file, encoding=self.encoding, index=False)
        df = np.array(df)
        for stock in df:  # 判断每一只股票
            # 开盘涨/跌停股票开盘/收盘/最高/最低价都是相同的
            if stock[2] == stock[3] == stock[4] == stock[5]:
                if up_down.lower() == "up":
                    if (stock[2] - stock[6]) / stock[6] > 0.09:
                        logger.debug(
                            "开盘即涨停->代码:|%s|前一天股价:|%f|开盘价|%f" % (stock[0][0:6], stock[6], stock[2]))
                        result.append(stock[0][0:6])
                elif up_down.lower() == "down":
                    if (stock[6] - stock[2]) / stock[6] > 0.09 and stock[2] != 0:  # 排除停牌股票
                        logger.debug(
                            "开盘即跌停->代码:|%s|前一天股价:|%f|开盘价|%f" % (stock[0][0:6], stock[6], stock[2]))
                        result.append(stock[0][0:6])
                else:
                    logger.error("参数up_down未识别！")
        return result

    def is_working_time(self):
        """
        判断是否是交易时间
        """
        if not self.is_holiday():
            hour = datetime.datetime.hour
            if hour in range(9, 12):  # range(9,12)=[9,12)，即包含9，不包含12
                return True
            if hour in range(13, 15):
                return True
        return False

    def is_holiday(self, day_datetime: datetime.date = date.today()):
        """
        判断今天是不是休市，只适用于国内股市，不包括港股

        :param day_datetime: 给定日期的datetime类型
        :return: True 或 False
        """
        day_str = day_datetime.strftime("%Y%m%d")
        if (day_datetime.weekday() == 5) or (day_datetime.weekday() == 6):  # 周六和周日肯定是休市的，这里的5、6分别代表周六和周日
            return True
        date_str = day_str
        # fetch = select_in_table(table_name=self.假期表,
        #                         condition_dict={"calendarDate": date_str},
        #                         result_col=['isOpen'])
        is_open = self.sql.select_in_table(table_name=self.假期表, condition_dict={"calendarDate": day_datetime},
                                           result_col=['isOpen'])

        if is_open is None:  # 未查找到相关记录
            year = day_datetime.year
            try:
                holiday_data: pd.DataFrame = self.api.query('trade_cal', start_date=f'{year}0101',
                                                            end_date=f'{year}1231')  # tushare该接口现在收费了，不让使用
                holiday_data.rename(columns={"cal_date": "calendarDate", "is_open": "isOpen"}, inplace=True)
            except:
                traceback.print_exc()
                logger.debug(f'似乎没有tushare的数据获取权限，尝试使用baostock获取数据')
                bs.login()
                holiday_data: pd.DataFrame = bs.query_trade_dates(start_date=f'{year}-01-01',
                                                                  end_date=f'{year}-12-31').get_data()
                bs.logout()
                holiday_data.rename(columns={"calendar_date": "calendarDate", "is_trading_day": "isOpen"}, inplace=True)
            # 日期必须转换为datetime才能与数据库中的DATE列匹配
            holiday_data["calendarDate"] = pd.to_datetime(holiday_data["calendarDate"])
            holiday_data["isOpen"] = holiday_data["isOpen"].apply(lambda x: True if int(x) == 1 else False)

            # to_sql()语句存储时，使用dtype参数指定dataframe到数据库的对应类型
            # holiday_data[["calendarDate", "isOpen"]].to_sql(self.假期表, con=self.engine, if_exists='append',
            #                                                 index=False,
            #                                                 dtype={"calendarDate": DATE, "isOpen": BOOLEAN})
            self.sql.insert_dataframe(self.假期表, holiday_data, if_exists='append', index=False,
                                      dtype={"calendarDate": DATE, "isOpen": BOOLEAN})

            if holiday_data[holiday_data['calendarDate'] == date_str]['isOpen'].item():
                return False
            else:
                return True
        else:
            return not is_open  # python会自动将0转换成False，1转换成True

    def get_previous_working_day(self, last_date=date.today(), include_time=False) -> datetime.date:
        """
        获得指定日期的上一个交易日，如果不传入参数，则获得最近的一个交易日，包含今天。

        :param last_date: 指定的日期
        :param include_time: 是否判断时间，如果includeTime=True，则会进一步判断今天的时间，如果时间在下午4:00之前，则不包括今天，因为当天的股票数据还没整理出来
        :return: 最近一个交易日日期
        """
        if last_date > date.today():
            logger.warning("因为未来的股票数据不存在，不能获得将来日期的前一个工作日！")
        if self.is_holiday(last_date):
            one_day = datetime.timedelta(days=1)
            pre_day = last_date - one_day
            while self.is_holiday(pre_day):
                pre_day = pre_day - one_day
            return pre_day
        else:
            if include_time and last_date == date.today():  # 需要判断时间，且last_date是今天
                dt = datetime.datetime.now()
                hour = dt.hour
                if hour < 16:  # 额外对时间进行判断
                    return self.get_working_day_before_day(date.today())
            return last_date

    def recent_30_open_limit_down_to_file(self, up_down='up', days=30):
        """
        获得最近30天内涨/跌停的所有股票的信息，并计算涨/跌停后两天的股价变化情况。
        剔除开始统计的第一天和最后一天的涨/跌停股票，因为它们仍在涨/跌停过程中，统计的持续涨跌天数不全。
        倒数第二天的股票信息中，停止涨/跌停后第二天的增幅数据为'OutOfRange'，因为第二天的增幅需要明天的数据，
        明天还未到来，因此没有明天的数据，无法计算。
        增幅显示为NaN，则表示股票存在停牌。
        该方法主要为连跌后股票的增幅变化提供依据。
        返回的'连涨/跌出现次数'：1 表示正常情况，出现了一次连续涨/跌；2 表示出现了一次或多次连续涨跌；-2 表示以停牌持续到
        统计天数末尾；-3 表示倒数第二天的股票，这些股票不存在'连涨/跌后增幅2'。

        :param up_down: 统计连续涨停还是连续跌停，可以取值'up'或'down'，这里的涨/跌停不包括ST股。
        :param days: 统计的天数
        :return: codes, lastDays 股票代码和连续跌停的天数
        """
        # 确保OpenLimit文件夹存在
        file = os.path.join(self.data_folder, "OpenLimit")
        if not os.path.exists(file):
            os.mkdir(file)
        file = os.path.join(file, "recent30Down.csv")

        if up_down.lower() == 'up':
            title = "涨"
        elif up_down.lower() == 'down':
            title = "跌"
        else:
            logger.debug("参数未识别，up_down只能取值'up'或'down'！")

        # 拿到30天内每天的开盘涨/跌停股，以及对应的日期的列表
        limits, dates = self.get_open_limit_of_n_trading_days(days, up_down, sort='ascend')
        limits_set = []  # 将limit_downs中的每一个元素转化为集合
        for limit in limits:
            limits_set.append(set(limit))

        # <<<<<<<<<<<<<<<<<<<<<<首先删除数据不全的跌停股-----------------------------
        temp_set = limits_set[0]
        for i in range(1, len(limits_set)):
            """
            将temp_set和limits_set[i]的共有元素选出来，删除其他不共有的元素；因为如果一个元素在temp_set中，但
            不在limits_set[i]中，那么相对与limits_set[i]更靠内侧的日期的股票中再出现的temp_set中的元素就不属于数据
            不完整的股票了。举例如下：
            temp_set=['000001','000002']
            limits_set[0]=['000001','000002']
            limits_set[1]=['000001','000002','200002']
            limits_set[2]=['200003']
            limits_set[3]=['000001']
            limits_set[4]=['200005']
            则'000001'虽然在temp_set中，但是他在limits_set[3]中出现的时候，它的数据是全的。上一行是删除外侧日期
            即limits_set[0]和limits_set[1]中的'000001'，然后在limits_set[2]中没有'000001'，这是就要删除temp_set中的
            '000001'，下一行的工作就是删除temp_set中的'000001'
            """
            temp_set = limits_set[i] & temp_set
            # 从第一个集合里删除temp_set的元素，这里第一个集合指的是靠近最外边的日期的股票
            limits_set[i] = limits_set[i] - temp_set
            if len(temp_set) == 0:
                break
        # 删除另一侧的
        temp_set = limits_set[-1]
        for i in range(len(limits_set) - 2, 0, -1):
            temp_set = limits_set[i] & temp_set
            limits_set[i] = limits_set[i] - temp_set
            if len(temp_set) == 0:
                break
        # 删除边缘两天的所有股票，因为他们的持续天数都可能不全
        limits_set[0] = set()
        limits_set[-1] = set()
        # ----------------------首先删除数据不全的跌停股>>>>>>>>>>>>>>>>>>>>>>>>>>>>>

        # <<<<<<<<<<<<<<<<<<<<<<获得30天内所有出现过的开盘跌停股的集合-----------------
        codes = limits_set[0]  # 所有跌停股列表，为limit_downs_set所有元素的并集
        for i in range(1, len(limits_set)):
            codes = codes | limits_set[i]
        # ----------------------获得30天内所有出现过的开盘跌停股的集合>>>>>>>>>>>>>>>>>
        # 将limit_downs_set转换为列表，以方便进行索引
        limits = list(limits_set)
        # 将limits中每个元素转换为列表
        limits = [list(e) for e in limits]
        # 将codes转换为列表
        codes = list(codes)
        codes.sort(reverse=False)  # 随便排个序，不影响结果，只是为了方便每次调试时股票顺序一致好对比
        lastDays = []  # 每个涨/跌停股对应的持续天数
        increase_after1 = []  # 每个涨/跌停股停止涨/跌停后的第二天开盘价涨跌幅
        increase_after2 = []
        flags = []  # 标记当前股数据是否已经统计
        # -3表示倒数第二天的股票，这些股票的increase_after2不存在，因为涉及到了明天的数据，明天数据不存在，使得对应增幅无法计算
        # -2表示以跌停停牌，至今没有开牌
        # -1没统计，
        # 0正在统计/至今还处于跌停状态，
        # 1统计过一次，
        # 2开盘跌停在30天内出现一次以上

        for i in range(len(codes)):  # 数据齐全的所有股票代码
            lastDays.append(0)
            increase_after1.append(None)
            increase_after2.append(None)
            flags.append(-1)
        for i in range(len(codes)):  # 遍历所有需要统计的代码
            for j in range(len(limits)):  # 遍历所有日期的涨/跌停股
                if codes[i] in limits[j]:  # 首先查找包含codes[i]的日期，然后开始进入统计
                    if flags[i] == -1 or flags[i] == 0:
                        lastDays[i] = lastDays[i] + 1
                        flags[i] = 0
                    else:
                        flags[i] = flags[i] + 1
                else:
                    if flags[i] == 0:
                        flags[i] = 1
                        # 获得昨天和今天的股票数据，因为连续跌停时，停牌股概率大增，这里要考虑停牌影响
                        yesterday = self.get_data_of_day(codes[i], dates[j - 1], next_day=-1)
                        today = self.get_data_of_day(codes[i], dates[j], next_day=0)
                        if j < len(limits) - 1:
                            tomorrow = self.get_data_of_day(codes[i], dates[j + 1], next_day=0)
                        else:
                            tomorrow = []
                            outOfRange = True

                        if len(today) == 0:  # 因为dates中的日期都是工作日，如果查不到数据，则必然是股票停牌
                            flags[i] = -2
                        elif len(tomorrow) == 0:  # 停牌
                            open_yest = float(yesterday['close'])
                            open_today = float(today['close'])
                            increase_after1[i] = (open_today - open_yest) / open_yest
                            flags[i] = -2
                            if outOfRange:
                                increase_after2[i] = 'OutOfRange'
                                flags[i] = -3
                        else:
                            open_yest = float(yesterday['close'])
                            open_today = float(today['close'])
                            open_next = float(tomorrow['close'])
                            increase_after1[i] = (open_today - open_yest) / open_yest
                            increase_after2[i] = (open_next - open_today) / open_today
        # <<<<<<<<<<<<<<<<<<<<<<<获得具有完整跌停信息的股票的数据----------------------
        localData = {'股票代码': codes, '持续天数': lastDays, '连{}后增幅1'.format(title): increase_after1,
                     '连{}后增幅2'.format(title): increase_after2, '连{}出现次数'.format(title): flags}
        df = pd.DataFrame(localData)
        df.to_csv(file, encoding="utf8", index=False)
        logger.debug("{}到{}日连续{}停股票统计：\n{}".format(dates[0], dates[-1], title, df))
        return df

    def get_open_limit_and_lastdays(self, up_down='up', days: int = 10, last_date=date.today()):
        """
        将最近一个工作日的开盘涨/跌停股及其持续涨/跌停天数写到文件"./OpenLimit/recentDown.csv"

        :param up_down:
        :param days: 统计的天数，这里设置为10，因为连续涨/跌停
        :param last_date: 如果指定last_date，则获得该日期时所有开盘涨/跌停股的信息
        :return: DataFrame 股票代码和连续跌停的天数
        """
        # 处理up_down参数
        if up_down.lower() == 'up':
            title = "Up"
            title1 = "涨停"
        elif up_down.lower() == 'down':
            title = "Down"
            title1 = "跌停"
        else:
            logger.error("输入参数未识别，up_down只能取值'up'或'down'！")
        # 确保OpenLimit文件夹存在
        file = os.path.join(self.data_folder, "OpenLimit")
        if not os.path.exists(file):  # 确保文件夹存在
            os.mkdir(file)

        logger.debug("获取{}的{}股代码及持续天数...".format(last_date, title1))
        limits, dates = self.get_open_limit_of_n_trading_days(days, up_down, 'descend', last_date=last_date)
        file = os.path.join(file, "recent{}{}.csv".format(title, dates[0]))
        if os.path.exists(file):
            df = pd.read_csv(file, encoding=self.encode, dtype={'股票代码': str})
        else:
            today_codes = limits[0]
            codes = today_codes
            lastDays = []
            flag = []
            for _ in codes:  # 初始化最近一天的涨/跌停股，每一个股对应的持续天数初始化为1
                lastDays.append(1)
                flag.append(True)
            for i in range(1, days):  # 天数
                for j in range(len(codes)):  # 第一天涨/跌股的数量，逐个检查每个股
                    if (codes[j] in limits[i]) and flag[j]:  # 如果前一天的跌停股列表里仍有code，则code的持续天数+1
                        lastDays[j] = lastDays[j] + 1
                    else:  # 如果前一天的跌停股列表里没有code，则将code标记为False，其lastdays列表中对应的天数将不再改变
                        flag[j] = False
            data = {'股票代码': codes, '持续天数': lastDays}
            df = pd.DataFrame(data)
            df.to_csv(file, encoding=self.encode, index=False)
        logger.debug("get_open_limit_and_lastdays()获得{}的{}股和持续天数：\n{}".format(last_date, title1, df))
        return df

    def get_open_limit_of_n_trading_days(self, days=30, up_down='up',
                                         sort='ascend', last_date=date.today()):
        """
        获得最近n个交易日的开盘涨/跌停股，包括今天（如果今天是交易日）；这里的涨/跌停不包括ST股。

        :param days: 统计的天数。
        :param up_down: 涨停还是跌停，取值：'up'或者'down'，默认是'up'。
        :param sort: 排序方式，'ascend'：从前到后，'descend'：从后到前；默认从前到后。
        :param last_date: 指定日期，如果不指定，则是最近的n个交易日，如果指定，则以指定日期为最后一个交易日。
        :return: limit_down_of_days和dates: [[],[],...[]]，列表中的每一个列表对应每一天的跌停股，result[0]是最近一个交易日的跌停股，如果今天是交易日，则result[0]就是今天。result[n]是第n个交易日前的跌停股，dates: 返回长度为n的date列表,列表为字符串，格式为"%Y-%m-%d"
        """
        if up_down.lower() == 'up':
            tmp = '涨停'
        elif up_down.lower() == 'down':
            tmp = '跌停'
        else:
            logger.error("参数未识别，up_down只能取值'up'或'down'！")
        config.loggingTitle("获取最近{}个交易日的{}信息".format(days, tmp), 'start')
        date_times = self.get_last_n_working_day(days, sort, last_date=last_date)
        open_limit_list = []

        for day_datetime in date_times:
            logger.debug("获取{}的开盘{}股...".format(day_datetime, tmp))
            limit = self.get_open_limit(up_down, day_datetime)
            open_limit_list.append(limit)
            logger.debug("{}的开盘{}股有：{}".format(day_datetime, tmp, limit))

        config.loggingTitle("获取最近{}个交易日的{}信息".format(days, tmp), 'end')
        return open_limit_list, date_times

    def get_working_day_before_day(self, day_datetime: datetime.date = date.today(), day_num: int = 1) -> datetime.date:
        """
        获得指定日期的前一个交易日或后一个交易日，需要注意的是，该函数不对指定日期是否为工作日进行判断，
        因此，获得最近一个交易日，不能使用get_working_day_before_day(date.today(),1)
        :param day_datetime: datetime类型，默认为今天
        :param day_num: 可取正整数和-1，前day_num个交易日，取-1时,表示指定日期的后一个交易日，不可以取其他负值
        :return: date_datetime】,-----返回对应交易日的datetime类型数据
        """
        # day = day_datetime.strftime("%Y-%m-%d")
        if day_num < -1:
            logger.error("day_num不能取-1以外的负值（{}）".format(day_num))
            return None
        if day_num == 1 or day_num == -1:
            one_day = datetime.timedelta(days=1)
            if day_num == -1:
                pre_day = day_datetime + one_day
            else:
                pre_day = day_datetime - one_day
            while self.is_holiday(pre_day):
                if day_num == -1:
                    pre_day = pre_day + one_day
                else:
                    pre_day = pre_day - one_day
            return pre_day
        else:  # day_num为大于1的整数
            day_datetime = self.get_working_day_before_day(day_datetime, 1)  # 计算上一个交易日
            return self.get_working_day_before_day(day_datetime, day_num - 1)  # 返回上一个交易日的上day_num-1个交易日

    @staticmethod
    def get_realtime_price(symbol):
        """
        获得股票实时价格
        只有实时价格需要实时获取，开盘、收盘等价格可能不需要实时更新，为了节省时间，顺便更新其他价格
        """
        data = ts.get_realtime_quotes(symbol)  # ts的实时行情接口在某些股票中会报错
        # ，因为某些股票的数据多了一个空数据，需要等待Tushare更新或者覆盖get_realtime_quotes中
        # 的相关参数，具体可以在get_realtime_quotes的下面代码之前，添加代码
        # df = pd.DataFrame(data_list, columns=ct.LIVE_DATA_COLS)
        # 添加的代码为：
        # if len(data_list[0])>33:
        #   data_list=data_list[0][:-1]
        name = str(data['name'][0])  # 这四条语句的执行是本地执行，且耗时很少
        open_price = float(data['open'])
        high = float(data['high'])
        low = float(data['low'])
        now_price = float(data['price'])
        return name, now_price, open_price, high, low

    def init_storage(self):
        """
        初始化存储的数据库操作对象
        """
        if self.storage is None:
            return
        if self.storage.lower() == "sqlite":
            if self.engine is None:
                if self.project.db_name is None:
                    self.project.db_name = 'stocks.db'
                self.engine = create_engine(f'sqlite:///{self.project.db_name}', echo=False, encoding=self.encoding)
                self.sql = SqlOperator(self.engine)
                self.sql.create_all_base_table()
            else:
                pass
        elif self.storage == "mysql":
            if self.engine is None:
                user = self.project.db_user
                passwd = self.project.db_passwd
                ip = self.project.db_ip
                port = self.project.db_port
                db = self.project.db_name

                self.engine = create_engine(
                    # f"mysql+mysqlconnector://{user}:{passwd}@{ip}:{port}/{db}?charset={self.encoding}", echo=False,
                    f"mysql+pymysql://{user}:{passwd}@{ip}:{port}/{db}?charset={self.encoding}", echo=False,
                    poolclass=QueuePool,
                    pool_size=5,
                    max_overflow=10,
                    pool_timeout=30,  # 连接池中没有可用连接时的等待时间，超过该时间还没有可用连接，则报错
                    pool_recycle=3600,  # 连接池中的连接1小时后会回收
                    pool_pre_ping=True,  # 每次连接请求时，先测试返回的连接是否还存活，不存活则从连接池中取下一个连接
                )  # mysql数据库连接时charset最好在url中指定，因为SqlAlchemy2.0在外部指定时会报错

                self.sql = SqlOperator(self.engine)
                self.sql.create_all_base_table()
            else:
                pass


if __name__ == '__main__':
    sd = StockData(project={"storage": "mysql"})
    sd.download_all_stocks()
