def get_stockdata(stock_list,start_date,end_date): df={} for stock in stock_list: code = stock filename = code+".csv" path = "./stock/" if not os.path.exists(path): os.makedirs(path) if os.path.exists(path + filename): df_stock = pd.read_csv(path + filename) else: quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111) ret, df_stock, page_req_key = quote_ctx.request_history_kline(stock, start=start_date, end=end_date) quote_ctx.close() df_stock['date']=pd.to_datetime(df_stock['time_key']) df_stock.to_csv(path + filename) df_stock.index = pd.to_datetime(df_stock.date) df_stock['openinterest']=0 df_stock = df_stock[['open','high','low','close','volume','openinterest']] df.update({stock:df_stock}) return df
class FutuGateway(BaseGateway): """""" default_setting = { "密码": "", "地址": "127.0.0.1", "端口": 11111, "市场": ["HK", "US"], "环境": [TrdEnv.REAL, TrdEnv.SIMULATE], } exchanges = list(EXCHANGE_FUTU2VT.values()) def __init__(self, event_engine): """Constructor""" super(FutuGateway, self).__init__(event_engine, "FUTU") self.quote_ctx = None self.trade_ctx = None self.host = "" self.port = 0 self.market = "" self.password = "" self.env = TrdEnv.SIMULATE self.ticks = {} self.trades = set() self.contracts = {} self.thread = Thread(target=self.query_data) # For query function. self.count = 0 self.interval = 1 self.query_funcs = [self.query_account, self.query_position] # For historical data fetching self.page_req_key = None self.history_list = [] def connect(self, setting: dict): """""" self.host = setting["地址"] self.port = setting["端口"] self.market = setting["市场"] self.password = setting["密码"] self.env = setting["环境"] self.connect_quote() self.connect_trade() self.thread.start() def query_data(self): """ Query all data necessary. """ sleep(2.0) # Wait 2 seconds till connection completed. self.query_contract() self.query_trade() self.query_order() self.query_position() self.query_account() # Start fixed interval query. self.event_engine.register(EVENT_TIMER, self.process_timer_event) def process_timer_event(self, event): """""" self.count += 1 if self.count < self.interval: return self.count = 0 func = self.query_funcs.pop(0) func() self.query_funcs.append(func) def connect_quote(self): """ Connect to market data server. """ self.quote_ctx = OpenQuoteContext(self.host, self.port) class QuoteHandler(StockQuoteHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(QuoteHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_quote(content) return RET_OK, content class OrderBookHandler(OrderBookHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderBookHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_orderbook(content) return RET_OK, content self.quote_ctx.set_handler(QuoteHandler()) self.quote_ctx.set_handler(OrderBookHandler()) self.quote_ctx.start() self.write_log("行情接口连接成功") def connect_trade(self): """ Connect to trade server. """ # Initialize context according to market. if self.market == "US": self.trade_ctx = OpenUSTradeContext(self.host, self.port) else: self.trade_ctx = OpenHKTradeContext(self.host, self.port) # Implement handlers. class OrderHandler(TradeOrderHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(OrderHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_order(content) return RET_OK, content class DealHandler(TradeDealHandlerBase): gateway = self def on_recv_rsp(self, rsp_str): ret_code, content = super(DealHandler, self).on_recv_rsp(rsp_str) if ret_code != RET_OK: return RET_ERROR, content self.gateway.process_deal(content) return RET_OK, content # Unlock to allow trading. code, data = self.trade_ctx.unlock_trade(self.password) if code == RET_OK: self.write_log("交易接口解锁成功") else: self.write_log(f"交易接口解锁失败,原因:{data}") # Start context. self.trade_ctx.set_handler(OrderHandler()) self.trade_ctx.set_handler(DealHandler()) self.trade_ctx.start() self.write_log("交易接口连接成功") def subscribe(self, req: SubscribeRequest): """""" for data_type in ["QUOTE", "ORDER_BOOK"]: futu_symbol = convert_symbol_vt2futu(req.symbol, req.exchange) code, data = self.quote_ctx.subscribe(futu_symbol, data_type, True) if code: self.write_log(f"订阅行情失败:{data}") def send_order(self, req: OrderRequest): """""" side = DIRECTION_VT2FUTU[req.direction] futu_order_type = OrderType.NORMAL # Only limit order is supported. # Set price adjustment mode to inside adjustment. if req.direction is Direction.LONG: adjust_limit = 0.05 else: adjust_limit = -0.05 futu_symbol = convert_symbol_vt2futu(req.symbol, req.exchange) code, data = self.trade_ctx.place_order( req.price, req.volume, futu_symbol, side, futu_order_type, trd_env=self.env, adjust_limit=adjust_limit, ) if code: self.write_log(f"委托失败:{data}") return "" for ix, row in data.iterrows(): orderid = str(row["order_id"]) order = req.create_order_data(orderid, self.gateway_name) self.on_order(order) return order.vt_orderid def cancel_order(self, req: CancelRequest): """""" code, data = self.trade_ctx.modify_order(ModifyOrderOp.CANCEL, req.orderid, 0, 0, trd_env=self.env) if code: self.write_log(f"撤单失败:{data}") def query_contract(self): """""" for product, futu_product in PRODUCT_VT2FUTU.items(): code, data = self.quote_ctx.get_stock_basicinfo( self.market, futu_product) if code: self.write_log(f"查询合约信息失败:{data}") return for ix, row in data.iterrows(): symbol, exchange = convert_symbol_futu2vt(row["code"]) contract = ContractData( symbol=symbol, exchange=exchange, name=row["name"], product=product, size=1, pricetick=0.001, net_position=True, gateway_name=self.gateway_name, ) self.on_contract(contract) self.contracts[contract.vt_symbol] = contract self.write_log("合约信息查询成功") def query_account(self): """""" code, data = self.trade_ctx.accinfo_query(trd_env=self.env, acc_id=0) if code: self.write_log(f"查询账户资金失败:{data}") return for ix, row in data.iterrows(): account = AccountData( accountid=f"{self.gateway_name}_{self.market}", balance=float(row["total_assets"]), frozen=(float(row["total_assets"]) - float(row["avl_withdrawal_cash"])), gateway_name=self.gateway_name, ) self.on_account(account) def query_position(self): """""" code, data = self.trade_ctx.position_list_query(trd_env=self.env, acc_id=0) if code: self.write_log(f"查询持仓失败:{data}") return for ix, row in data.iterrows(): symbol, exchange = convert_symbol_futu2vt(row["code"]) pos = PositionData( symbol=symbol, exchange=exchange, direction=Direction.LONG, volume=row["qty"], frozen=(float(row["qty"]) - float(row["can_sell_qty"])), price=float(row["cost_price"]), pnl=float(row["pl_val"]), gateway_name=self.gateway_name, ) self.on_position(pos) def query_order(self): """""" code, data = self.trade_ctx.order_list_query("", trd_env=self.env) if code: self.write_log(f"查询委托失败:{data}") return self.process_order(data) self.write_log("委托查询成功") def query_trade(self): """""" code, data = self.trade_ctx.deal_list_query("", trd_env=self.env) if code: self.write_log(f"查询成交失败:{data}") return self.process_deal(data) self.write_log("成交查询成功") def query_history(self, req: HistoryRequest): """""" self.write_log("FUTU Gateway:开始下载") print(f"FUTU download{req.symbol}") print(f"req.startdate:{req.start} - {req.end}") code = convert_symbol_vt2futu(req.symbol, req.exchange) start = req.start.strftime("%Y-%m-%d") end = req.end.strftime("%Y-%m-%d") ret, data, self.page_req_key = self.quote_ctx.request_history_kline( code, start=start, end=end, ktype=INTERVAL_KLINE_VT2FUTU[req.interval], max_count=1) dt = datetime.strptime(data.ix[0].time_key, "%Y-%m-%d %H:%M:%S") data_bar = BarData(gateway_name=self.gateway_name, symbol=req.symbol, exchange=req.exchange, datetime=dt, interval=req.interval, volume=float(data.volume), open_price=data.open, high_price=data.high, low_price=data.low, close_price=data.close) self.history_list.append(data_bar) while self.page_req_key is not None: ret, data, self.page_req_key = self.quote_ctx.request_history_kline( code, start=start, end=end, ktype=INTERVAL_KLINE_VT2FUTU[req.interval], max_count=1000, page_req_key=self.page_req_key) # data append for index, df in data.iterrows(): print(index, " df", df.time_key) dt = datetime.strptime(df.time_key, "%Y-%m-%d %H:%M:%S") data_bar = BarData(gateway_name=self.gateway_name, symbol=req.symbol, exchange=req.exchange, datetime=dt, interval=req.interval, volume=float(df.volume), open_price=df.open, high_price=df.high, low_price=df.low, close_price=df.close) self.history_list.append(data_bar) history = self.history_list self.history_list = [] return history def close(self): """""" if self.quote_ctx: self.quote_ctx.close() if self.trade_ctx: self.trade_ctx.close() def get_tick(self, code): """ Get tick buffer. """ tick = self.ticks.get(code, None) symbol, exchange = convert_symbol_futu2vt(code) if not tick: tick = TickData( symbol=symbol, exchange=exchange, datetime=datetime.now(CHINA_TZ), gateway_name=self.gateway_name, ) self.ticks[code] = tick contract = self.contracts.get(tick.vt_symbol, None) if contract: tick.name = contract.name return tick def process_quote(self, data): """报价推送""" for ix, row in data.iterrows(): symbol = row["code"] date = row["data_date"].replace("-", "") time = row["data_time"] dt = datetime.strptime(f"{date} {time}", "%Y%m%d %H:%M:%S") dt = dt.replace(tzinfo=CHINA_TZ) tick = self.get_tick(symbol) tick.datetime = dt tick.open_price = row["open_price"] tick.high_price = row["high_price"] tick.low_price = row["low_price"] tick.pre_close = row["prev_close_price"] tick.last_price = row["last_price"] tick.volume = row["volume"] if "price_spread" in row: spread = row["price_spread"] tick.limit_up = tick.last_price + spread * 10 tick.limit_down = tick.last_price - spread * 10 self.on_tick(copy(tick)) def process_orderbook(self, data): """""" symbol = data["code"] tick = self.get_tick(symbol) d = tick.__dict__ for i in range(5): bid_data = data["Bid"][i] ask_data = data["Ask"][i] n = i + 1 d["bid_price_%s" % n] = bid_data[0] d["bid_volume_%s" % n] = bid_data[1] d["ask_price_%s" % n] = ask_data[0] d["ask_volume_%s" % n] = ask_data[1] if tick.datetime: self.on_tick(copy(tick)) def process_order(self, data): """ Process order data for both query and update. """ for ix, row in data.iterrows(): # Ignore order with status DELETED if row["order_status"] == OrderStatus.DELETED: continue symbol, exchange = convert_symbol_futu2vt(row["code"]) order = OrderData( symbol=symbol, exchange=exchange, orderid=str(row["order_id"]), direction=DIRECTION_FUTU2VT[row["trd_side"]], price=float(row["price"]), volume=row["qty"], traded=row["dealt_qty"], status=STATUS_FUTU2VT[row["order_status"]], datetime=generate_datetime(row["create_time"]), gateway_name=self.gateway_name, ) self.on_order(order) def process_deal(self, data): """ Process trade data for both query and update. """ for ix, row in data.iterrows(): tradeid = str(row["deal_id"]) if tradeid in self.trades: continue self.trades.add(tradeid) symbol, exchange = convert_symbol_futu2vt(row["code"]) trade = TradeData( symbol=symbol, exchange=exchange, direction=DIRECTION_FUTU2VT[row["trd_side"]], tradeid=tradeid, orderid=row["order_id"], price=float(row["price"]), volume=row["qty"], datetime=generate_datetime(row["create_time"]), gateway_name=self.gateway_name, ) self.on_trade(trade)
class FutuAPI: # DATA_PATH = "/Users/joseph/Dropbox/code/stat-arb/data" DATA_PATH = "/home/atabet/projects/data" HK_EQUITY_AM_START = timer(9, 30, 0) HK_EQUITY_AM_END = timer(12, 0, 0) HK_EQUITY_PM_START = timer(13, 0, 0) HK_EQUITY_PM_END = timer(16, 0, 0) def __init__(self): self.subscribe_data = dict() self.broker_queue = dict() def __enter__(self): self.quote_ctx = OpenQuoteContext(host='127.0.0.1', port=11111) self.connect_quote() return self def __exit__(self, type, value, trace): self.quote_ctx.close() # 结束后记得关闭当条连接,防止连接条数用尽 print("Close quote context!") def subscribe(self, codes: List[str], sub_types: List[SubType], sub_push: bool): ret_sub, err_message = self.quote_ctx.subscribe( codes, sub_types, subscribe_push=sub_push) if ret_sub == RET_ERROR: raise ValueError(f"subscribe error: {err_message}") def get_global_state(self) -> dict: """ 获取全局市场状态 https://openapi.futunn.com/futu-api-doc/quote/get-global-state.html :return: (0, {'market_sz': 'CLOSED', 'market_us': 'PRE_MARKET_BEGIN', 'market_sh': 'CLOSED', 'market_hk': 'CLOSED', 'market_hkfuture': 'FUTURE_DAY_OPEN', 'market_usfuture': 'FUTURE_OPEN', 'server_ver': '217', 'trd_logined': True, 'timestamp': '1602491044', 'qot_logined': True, 'local_timestamp': 1602491044.555623, 'program_status_type': 'READY', 'program_status_desc': ''}) """ ret, data = self.quote_ctx.get_global_state() if ret == RET_OK: return data raise ValueError(f"get_global_state error: {data}") def get_capital_distribution(self, code: str = "HK.00700"): """ 获取资金分布 :param code: 股票代号 :return: """ ret, data = self.quote_ctx.get_capital_distribution(code) if ret == RET_OK: return data else: print('error:', data) def get_capital_flow(self, code: str = "HK.00700"): """ 获取资金流向 :param code: 股票代号 :return: """ ret, data = self.quote_ctx.get_capital_flow(code) if ret == RET_OK: return data else: print('error:', data) def get_broker_queue(self, code: str = "HK.00700"): """ 获取实时经纪队列 :param code: :return: """ # 如果通过推送获取数据,直接在缓存里提取最新的copy if code in self.broker_queue: return self.broker_queue[code] # 否则通过富途服务器获取。先订阅经纪队列类型。订阅成功后FutuOpenD将持续收到服务器的推送,False代表暂时不需要推送给脚本 if (code not in self.subscribe_data): ret_sub, err_message = self.quote_ctx.subscribe( [code], [SubType.BROKER], subscribe_push=False) if ret_sub != RET_OK: print(f"获取实时经纪队列(get_broker_queue)失败, {err_message}") self.subscribe_data[code] = dict() self.subscribe_data[code][SubType.BROKER] = True elif (SubType.BROKER not in self.subscribe_data[code]): ret_sub, err_message = self.quote_ctx.subscribe( [code], [SubType.BROKER], subscribe_push=False) if ret_sub != RET_OK: print(f"获取实时经纪队列(get_broker_queue)失败, {err_message}") self.subscribe_data[code][SubType.BROKER] = True ret, bid_frame_table, ask_frame_table = self.quote_ctx.get_broker_queue( code) # 获取一次经纪队列数据 if ret == RET_OK: return bid_frame_table, ask_frame_table else: print('error:', bid_frame_table) def process_broker_queue(self, data: pd.DataFrame): bid_broker, ask_broker = data codes = list(bid_broker["code"].unique()) assert len(codes) == 1, f"broker_queue pushback 不是合法的数据:{codes}" code = codes[0] self.broker_queue[code] = (bid_broker, ask_broker) def connect_quote(self): class BrokerQueueHandler(BrokerHandlerBase): api = self def on_recv_rsp(self, rsp_pb): ret_code, err_or_stock_code, data = super( BrokerQueueHandler, self).on_recv_rsp(rsp_pb) if ret_code != RET_OK: print( "BrokerTest: error, msg: {}".format(err_or_stock_code)) return RET_ERROR, data api.process_broker_queue(data) # BrokerQueueHandler自己的处理逻辑 return RET_OK, data self.quote_ctx.set_handler(BrokerQueueHandler()) self.quote_ctx.start() def get_history_kl_quota(self, get_detail: bool = False): """ 获取历史 K 线额度使用明细 接口限制 ------ 我们会根据您账户的资产和交易的情况,下发历史 K 线额度。因此,30 天内您只能获取有限只股票的历史 K 线数据。具体规则参见 API 用户额度 。 您当日消耗的历史 K 线额度,会在 30 天后自动释放。 https://openapi.futunn.com/futu-api-doc/quote/get-history-kl-quota.html :param get_detail: 设置True代表需要返回详细的拉取历史K 线的记录 :return: 例子 (1, 99, [{'code': 'HK.00700', 'request_time': '2020-03-27 19:15:57'}]) """ ret, data = self.quote_ctx.get_history_kl_quota(get_detail=get_detail) if ret == RET_OK: return data else: print('error:', data) def request_history_kline(self, code: str, start: str, end: str, ktype: KLType = KLType.K_DAY, autype: AuType = AuType.QFQ, fields: List[KL_FIELD] = [KL_FIELD.ALL], max_count: int = 500, extended_time: bool = False): """ 获取历史 K 线 接口限制 ------- 我们会根据您账户的资产和交易的情况,下发历史 K 线额度。因此,30 天内您只能获取有限只股票的历史 K 线数据。具体规则参见 API 用户额度 。您当日消耗的 历史 K 线额度,会在 30 天后自动释放。 每 30 秒内最多请求 60 次历史 K 线接口。注意:如果您是分页获取数据,此限频规则仅适用于每只股票的首页,后续页请求不受限频规则的限制。 分 K 提供最近 2 年数据,日 K 及以上提供最近 10 年的数据。 美股盘前和盘后 K 线仅支持 60 分钟及以下级别。由于美股盘前和盘后时段为非常规交易时段,此时段的 K 线数据可能不足 2 年。 https://openapi.futunn.com/futu-api-doc/quote/request-history-kline.html :param code: 'HK.00700' :param start: '2019-09-11' :param end: '2019-09-18' :param ktype: KLType.K_DAY, :param autype: AuType.QFQ, :param fields: [KL_FIELD.ALL], :param max_count: 500, :param extended_time: False :return: """ ret, data, page_req_key = self.quote_ctx.request_history_kline( code=code, start=start, end=end, ktype=ktype, autype=autype, fields=fields, max_count=max_count, extended_time=extended_time, ) # 每页max_count个,请求第一页 if ret == RET_OK: yield data else: print('error:', data) return while page_req_key != None: # 请求后面的所有结果 print('*************************************') ret, data, page_req_key = self.quote_ctx.request_history_kline( code=code, start=start, end=end, ktype=ktype, autype=autype, fields=fields, max_count=max_count, extended_time=extended_time, page_req_key=page_req_key) # 请求翻页后的数据 if ret == RET_OK: yield data else: print('error:', data) return def get_rehab(self, code: str = "HK.00700"): """ 获取复权因子 接口限制 ------ 每 30 秒内最多请求 60 次获取复权因子接口。 https://openapi.futunn.com/futu-api-doc/quote/get-rehab.html :param market: :param security: :return: """ ret, data = self.quote_ctx.get_rehab(code) if ret == RET_OK: return data else: print('error:', data) def get_cur_kline(self, code_list: List[str] = ["00700.HK"], ktype_list: List[SubType] = [SubType.K_1M], num: int = 1000, autype=AuType.QFQ): """ 获取实时 K 线 :param code_list: 股票代码列表 :param ktype_list: K 线类型列表 :param num: K 线数据个数,最多 1000 根 :param autype: 复权类型 :return: """ ret_sub, err_message = self.quote_ctx.subscribe(code_list, ktype_list, subscribe_push=False) # 先订阅K 线类型。订阅成功后FutuOpenD将持续收到服务器的推送,False代表暂时不需要推送给脚本 if ret_sub == RET_OK: # 订阅成功 ret_data = [] for code, ktype in zip(code_list, ktype_list): ret, data = self.quote_ctx.get_cur_kline( code, num, ktype, autype) # 获取港股00700最近2个K线数据 if ret == RET_OK: ret_data.append(data) else: print('error:', data) ret_data.append(None) return ret_data else: print('subscription failed', err_message) def record_cur_kline(self, code_list: List[str] = ["00700.HK"], ktype_list: List[SubType] = [SubType.K_1M], record_time: int = 28800): # 3600*8 """ 记录实时 K 线 (非futu原有api) :param market: 市场 :param security: 股票代码 :param ktype: K 线类型 :return: """ handler = CurKlineHandler() self.quote_ctx.set_handler(handler) # 设置实时摆盘回调 self.quote_ctx.subscribe(code_list, ktype_list) # 订阅K线数据类型,FutuOpenD开始持续收到服务器的推送 time.sleep(record_time) # 设置脚本接收FutuOpenD的推送持续时间 self.quote_ctx.unsubscribe(code_list, ktype_list) # 反订阅K线数据类型(1分钟后生效) def query_subscription(self, is_all_conn: bool = True): """ :param is_all_conn: 是否返回所有连接的订阅状态。True:返回所有连接的订阅状态;False:只返回当前连接的订阅状态 :return: """ ret, data = self.quote_ctx.query_subscription(is_all_conn=is_all_conn) if ret == RET_OK: return data else: print('error:', data) def get_owner_plate(self, code_list: List): """ 获取股票所属板块 :param code_list: :return: """ ret, data = self.quote_ctx.get_owner_plate(code_list) if ret == RET_OK: return data else: print('error:', data) def get_plate_list(self, market: Market, plate_class: Plate): """ 获取板块列表 :param market: :param plate_class: :return: """ ret, data = self.quote_ctx.get_plate_list(market, plate_class) if ret == RET_OK: return data else: print('error:', data) def get_plate_stock(self, plate_code: str, sort_field: SortField = SortField.CODE, ascend: bool = True): """ 获取板块内股票列表 :param plate_code: :param sort_field: :param ascend: :return: """ ret, data = self.quote_ctx.get_plate_stock(plate_code) if ret == RET_OK: return data else: raise ValueError(f'error: {data}') def get_stock_filter(self, market: Market, filter_list: List[Union[SimpleFilter, AccumulateFilter, FinancialFilter]], plate_code: str = None, begin: int = 0, num: int = 200): """ 条件选股 :param market: :param filter_list: :param plate_code: :param begin: :param num: :return: """ ret, ls = self.quote_ctx.get_stock_filter(market, filter_list, plate_code=plate_code, begin=begin, num=num) # 对香港市场的股票做简单筛选 if ret == RET_OK: # last_page, all_count, ret_list = ls # print(len(ret_list), all_count, ret_list) # for item in ret_list: # print(item.stock_code) # 取其中的股票代码 return ls else: raise ValueError(f'error: {ls}') def save_history_kline(self, code: str, start: str = None, end: str = None, ktype: KLType = KLType.K_DAY, autype: AuType = AuType.QFQ, fields: List[KL_FIELD] = [KL_FIELD.ALL], max_count: int = 5000, extended_time: bool = False): # 检查开始和结束时间 now = datetime.now() if end is None: end_dt = now end = datetime.strftime(end_dt, "%Y-%m-%d") else: end_dt = datetime.strptime(end, "%Y-%m-%d") if start is None: start_dt = now - relativedelta(months=25) start = datetime.strftime(start_dt, "%Y-%m-%d") else: start_dt = datetime.strptime(start, "%Y-%m-%d") assert start_dt < end_dt, "start>=end, invalid time input!" history_kline = self.request_history_kline( code=code, start=start, end=end, ktype=ktype, max_count=max_count, fields=fields, # [KL_FIELD.DATE_TIME, KL_FIELD.CLOSE] ) for n, kl in enumerate(history_kline): if "klines" not in locals(): klines = kl else: klines = klines.append(kl, ignore_index=True) dts = list(set([t.split(" ")[0] for t in klines["time_key"]])) dts.sort() if len(dts) == 0: print(f"No data available for {code}") break if len(dts) > 1: for dt in dts[:-1]: df = klines[(klines["time_key"] >= f"{dt} 00:00:00") & (klines["time_key"] <= f"{dt} 23:59:59")] if not os.path.exists( f"{self.DATA_PATH}/k_line/{str(ktype)}/{code}"): os.mkdir( f"{self.DATA_PATH}/k_line/{str(ktype)}/{code}") df.to_csv( f"{self.DATA_PATH}/k_line/{str(ktype)}/{code}/{dt}.csv", index=False) print(f"{code}/{dt}.csv saved.") klines = klines[klines["time_key"] >= f"{dts[-1]} 00:00:00"] if not klines.empty: klines.to_csv( f"{self.DATA_PATH}/k_line/{str(ktype)}/{code}/{dts[-1]}.csv", index=False) print(f"{code}/{dts[-1]}.csv saved.") def is_hk_equity_market_time(self, cur_datetime: datetime) -> bool: """If current datetime is within market open time""" return (self.HK_EQUITY_AM_START <= cur_datetime.time() <= self.HK_EQUITY_AM_END or self.HK_EQUITY_PM_START <= cur_datetime.time() <= self.HK_EQUITY_PM_END) def next_hk_equity_market_time(self, cur_datetime: datetime) -> datetime: """Return next market open time""" if cur_datetime.time() < self.HK_EQUITY_AM_START: return datetime( cur_datetime.year, cur_datetime.month, cur_datetime.day, self.HK_EQUITY_AM_START.hour, self.HK_EQUITY_AM_START.minute, self.HK_EQUITY_AM_START.second, ) elif cur_datetime.time() < self.HK_EQUITY_PM_START: return datetime( cur_datetime.year, cur_datetime.month, cur_datetime.day, self.HK_EQUITY_PM_START.hour, self.HK_EQUITY_PM_START.minute, self.HK_EQUITY_PM_START.second, ) else: return None def get_stock_basicinfo(self) -> pd.DataFrame: """stock info""" ret_code, data = self.quote_ctx.get_stock_basicinfo( Market.HK, SecurityType.STOCK) if ret_code: print(f"Fail to get stock basicinfo: {data}") return return data