├── test ├── __init__.py ├── data │ └── BTCUSDT.pkl ├── config.py ├── data.py ├── 02-uTesting.py ├── test.py ├── testStratagy.py └── 01-fetchData.py ├── tests └── __init__.py ├── utils ├── __init__.py ├── global_vars.py ├── util.py └── logger.py ├── backtest ├── __init__.py └── backtest.py ├── engines ├── __init__.py ├── convertible_bonds.py ├── init_engine.py ├── warrant_engine.py ├── ding_engine.py ├── notice_engine.py ├── order_engine.py ├── screen_engine.py ├── data_engine.py └── trading_engine.py ├── strategies ├── __init__.py ├── strategies.py └── response.py ├── config └── config.ini ├── README.md ├── LICENSE ├── doc └── todo.md ├── main.py └── .gitignore /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backtest/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /engines/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /strategies/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/data/BTCUSDT.pkl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youzeliang/rquantify/HEAD/test/data/BTCUSDT.pkl -------------------------------------------------------------------------------- /test/config.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | from datetime import datetime 4 | 5 | TradePair = 'BTCUSDT' 6 | Interval = 86400 7 | DataStartTime = datetime.strptime("2023-01-01 00:00:00", "%Y-%m-%d %H:%M:%S") -------------------------------------------------------------------------------- /config/config.ini: -------------------------------------------------------------------------------- 1 | [FutuOpenD.Config] 2 | Host = 127.0.0.1 3 | Port = 11111 4 | 5 | 6 | [DingTalk] 7 | 8 | Secret = 9 | Webhook = 10 | AppSecret = 11 | 12 | 13 | 14 | [Order.Stock] 15 | CodeList = HK.HSImain,HK.800000,HK.00700,HK.03690,HK.09988 -------------------------------------------------------------------------------- /test/data.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | sys.path.append(BASE_DIR) 5 | 6 | from engines.data_engine import Data 7 | 8 | from pathlib import Path 9 | 10 | # Get the current directory where the script is located 11 | current_directory = Path(__file__).parent 12 | 13 | # Create the log directory at the same level as the "engines" directory 14 | download_directory = current_directory / "../downloads" 15 | download_directory.mkdir(parents=True, exist_ok=True) 16 | 17 | if __name__ == '__main__': 18 | data = Data() 19 | data.down_single_min_data('HK.00800', 6, download_directory) 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 想要一起写的联系我 3 | 4 | ### todo 5 | 6 | - [ ] 统计数据 7 | - [ ] 回测数据,先根据一些策略统计数据然后再写这一项 8 | - [ ] 金十数据监听 9 | 10 | ### 时间线 11 | 12 | 13 | 14 | #### 2025年06月21日 15 | 16 | - [x] 支持 @机器人时,服务收到命令后自定义处理逻辑 17 | 18 | 19 | #### 2024年11月09日 20 | 21 | - [x] 根据富途API下载美股成交量数据 22 | #### 2023年09月01日 23 | 24 | - [x] 主要经济数据 25 | - [x] 主要事件 26 | 27 | #### 2023年08月29日 28 | 29 | - [x] 可转债申购 30 | 31 | #### 2023年08月28日 32 | 33 | - [x] 接入钉钉群消息通知 34 | 35 | #### 2023年08月16日 36 | 37 | - [x] Linux 界面数据实时刷新,类似于在终端输入top然后就刷新股票代码的数据 38 | 39 | #### 2023年08月15日 40 | 41 | - [x] 富途数据下载 42 | - [x] 部分策略实现 43 | 44 | #### 2023年08月04日 45 | 46 | - [x] FutuApi端口监听 47 | - [x] 日志框架初始化完成 48 | 49 | #### 2023年08月01日 50 | 51 | - [x] 初始化框架 52 | -------------------------------------------------------------------------------- /strategies/strategies.py: -------------------------------------------------------------------------------- 1 | from datetime import date 2 | import time 3 | 4 | 5 | def valid_time(): 6 | """ 7 | 判断当前时间是否在交易时间内 todo 剔除掉节假日 8 | :return: 9 | """ 10 | str_today = str(date.today()) 11 | stamp = int(time.mktime(time.strptime(str_today + ' 09:30:00', '%Y-%m-%d %H:%M:%S'))) 12 | stamp_twelve = int(time.mktime(time.strptime(str_today + ' 11:59:00', '%Y-%m-%d %H:%M:%S'))) 13 | stamp_thirteen = int(time.mktime(time.strptime(str_today + ' 12:59:00', '%Y-%m-%d %H:%M:%S'))) 14 | stamp_fourteen = int(time.mktime(time.strptime(str_today + ' 16:00:00', '%Y-%m-%d %H:%M:%S'))) 15 | exit_flag = False 16 | now = int(time.time()) 17 | if stamp_fourteen <= now: 18 | exit_flag = True 19 | 20 | if now < stamp or (stamp_twelve <= now <= stamp_thirteen): 21 | return True, exit_flag 22 | return False, exit_flag 23 | 24 | 25 | class QuoteChange: 26 | 27 | def __init__(self): 28 | pass 29 | -------------------------------------------------------------------------------- /backtest/backtest.py: -------------------------------------------------------------------------------- 1 | import pandas as pd 2 | import numpy as np 3 | import talib 4 | import utils.util 5 | 6 | if __name__ == '__main__': 7 | file = "./恒指主连_HSImain_K_5M.csv" 8 | pd_reader = pd.read_csv(file) 9 | 10 | m = {} 11 | l = pd_reader['Time_key'].values.tolist() 12 | close_price = pd_reader['Close'].values.tolist() 13 | open_price = pd_reader['Open'].values.tolist() 14 | high_price = pd_reader['High'].values.tolist() 15 | low_price = pd_reader['Low'].values.tolist() 16 | 17 | upper, middle, lower = talib.BBANDS(np.array(close_price), timeperiod=20, matype=talib.MA_Type.EMA) 18 | 19 | for i in range(3, len(close_price)): 20 | if utils.util.trade_hk_time(l[i]): 21 | if close_price[i - 2] < open_price[i - 2] and close_price[i - 2] < lower[i - 2] and close_price[i - 1] < \ 22 | open_price[i - 1] and close_price[i - 1] < lower[i - 1] and close_price[i] < lower[i]: 23 | print(l[i]) 24 | -------------------------------------------------------------------------------- /engines/convertible_bonds.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import os, sys 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | sys.path.append(BASE_DIR) 7 | 8 | from utils import logger 9 | from ding_engine import Message 10 | 11 | 12 | class ConvertibleBonds: 13 | 14 | def __init__(self): 15 | self.message = Message() 16 | self.logger = logger.get_logger(log_dir=logger.LOG_FILE) 17 | 18 | def convertible_bonds(self): 19 | """ 20 | convertible_bonds 21 | :return: 22 | """ 23 | 24 | url = 'https://api.mrxiao.net/kzz' 25 | try: 26 | s = '' 27 | today_list = json.loads(requests.get(url).text)['today_start_kzz'] 28 | if len(today_list) > 0: 29 | for i in range(0, len(today_list)): 30 | s += ' ' + today_list[i]['SECURITY_NAME_ABBR'] 31 | self.message.send_ding_message(s, True) 32 | except Exception as e: 33 | self.logger.error('convertible_bonds-err', e) 34 | self.message.send_ding_message('可转债接口异常', True) 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 youzeliang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /engines/init_engine.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | 3 | from utils.global_vars import * 4 | 5 | from futu import * 6 | 7 | from trading_engine import FutuTrade 8 | 9 | 10 | class Init: 11 | def __init__(self): 12 | """ 13 | init some config and others 14 | """ 15 | self.config = config 16 | self.port = int(self.config['FutuOpenD.Config'].get('Port')) 17 | self.host = str(self.config['FutuOpenD.Config'].get('Host')) 18 | self.code_list = self.config['Order.Stock'].get('CodeList').split(',') 19 | 20 | # self.ignore_code = self.config['Order.Stock'].get('IgnoreCodeList').split(',') 21 | 22 | self.sub_type = [SubType.K_1M, SubType.K_5M, SubType.K_DAY] 23 | 24 | self.quote_ctx = OpenQuoteContext(host=self.host, 25 | port=self.port) 26 | self.trade_ctx = OpenSecTradeContext(filter_trdmarket=TrdMarket.HK, 27 | host=self.host, 28 | security_firm=SecurityFirm.FUTUSECURITIES) 29 | self.monitor = FutuTrade(self.quote_ctx, self.trade_ctx) 30 | 31 | self.monitor.kline_subscribe(self.code_list, self.sub_type) 32 | 33 | 34 | if __name__ == '__main__': 35 | pass 36 | -------------------------------------------------------------------------------- /utils/global_vars.py: -------------------------------------------------------------------------------- 1 | import configparser 2 | from futu import * 3 | from pathlib import Path 4 | 5 | PATH = Path.cwd() 6 | 7 | PATH_CONFIG = PATH.parent / 'config' 8 | 9 | print(PATH_CONFIG) 10 | config = configparser.ConfigParser() 11 | config.read( 12 | PATH_CONFIG / 'config.ini' if (PATH_CONFIG / 'config.ini').is_file() else PATH_CONFIG / 'config_template.ini') 13 | 14 | index_k = {0: 'K_1M', 1: 'K_3M', 2: 'K_5M', 3: 'K_15M', 4: 'K_30M', 5: 'K_60M', 6: 'K_DAY', 7: 'K_WEEK', 8: 'K_MON', 15 | 9: 'K_YEAR'} 16 | 17 | index_type = {0: KLType.K_1M, 1: KLType.K_3M, 2: KLType.K_5M, 3: KLType.K_15M, 4: KLType.K_30M, 5: KLType.K_60M, 18 | 6: KLType.K_DAY, 7: KLType.K_WEEK, 19 | 8: KLType.K_MON, 20 | 9: KLType.K_YEAR} 21 | 22 | futu_api = { 23 | '1': { 24 | 'cookie': 'xxxxxxx', 25 | 'quote-token': 'xxxxxxx'}, 26 | '0': { 27 | 'cookie': 'xxxx', 28 | 'quote-token': 'xxx'}, 29 | 30 | } 31 | 32 | ignore = [ 33 | '天然气', '芝加哥', '营建', 'ISM', '进口物价', '工业产出', '房产市场数据', '原油库存', '战略石油', '成品油', 34 | '经济状况褐皮书', '丽莎', '鲍曼', '经常帐', '费城', '商业', '房价指数', '里奇', '新屋', '谘商', '成屋', '贸易帐', 35 | '石油钻井', '社会用电量', '短期能源', '全球支付', '房产市场指数', '个人支出月率', '耐用品', '职位空缺', '批发销售', 36 | '威廉姆斯', '电量数据', '戴利', '博斯蒂克', '阿波利斯', '达拉斯联储', '明尼阿波利', '沃勒', '哈玛克', '库格勒', 37 | '穆萨莱姆', '国债竞拍', '杰斐逊', '零售销售', '巴尔'] 38 | -------------------------------------------------------------------------------- /doc/todo.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 基于富途证券开放的futuAPI接口,对恒生指数牛熊进行日内高频炒单的程序交易系统,包含实时筛选牛熊交易标的和交易策略两部分。交易策略有三块:入场,止盈和止损。入场和止损的信号主要来源于一分钟K线突破前一分钟k线的最高价和最低价,同时用收盘价、10均线和20均线组成的均线系统进行了过滤。止盈部分主要是卖二卖一排队卖,改单信号来源于当前一分钟k线的最高价、最低价和收盘价与5均线的比较。 4 | 5 | 6 | 7 | 一、牛熊筛选 8 | 9 | warrant_pool2(bear_pool1, bull_pool1) 筛选换股比率为10000、街货比小于50%的恒指牛熊、最新价格小于0.180, 记录下它们的收回价 10 | 11 | update_warrant_pool(bear_pool2, bear_rec_price, bull_pool2, bull_rec_price) 实时更新牛熊池, 恒指当月期货价格距离熊收回价小于-0.75%,距离牛收回价大于0.75% 12 | 13 | 14 | 二、入场 15 | 16 | avg_state_rank(data) 根据收盘价和一分钟10,20日均线值的大小,来评价走势的强弱,对信号进行过滤分类 17 | 18 | set_buy_trigger_range(data) 设置买牛熊触发值的范围(入场) 19 | 20 | buy_trigger_signal(data) 设置触发买牛熊的信号(入场) 21 | 22 | market_in(data, bear_candidate_list, bull_candidate_list) 当信号出现时,以买一价或者中间价排队买入 23 | 24 | chase_buy_change_order(data) 当排队买入未全部成交且往盈利方向变化超4格且小于10格时,立马以卖一价或者中间价买入 25 | 26 | 27 | 三、止盈 28 | 29 | compare_with_avg_line5(data) 每一根一分钟k线的最低价、最高价和中间价与一分钟k线的5日均线比较,用来评估近几分钟走势的强弱(止盈,排队卖) 30 | 31 | market_out(data) 包括止盈和止损两部分 32 | 33 | 34 | 35 | 四、止损 36 | 37 | avg_state_rank(data) 根据收盘价和一分钟10,20日均线值的大小,来评价走势的强弱,对信号进行过滤分类 38 | 39 | set_sell_trigger_range(data) 设置卖牛熊触发值的范围(止损) 40 | 41 | sell_trigger_signal(data) 设置触发卖牛熊的信号(止损) 42 | 43 | market_out(data) 包括止盈和止损两部分 44 | 45 | chase_sell_change_order(data) 当止损未全部成交且往亏损方向变化超5格时,立马以买一价格卖出 46 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import hmac 2 | import base64 3 | import os 4 | import sys 5 | from flask import Flask, request 6 | 7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | sys.path.append(BASE_DIR) 9 | 10 | from utils import global_vars 11 | 12 | from futu import * 13 | 14 | from strategies.response import OResponse 15 | 16 | app = Flask(__name__) 17 | 18 | 19 | @app.route('/', methods=['GET', 'POST']) 20 | def get_data(): 21 | if request.method == "POST": 22 | timestamp = request.headers.get('Timestamp') 23 | sign = request.headers.get('Sign') 24 | if check_sig(timestamp) == sign: 25 | req_data = json.loads(str(request.data, 'utf-8')) 26 | logger.get_logger(log_dir=logger.LOG_FILE).info(req_data) 27 | r = OResponse(req_data['text']['content'].strip(), req_data['sessionWebhook'], req_data['senderStaffId'], 28 | logger.get_logger(log_dir=logger.LOG_FILE) 29 | ) 30 | r.handle_text_info() 31 | return 's' 32 | 33 | 34 | def check_sig(timestamp): 35 | app_secret = str(global_vars.config['DingTalk'].get('AppSecret')) 36 | string_to_sign = '{}\n{}'.format(timestamp, app_secret) 37 | hmac_code = hmac.new(app_secret.encode('utf-8'), string_to_sign.encode('utf-8'), digestmod=hashlib.sha256).digest() 38 | sign = base64.b64encode(hmac_code).decode('utf-8') 39 | return sign 40 | 41 | 42 | if __name__ == '__main__': 43 | app.run(host='0.0.0.0', port=8088) 44 | -------------------------------------------------------------------------------- /strategies/response.py: -------------------------------------------------------------------------------- 1 | from futu import * 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | sys.path.append(BASE_DIR) 5 | 6 | import requests 7 | 8 | t = time.localtime() 9 | d = t.tm_mday 10 | h = t.tm_hour 11 | m = t.tm_min 12 | 13 | import pandas as pd 14 | 15 | pd.set_option('display.max_rows', 500) 16 | pd.set_option('display.max_columns', 500) 17 | pd.set_option('display.width', 1000) 18 | 19 | 20 | class OResponse: 21 | 22 | def __init__(self, text_info, webhook_url, sender_id, s_logger): 23 | self.webhook_url = webhook_url 24 | self.sender_id = sender_id 25 | self.default_logger = s_logger 26 | self.text_info = text_info 27 | 28 | def send_msg(self, userid, message, webhook_url): 29 | data = { 30 | "msgtype": "text", 31 | "text": { 32 | "content": message 33 | }, 34 | "at": { 35 | "atUserIds": [ 36 | userid 37 | ] 38 | } 39 | } 40 | if message == '': 41 | self.default_logger.info('message没有数据') 42 | return 43 | if platform.system() == 'Darwin': 44 | print(message) 45 | return 46 | req = requests.post(webhook_url, json=data) 47 | if req.status_code != 200: 48 | self.default_logger.error(req.text) 49 | 50 | def handle_text_info(self): 51 | pass 52 | # todo handle self.text_info 53 | 54 | 55 | if __name__ == '__main__': 56 | pass 57 | -------------------------------------------------------------------------------- /test/02-uTesting.py: -------------------------------------------------------------------------------- 1 | import os 2 | from datetime import datetime, timedelta 3 | 4 | import pandas as pd 5 | import backtrader as bt 6 | 7 | from testStratagy import VerifyStratagy 8 | import config 9 | 10 | 11 | def loadDataFrameFromPklFile(file_name, folder_abs_path=None): 12 | folder_abs_path = folder_abs_path if folder_abs_path else PandasConfig.APP_DATA_PATH() 13 | filepath = os.path.join(folder_abs_path, file_name) 14 | return pd.read_feather(filepath) 15 | 16 | 17 | if __name__ == '__main__': 18 | cerebro = bt.Cerebro(stdstats=False) 19 | 20 | cerebro.addstrategy(VerifyStratagy) 21 | 22 | cerebro.adddata(bt.feeds.PandasData( 23 | dataname=loadDataFrameFromPklFile(f'{config.TradePair}.pkl', folder_abs_path='./data'), 24 | fromdate=datetime(2023, 1, 1, 0, 0, 0), 25 | todate=datetime(2023, 9, 1, 0, 0, 0), 26 | datetime='open_time', 27 | open='open', 28 | high='high', 29 | low='low', 30 | close='close', 31 | volume='volume', 32 | openinterest=-1 33 | ), name='BTCUSDT') 34 | 35 | cerebro.broker.setcommission(commission=0.0002) 36 | cerebro.addsizer(bt.sizers.FixedSize, stake=1) 37 | cerebro.broker.setcash(100000) 38 | 39 | cerebro.addobserver(bt.observers.Trades) 40 | cerebro.addobserver(bt.observers.BuySell) 41 | cerebro.addobserver(bt.observers.DrawDown) 42 | cerebro.addobserver(bt.observers.Value) 43 | cerebro.addobserver(bt.observers.TimeReturn) 44 | 45 | print(cerebro.broker.getvalue()) 46 | cerebro.run() 47 | print(cerebro.broker.getvalue()) 48 | 49 | cerebro.plot(style='candle') -------------------------------------------------------------------------------- /utils/util.py: -------------------------------------------------------------------------------- 1 | import time 2 | import datetime 3 | from datetime import datetime 4 | 5 | 6 | def warrant_vol(): 7 | x = minutes_passed() 8 | if x < 100: 9 | return int(x ** 2 * 2400) 10 | else: 11 | return int(100 ** 2 * 300 + x * 20000) 12 | 13 | 14 | def minutes_passed(): 15 | current_time = datetime.now() 16 | target_time = current_time.replace(hour=9, minute=30, second=0, microsecond=0) 17 | return int((current_time - target_time).total_seconds() / 60) 18 | 19 | 20 | def str_of_num(num): 21 | """ 22 | 成交量转化 23 | :param num: 24 | :return: 25 | """ 26 | 27 | def strofsize(num, level): 28 | if level >= 2: 29 | return num, level 30 | elif num >= 10000: 31 | num /= 10000 32 | level += 1 33 | return strofsize(num, level) 34 | else: 35 | return num, level 36 | 37 | units = ['', '万', '亿'] 38 | num, level = strofsize(num, 0) 39 | if level > len(units): 40 | level -= 1 41 | return '{}{}'.format(round(num, 2), units[level]) 42 | 43 | 44 | def trade_hk_time(l): 45 | """ 46 | 回撤时判断是否在港股交易时间 47 | :param l: 48 | :return: 49 | """ 50 | today = l[:10] 51 | nine_stamp = int(time.mktime(time.strptime(today + ' 09:30:00', '%Y-%m-%d %H:%M:%S'))) 52 | sixteen_stamp = int(time.mktime(time.strptime(today + ' 16:00:00', '%Y-%m-%d %H:%M:%S'))) 53 | now_stamp = int(time.mktime(time.strptime(l, '%Y-%m-%d %H:%M:%S'))) 54 | if now_stamp < nine_stamp or now_stamp > sixteen_stamp: 55 | return False 56 | return True 57 | 58 | 59 | def day_time(): 60 | t = time.localtime() 61 | n = t.tm_hour 62 | 63 | if 8 <= n <= 22: 64 | return True 65 | return False 66 | 67 | 68 | def working_day(): 69 | t = time.localtime() 70 | 71 | n = t.tm_wday 72 | if 0 <= n <= 4: 73 | return True 74 | return False 75 | -------------------------------------------------------------------------------- /engines/warrant_engine.py: -------------------------------------------------------------------------------- 1 | from futu import * 2 | 3 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 4 | sys.path.append(BASE_DIR) 5 | 6 | from utils.global_vars import * 7 | import utils.util 8 | 9 | 10 | class Warrant: 11 | 12 | def __init__(self, quote_ctx: OpenQuoteContext): 13 | """ 14 | Futu Warrant Engine Constructor 15 | :param quote_ctx: 16 | """ 17 | self.config = config 18 | self.quote_ctx = quote_ctx 19 | self.logger = logger.get_logger(log_dir=logger.LOG_FILE) 20 | 21 | def get_warrant(self, stock_code, code_type, vol_min=utils.util.warrant_vol(), conversion_max=10000, 22 | conversion_min=10000, num=50, 23 | cur_price_min=0.07, 24 | cur_price_max=0.2): 25 | req = WarrantRequest() 26 | req.status = WarrantStatus.NORMAL 27 | req.sort_field = SortField.VOLUME 28 | req.issuer_list = ["SG", "BP", "CS", "JP", "UB"] 29 | req.ascend = False 30 | req.street_max = 35 31 | req.conversion_max = conversion_max 32 | req.conversion_min = conversion_min 33 | req.leverage_ratio_min = 10 34 | req.vol_min = vol_min 35 | req.num = num 36 | req.cur_price_min = cur_price_min 37 | req.cur_price_max = cur_price_max 38 | 39 | if code_type == 'bear': 40 | req.type_list = WrtType.BEAR 41 | elif code_type == 'all': 42 | req.type_list = [WrtType.BEAR, WrtType.BULL] 43 | else: 44 | req.type_list = WrtType.BULL 45 | 46 | ret, ls = self.quote_ctx.get_warrant(stock_code, req) 47 | if ret == RET_OK: 48 | warrant_data_list, last_page, all_count = ls 49 | if len(warrant_data_list) == 0: 50 | self.logger.error('富途暂未返回数据') 51 | return 52 | return warrant_data_list 53 | self.logger.error(ret) 54 | return [] 55 | -------------------------------------------------------------------------------- /engines/ding_engine.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import sys 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | sys.path.append(BASE_DIR) 7 | 8 | import base64 9 | import hmac 10 | import urllib.parse 11 | import requests 12 | from utils import logger 13 | from utils.global_vars import * 14 | 15 | 16 | class Message: 17 | def __init__(self, secret=config['DingTalk'].get('Secret'), webhook=config['DingTalk'].get('Webhook')): 18 | """" 19 | send the message to DingTalk Group 20 | see the detail https://open.dingtalk.com/document/robots/robot-overview 21 | """ 22 | 23 | self.logger = logger.get_logger(log_dir=logger.LOG_FILE) 24 | 25 | self.secret = secret 26 | self.webhook = webhook 27 | 28 | def sign(self): 29 | timestamp = str(round(time.time() * 1000)) 30 | secret = self.secret 31 | secret_enc = secret.encode('utf-8') 32 | string_to_sign = '{}\n{}'.format(timestamp, secret) 33 | string_to_sign_enc = string_to_sign.encode('utf-8') 34 | hmac_code = hmac.new(secret_enc, string_to_sign_enc, digestmod=hashlib.sha256).digest() 35 | sign_res = urllib.parse.quote_plus(base64.b64encode(hmac_code)) 36 | return timestamp, sign_res 37 | 38 | def send_ding_message(self, text_info, flag): 39 | webhook = self.webhook 40 | header = { 41 | "Content-Type": "application/json", 42 | "Charset": "UTF-8" 43 | } 44 | text = text_info 45 | message = { 46 | "msgtype": "text", 47 | "text": { 48 | "content": text 49 | }, 50 | "at": { 51 | "isAtAll": flag 52 | } 53 | } 54 | timestamp, sign_res = self.sign() 55 | webhook += "×tamp=" + timestamp + "&sign=" + sign_res 56 | info = requests.post(url=webhook, data=json.dumps(message), headers=header) 57 | if json.loads(info.text).get('errcode') != 0: 58 | self.logger.error('send DingTalk Group message err.%s', json.loads(info.text)) 59 | 60 | 61 | if __name__ == '__main__': 62 | m = Message() 63 | m.send_ding_message('fdfs', True) 64 | -------------------------------------------------------------------------------- /utils/logger.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import sys 4 | from logging.handlers import TimedRotatingFileHandler 5 | from pathlib import Path 6 | 7 | FORMATTER = logging.Formatter("%(asctime)s — %(name)s — %(levelname)s — %(message)s") 8 | 9 | # Get the current directory where the script is located 10 | current_directory = Path(__file__).parent 11 | 12 | # Create the log directory at the same level as the "engines" directory 13 | log_directory = current_directory / "../log" 14 | log_directory.mkdir(parents=True, exist_ok=True) 15 | 16 | # Use the current date to construct the log file name 17 | LOG_FILE = log_directory / f"{str(datetime.date.today())}.log" 18 | ERROR_LOG_FILE = log_directory / f"{str(datetime.date.today())}.log.error" 19 | 20 | 21 | def get_console_handler(): 22 | console_handler = logging.StreamHandler(sys.stdout) 23 | console_handler.setFormatter(FORMATTER) 24 | return console_handler 25 | 26 | 27 | def get_file_handler(log_file): 28 | file_handler = TimedRotatingFileHandler(log_file, when='midnight') 29 | file_handler.setFormatter(FORMATTER) 30 | return file_handler 31 | 32 | 33 | def get_logger(logger_name="default_logger", log_dir=None): 34 | logger = logging.getLogger(logger_name) 35 | logger.setLevel(logging.INFO) # better to have too much log than not enough 36 | logger.addHandler(get_console_handler()) 37 | 38 | if log_dir is None: 39 | # Use default LOG_FILE 40 | logger.addHandler(get_file_handler(LOG_FILE)) 41 | else: 42 | # Use the provided log_dir with the current date as part of the file name 43 | log_file = log_directory / f"{log_dir}" 44 | logger.addHandler(get_file_handler(log_file)) 45 | 46 | # with this pattern, it's rarely necessary to propagate the error up to parent 47 | logger.propagate = False 48 | return logger 49 | 50 | 51 | # class Logger: 52 | # _instance = None 53 | # _logger = None 54 | # 55 | # def __new__(cls, *args, **kwargs): 56 | # if not cls._instance: 57 | # cls._instance = super(Logger, cls).__new__(cls, *args, **kwargs) 58 | # cls._instance._logger = get_logger() 59 | # return cls._instance 60 | # 61 | # @property 62 | # def logger(self): 63 | # return self._logger 64 | -------------------------------------------------------------------------------- /engines/notice_engine.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import socket 3 | import requests 4 | import time 5 | import datetime 6 | 7 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 8 | sys.path.append(BASE_DIR) 9 | 10 | from utils import logger 11 | from utils.global_vars import * 12 | 13 | from engines.init_engine import Init 14 | 15 | 16 | class Notice: 17 | 18 | def __init__(self): 19 | self.init = Init() 20 | self.logger = logger.get_logger(log_dir=logger.LOG_FILE) 21 | 22 | def check_port(self): 23 | """ 24 | Check if the Futu port is started up correctly. 25 | 26 | :return: 27 | """ 28 | try: 29 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 30 | s.connect((self.init.host, self.init.port)) 31 | s.shutdown(2) 32 | self.logger.info("port is open") 33 | return True 34 | except Exception as e: 35 | logger.get_logger(log_dir=logger.ERROR_LOG_FILE).error(e) 36 | return False 37 | 38 | def economic_data(self): 39 | current_date = datetime.datetime.now() 40 | formatted_date = current_date.strftime('%m/%d') 41 | url = f'https://cdn-rili.jin10.com/web_data/{current_date.year}/daily/{formatted_date}/economics.json?t=1693451525225' 42 | response = requests.get(url) 43 | data = response.json() 44 | for i in data: 45 | if i['country'] in ['中国', '美国'] or int(i['star']) >= 3: 46 | continue 47 | 48 | flag = False 49 | for x in ignore: 50 | if x in str(i['name']): 51 | flag = True 52 | break 53 | if flag: 54 | continue 55 | 56 | current_timestamp = int(time.time()) 57 | if i['pub_time_unix'] > current_timestamp: 58 | continue 59 | 60 | s = i['country'] + ' ' + i['name'] + ' ' + ' 前值:' + str(i['previous']) + ' ' + ' 预期值: ' + str( 61 | i['consensus']) + ' 实际值: ' + str(i['actual']) + ' ' + str(i['star']) + '星' 62 | 63 | return s 64 | 65 | def event(self): 66 | current_date = datetime.datetime.now() 67 | formatted_date = current_date.strftime('%m/%d') 68 | url = f'https://cdn-rili.jin10.com/web_data/{current_date.year}/daily/{formatted_date}/event.json' 69 | response = requests.get(url) 70 | if response.status_code == 200: 71 | data = response.json() 72 | for i in data: 73 | if i['country'] not in ['中国', '美国'] or i['star'] <= 2: 74 | continue 75 | 76 | print(i['event_content']) 77 | 78 | else: 79 | print(f"Request failed with status code: {response.status_code}") 80 | -------------------------------------------------------------------------------- /test/test.py: -------------------------------------------------------------------------------- 1 | 2 | import os, time, itertools 3 | from datetime import datetime, timedelta 4 | import logging 5 | import pandas as pd 6 | 7 | pd.set_option('display.max_rows', None) 8 | pd.set_option('display.min_rows', None) 9 | pd.set_option('display.max_columns', 100) 10 | pd.set_option('display.width', 1000) 11 | pd.set_option('display.max_colwidth', 1000) 12 | pd.set_option('display.unicode.ambiguous_as_wide', True) 13 | pd.set_option('display.unicode.east_asian_width', True) 14 | pd.set_option('expand_frame_repr', False) 15 | 16 | 17 | import config 18 | 19 | def find_peaks(arr, time_list): 20 | """ 21 | 寻找波峰 22 | :param arr: 23 | :param time_list: 24 | :return: 25 | """ 26 | peaks = [] 27 | t = [] 28 | if len(arr) == 0: 29 | return [], [] 30 | min_value = min(arr) 31 | last_value = min(arr) 32 | 33 | for i in range(2, len(arr) - 2): 34 | if arr[i] > arr[i - 1] and arr[i] > arr[i + 1] and arr[i] > arr[i + 2] and arr[i] > arr[i - 2] and arr[i] >= last_value: 35 | 36 | if len(peaks) >= 2 and i - peaks[-1] <= 3: 37 | peaks[-1] = i 38 | continue 39 | peaks.append(i) 40 | last_value = arr[i] 41 | t.append(time_list[i]) 42 | if arr[i] == min_value: 43 | last_value = min(arr) 44 | t = [] 45 | peaks = [] 46 | 47 | return [arr[i] for i in peaks], t 48 | 49 | 50 | def find_valleys(arr, time_list): 51 | """ 52 | 寻找波谷 53 | :param arr: 54 | :param time_list: 55 | :return: 56 | """ 57 | valleys = [] 58 | t = [] 59 | if len(arr) == 0: 60 | return [], [] 61 | max_value = max(arr) 62 | last_value = max(arr) 63 | 64 | for i in range(2, len(arr) - 2): 65 | if arr[i] < arr[i - 1] and arr[i] < arr[i + 1] and arr[i] < arr[i + 2] and arr[i] < arr[i - 2] and arr[i] <= last_value: 66 | 67 | if len(valleys) >= 2 and i - valleys[-1] <= 3: 68 | valleys[-1] = i 69 | continue 70 | valleys.append(i) 71 | last_value = arr[i] 72 | t.append(time_list[i]) 73 | if arr[i] == max_value: 74 | last_value = max(arr) 75 | t = [] 76 | valleys = [] 77 | 78 | return [arr[i] for i in valleys], t 79 | 80 | def loadDataFrameFromPklFile(file_name, folder_abs_path=None): 81 | folder_abs_path = folder_abs_path if folder_abs_path else PandasConfig.APP_DATA_PATH() 82 | filepath = os.path.join(folder_abs_path, file_name) 83 | return pd.read_feather(filepath) 84 | 85 | 86 | data = loadDataFrameFromPklFile(f'{config.TradePair}.pkl', folder_abs_path='./data') 87 | 88 | 89 | print(data) 90 | 91 | 92 | 93 | # result = pd.DataFrame(find_peaks(data['close'], data['open_time'])) 94 | result = pd.DataFrame(find_valleys(data['close'], data['open_time'])) 95 | print(result) -------------------------------------------------------------------------------- /engines/order_engine.py: -------------------------------------------------------------------------------- 1 | import json, requests, time 2 | import json 3 | import requests 4 | 5 | from utils import logger 6 | from utils.global_vars import * 7 | 8 | order_url = 'http://127.0.0.1:12344/order/insert' 9 | position_url = 'http://127.0.0.1:12344/query/position' 10 | account_url = 'http://127.0.0.1:12344/query/account' 11 | queue_order_url = 'http://127.0.0.1:12344/query/order' 12 | trade_url = 'http://127.0.0.1:12344/query/trade' 13 | cancel_url = 'http://127.0.0.1:12344/order/cancel' 14 | modify_url = 'http://127.0.0.1:12344/order/modify' 15 | 16 | header = { 17 | "Content-Type": "application/json", 18 | "Charset": "UTF-8" 19 | } 20 | 21 | 22 | class OrderEngine: 23 | 24 | def __init__(self, quote_ctx: OpenQuoteContext, trade_ctx: OpenSecTradeContext): 25 | self.default_logger = logger.Logger().logger 26 | self.quote_ctx = quote_ctx 27 | self.trade_ctx = trade_ctx 28 | self.trd_env = TrdEnv.REAL 29 | 30 | def get_order_list(self): 31 | """ 32 | 查询多日订单,默认当日 33 | :return: 34 | """ 35 | # https://openapi.futunn.com/futu-api-doc/trade/get-order-list.html,每3s 才能请求一次 36 | ret_code, order_list_data = self.trade_ctx.order_list_query(order_id="", trd_env=self.trd_env, 37 | start=time.strftime("%Y-%m-%d", time.localtime()), 38 | refresh_cache=False) 39 | 40 | if ret_code != RET_OK: 41 | self.default_logger.error(f"Cannot acquire order list {order_list_data}") 42 | return None 43 | return order_list_data 44 | 45 | def place_order(self, code: str, side: str, exchange: str, type: str, price: str, qty: int): 46 | """ 47 | :param code: 48 | :param qty: 49 | """ 50 | data = { 51 | "clord_id": 'api_' + str(int(time.time())), 52 | "exchange": exchange, 53 | "symbol": code, 54 | "side": side, 55 | "open_close": "", 56 | "qty": qty, 57 | "type": type, 58 | "price": price, 59 | "tif": "DAY", 60 | "allow_eth": "YES" 61 | } 62 | 63 | res = json.loads(requests.post(order_url, headers=header, json=data).text) 64 | error_id = res['error_id'] 65 | error_msg = res['error_msg'] 66 | if error_id != '0' or error_msg != '': 67 | return 68 | 69 | order_id = res['order_id'] 70 | 71 | return order_id 72 | 73 | def z_cancel(self, order_id: str): 74 | """ 75 | 取消订单 76 | :param order_id: 77 | :return: 78 | """ 79 | data = { 80 | "order_id": order_id 81 | } 82 | 83 | try: 84 | res = json.loads(requests.post(cancel_url, headers=header, json=data).text) 85 | error_id = res['error_id'] 86 | error_msg = res['error_msg'] 87 | if error_id != '0' or error_msg != '': 88 | self.logger.error(error_msg) 89 | except Exception as e: 90 | self.logger.error(e) 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 158 | # and can be added to the global gitignore or merged into this file. For a more nuclear 159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 160 | .idea/ 161 | -------------------------------------------------------------------------------- /engines/screen_engine.py: -------------------------------------------------------------------------------- 1 | import curses 2 | import os 3 | import sys 4 | 5 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 6 | sys.path.append(BASE_DIR) 7 | from utils.global_vars import * 8 | from trading_engine import FutuTrade 9 | 10 | 11 | class StdScr: 12 | def __init__(self, stdscr): 13 | self.config = config 14 | self.code_list = self.config['Order.Stock'].get('CodeList').split(',') 15 | self.stdscr = stdscr 16 | self.config = config 17 | self.sub_type = [SubType.K_1M, SubType.K_5M, SubType.K_DAY] 18 | self.port = int(self.config['FutuOpenD.Config'].get('Port')) 19 | self.host = str(self.config['FutuOpenD.Config'].get('Host')) 20 | self.quote_ctx = OpenQuoteContext(host=self.host, 21 | port=self.port) 22 | self.trade_ctx = OpenSecTradeContext(filter_trdmarket=TrdMarket.HK, 23 | host=self.host, 24 | security_firm=SecurityFirm.FUTUSECURITIES) 25 | self.trading = FutuTrade(self.quote_ctx, self.trade_ctx) 26 | self.trading.kline_subscribe(self.code_list, self.sub_type) 27 | 28 | def get_stock(self, stocks, yesterday): 29 | s = [] 30 | i = 0 31 | for stock_code in stocks: 32 | data = self.trading.get_history_kline(stock_code, KLType.K_DAY, start_date=yesterday, 33 | end_date=yesterday) 34 | yesterday_close = data['close'][0] 35 | re, cur_data = self.trading.quote_ctx.get_cur_kline(stock_code, 1, KLType.K_DAY, AuType.QFQ) 36 | today_close, today_high, today_low = cur_data['close'][0], cur_data['high'][0], cur_data['low'][0] 37 | amplitude = str(round((today_high - today_low) / today_low * 100, 2)) + '% ' 38 | m = 'down ' + str(round((yesterday_close - today_close) / today_close * 100, 2)) + '%' 39 | if today_close >= yesterday_close: 40 | m = 'up ' + str(round((today_close - yesterday_close) / yesterday_close * 100, 2)) + '%' 41 | 42 | temp = [i, stock_code, str(today_close), m, str(today_high), str(today_low), str(amplitude)] 43 | s.append(temp) 44 | i += 1 45 | return s 46 | 47 | def run(self): 48 | curses.curs_set(0) 49 | max_y, max_x = self.stdscr.getmaxyx() 50 | header = ["No", "stock", "close", "change_rate", "high", "low", "amplitude"] 51 | num_cols = len(header) 52 | col_width = max_x // num_cols 53 | 54 | trade_res = self.trading.request_trading_days() 55 | if len(trade_res) <= 3: 56 | return 57 | yesterday = trade_res[-2] 58 | while True: 59 | time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 60 | self.stdscr.clear() 61 | self.stdscr.addstr(0, 0, "Fixed text that will not change") 62 | data = self.get_stock(self.code_list, yesterday) 63 | for i, col_name in enumerate(header): 64 | self.stdscr.addstr(2, i * col_width, col_name.center(col_width)) 65 | 66 | for row_num, row_data in enumerate(data): 67 | for col_num, cell_data in enumerate(row_data): 68 | self.stdscr.addstr( 69 | row_num + 3, 70 | col_num * col_width, 71 | str(cell_data).center(col_width) 72 | ) 73 | self.stdscr.addstr(8, 0, "Current time is: {}".format(time_str)) 74 | self.stdscr.refresh() 75 | time.sleep(2) 76 | key = self.stdscr.getch() 77 | if key == ord('q'): 78 | break 79 | curses.endwin() 80 | 81 | 82 | def main(stdscr): 83 | my_app = StdScr(stdscr) 84 | my_app.run() 85 | 86 | 87 | if __name__ == '__main__': 88 | curses.wrapper(main) 89 | -------------------------------------------------------------------------------- /test/testStratagy.py: -------------------------------------------------------------------------------- 1 | 2 | import backtrader 3 | 4 | 5 | 6 | 7 | 8 | class VerifyStratagy(backtrader.Strategy): 9 | params = dict( pivot_left = 2, 10 | pivot_right = 2, 11 | PVs_len_threshold = 3, 12 | PLOT_SWITCH = False, 13 | RECORD_TO_FILE = False) 14 | 15 | def log(self, txt, dt=None, log_level='DEBUG', doprint=True): 16 | ''' Logging function for this strategy''' 17 | dt = dt or self.datas[0].datetime.datetime(0) 18 | if doprint: 19 | print(f'[{dt.isoformat()}]: {txt}') 20 | 21 | def __init__(self): 22 | self.peakarr_sliding_window = [] 23 | self.valleyarr_sliding_window = [] 24 | 25 | def find_valleys(self): 26 | valleys = [] 27 | if len(self.valleyarr_sliding_window) == 0: 28 | return 0 29 | max_value = max(self.valleyarr_sliding_window) 30 | last_value = max(self.valleyarr_sliding_window) 31 | 32 | for i in range(self.params.pivot_left, len(self.valleyarr_sliding_window) - self.params.pivot_right): 33 | valley_hit = True 34 | for e in range(1, self.params.pivot_left + 1): 35 | if self.valleyarr_sliding_window[i] > self.valleyarr_sliding_window[i-e]: valley_hit = False 36 | for e in range(1, self.params.pivot_right + 1): 37 | if self.valleyarr_sliding_window[i] > self.valleyarr_sliding_window[i+e]: valley_hit = False 38 | if self.valleyarr_sliding_window[i] > last_value: valley_hit = False 39 | 40 | if valley_hit: 41 | if len(valleys) >= 2 and i - valleys[-1] <= 3: 42 | valleys[-1] = i 43 | continue 44 | valleys.append(i) 45 | last_value = self.valleyarr_sliding_window[i] 46 | if self.valleyarr_sliding_window[i] == max_value: 47 | last_value = max(self.valleyarr_sliding_window) 48 | valleys = [] 49 | return len(valleys) 50 | 51 | 52 | def find_peaks(self): 53 | peaks = [] 54 | if len(self.peakarr_sliding_window) == 0: 55 | return 0 56 | min_value = min(self.peakarr_sliding_window) 57 | last_value = min(self.peakarr_sliding_window) 58 | 59 | for i in range(self.params.pivot_left, len(self.peakarr_sliding_window) - self.params.pivot_right): 60 | peak_hit = True 61 | for e in range(1, self.params.pivot_left + 1): 62 | if self.peakarr_sliding_window[i] < self.peakarr_sliding_window[i-e]: peak_hit = False 63 | for e in range(1, self.params.pivot_right + 1): 64 | if self.peakarr_sliding_window[i] < self.peakarr_sliding_window[i+e]: peak_hit = False 65 | if self.peakarr_sliding_window[i] < last_value: peak_hit = False 66 | 67 | if peak_hit: 68 | if len(peaks) >= 2 and i - peaks[-1] <= 3: 69 | peaks[-1] = i 70 | continue 71 | peaks.append(i) 72 | last_value = self.peakarr_sliding_window[i] 73 | if self.peakarr_sliding_window[i] == min_value: 74 | last_value = min(self.peakarr_sliding_window) 75 | peaks = [] 76 | 77 | return len(peaks) 78 | 79 | 80 | def next(self): 81 | 82 | data = self.getdatabyname('BTCUSDT') 83 | self.peakarr_sliding_window.append(data.close[0]) 84 | self.valleyarr_sliding_window.append(data.close[0]) 85 | 86 | self.log(f'{type(self).__name__}.nextBar: Time:[{data.datetime.datetime(0)}], Current position: {self.getposition().size}; dataclose[0]=[{data.close[0]}], len(self.peakarr_sliding_window)=[{len(self.peakarr_sliding_window)}], len(self.valleyarr_sliding_window)=[{len(self.valleyarr_sliding_window)}].', log_level='INFO') 87 | 88 | 89 | if self.find_peaks() >= self.params.PVs_len_threshold: 90 | self.sell() 91 | self.peakarr_sliding_window = [] 92 | self.log(f'Peak found at Time:[{data.datetime.datetime(0)}].') 93 | 94 | 95 | if self.find_valleys() >= self.params.PVs_len_threshold: 96 | self.buy() 97 | self.valleyarr_sliding_window = [] 98 | self.log(f'Valley found at Time:[{data.datetime.datetime(0)}].') 99 | 100 | -------------------------------------------------------------------------------- /test/01-fetchData.py: -------------------------------------------------------------------------------- 1 | # move this demo file to your application root path. 2 | # eg. from python_library.utils.appUtils import AppUtils 3 | 4 | import os, time, itertools 5 | from datetime import datetime, timedelta 6 | import logging 7 | import pandas as pd 8 | 9 | import ccxt 10 | 11 | import config 12 | 13 | INTERVAL_MAP = { 14 | 86400: '1d', 15 | } 16 | 17 | """ 18 | 获取期货k线OHLCV数据 19 | Parameters: 20 | self myExchange(Public) 21 | underlying - str - underlying 22 | base_asset - str - base_asset 23 | start_time - datetime - 查询起始时间 24 | interval - ETimeConstant - 间隔 25 | sample_size - int - 样本容量 26 | Returns: 27 | exchange - str - binance 28 | open_time - datetime64[ns] - k线开始时间(UTC时间) 29 | interval - int - base_asset 30 | start_time - datetime - 间隔 31 | open - float - open 32 | high - float - high 33 | low - float - low 34 | close - float - close 35 | volume - float - volume 36 | Raises: 37 | (ccxt) 38 | """ 39 | def getFutureOHLCVSample(exchange, trade_pair: str, start_time: datetime, interval: int, sample_size: int): 40 | 41 | future_kline_sample = pd.DataFrame() 42 | while future_kline_sample.shape[0] < sample_size: 43 | REQ_LIMIT = 1500 44 | req_start_time = start_time + timedelta(weeks=0, days=0, hours=0, minutes=0, seconds=interval * future_kline_sample.shape[0], microseconds=0, milliseconds=0) 45 | if req_start_time > datetime.now(): break 46 | params = { 47 | 'pair': trade_pair, 48 | 'contractType': 'PERPETUAL', 49 | 'interval': INTERVAL_MAP[interval], 50 | 'startTime': int(time.mktime(req_start_time.timetuple())) * 1000, 51 | 'limit': min(REQ_LIMIT, sample_size - future_kline_sample.shape[0]) 52 | } 53 | data = exchange.fapiPublicGetContinuousKlines(params=params) 54 | data = pd.DataFrame(data, columns=['open_time', 'open', 'high', 'low', 'close', 'volume', 'close_time', 'quote_asset_volume', 'number_of_trades', 'taker_buy_volume', 'taker_buy_quote_asset_volume', 'ignore']) 55 | 56 | data['interval'] = interval 57 | data['open_time'] = pd.to_datetime(data['open_time'], unit='ms') 58 | data[['open']] = data[['open']].astype(float) 59 | data[['high']] = data[['high']].astype(float) 60 | data[['low']] = data[['low']].astype(float) 61 | data[['close']] = data[['close']].astype(float) 62 | data[['volume']] = data[['volume']].astype(float) 63 | 64 | data = data[['open_time', 'interval', 'open', 'high', 'low', 'close', 'volume']] 65 | 66 | # 合并统计数据 67 | future_kline_sample_lines = future_kline_sample.shape[0] 68 | future_kline_sample = pd.concat(objs=[future_kline_sample, data]) 69 | future_kline_sample.drop_duplicates(['open_time'], inplace=True) 70 | # assert future_kline_sample_lines < future_kline_sample.shape[0] 71 | 72 | # future_kline_sample['exchange'] = type(self).EXCHANGE_NAME 73 | return future_kline_sample 74 | 75 | 76 | 77 | def appendDataFrameToLocalFile(file_name, append_data, folder_abs_path=None, sort_values=[], drop_duplicates=[]): 78 | folder_abs_path = folder_abs_path if folder_abs_path else PandasConfig.APP_DATA_PATH() 79 | if not os.path.exists(folder_abs_path): os.mkdir(folder_abs_path) 80 | filepath = os.path.join(folder_abs_path, file_name) 81 | # logger.debug(f"Start append DataFrame to file [{filepath}]. append_data.shape=[{append_data.shape}], sort_values=[{sort_values}], drop_duplicates=[{drop_duplicates}]...") 82 | 83 | if not os.path.exists(filepath): 84 | append_data.to_feather(filepath) 85 | # logger.debug(f"just created file [{filepath}]...") 86 | else: 87 | data = pd.read_feather(filepath) 88 | data = pd.concat([data, append_data]) 89 | append_data.reset_index(inplace=True) 90 | if len(drop_duplicates) > 0: 91 | data.drop_duplicates(drop_duplicates, inplace=True) 92 | if len(sort_values) > 0: 93 | data.sort_values(sort_values, inplace=True) 94 | data.reset_index(drop=True, inplace=True) 95 | data.to_feather(filepath) 96 | # logger.debug(f"[{filepath}] already exist, update data done, file_data.shape=[{data.shape}]...") 97 | 98 | # logger.info(f"append DataFrame to file [{filepath}] Done. sort_values=[{sort_values}], drop_duplicates=[{drop_duplicates}], append_data.shape=[{append_data.shape}]") 99 | 100 | 101 | 102 | if __name__ == '__main__': 103 | 104 | klines = getFutureOHLCVSample(exchange=ccxt.binance(), trade_pair=config.TradePair, start_time=config.DataStartTime, interval=config.Interval, sample_size=400) 105 | 106 | appendDataFrameToLocalFile(f'{config.TradePair}.pkl', klines, drop_duplicates=['open_time'], sort_values=['open_time'], folder_abs_path='./data') 107 | -------------------------------------------------------------------------------- /engines/data_engine.py: -------------------------------------------------------------------------------- 1 | import csv 2 | import os 3 | import sys 4 | import requests 5 | 6 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 7 | sys.path.append(BASE_DIR) 8 | from pathlib import Path 9 | 10 | from trading_engine import FutuTrade 11 | 12 | # Get the current directory where the script is located 13 | current_directory = Path(__file__).parent 14 | 15 | # Create the log directory at the same level as the "engines" directory 16 | download_directory = current_directory / "../downloads" 17 | download_directory.mkdir(parents=True, exist_ok=True) 18 | 19 | from utils.global_vars import * 20 | from utils import logger 21 | 22 | import datetime 23 | from datetime import date 24 | 25 | 26 | class Data: 27 | def __init__(self): 28 | """ 29 | Futu download data 30 | """ 31 | self.config = config 32 | self.port = int(self.config['FutuOpenD.Config'].get('Port')) 33 | self.host = str(self.config['FutuOpenD.Config'].get('Host')) 34 | self.quote_ctx = OpenQuoteContext(host=self.host, 35 | port=self.port) 36 | self.trade_ctx = OpenSecTradeContext(filter_trdmarket=TrdMarket.HK, 37 | host=self.host, 38 | security_firm=SecurityFirm.FUTUSECURITIES) 39 | self.logger = logger.get_logger(log_dir=logger.LOG_FILE) 40 | self.trading = FutuTrade(self.quote_ctx, self.trade_ctx) 41 | 42 | 43 | def __del__(self): 44 | pass 45 | 46 | def down_single_min_data(self, stock_code, index, data_path): 47 | csv_name = stock_code.split(".")[-1] + '_' + index_k[index] 48 | yesterday = str(datetime.date.today() + datetime.timedelta(-1)) 49 | path = data_path 50 | if platform.system().lower() != 'linux': 51 | path = '../downloads/' 52 | if os.path.isfile(path + csv_name + '.csv'): 53 | last_line = '' 54 | with open(path + csv_name + '.csv') as f_l: 55 | lines = f_l.readlines() 56 | if len(lines) > 1: 57 | last_line = lines[-1] 58 | else: 59 | os.remove(path + csv_name + '.csv') 60 | if last_line != '': 61 | f = open(path + csv_name + '.csv', 'a+') 62 | writer = csv.writer(f) 63 | res = self.trading.get_history_kline(stock_code, index_type[index], start_date=yesterday, 64 | end_date=yesterday) 65 | if len(res) > 0: 66 | for x in range(0, len(res)): 67 | data_list_temp = [ 68 | [res['open'][x], res['close'][x], res['high'][x], res['low'][x], res['volume'][x], 69 | res['turnover'][x], res['change_rate'][x], res['last_close'][x], res['time_key'][x]]] 70 | writer.writerows(data_list_temp) 71 | 72 | else: 73 | pass_day = str(date.today().year - 2) + '-' + str(date.today().month) + '-' + str( 74 | date.today().day) 75 | if index == 6: 76 | pass_day = str(date.today().year - 10) + '-' + str(date.today().month) + '-' + str( 77 | date.today().day) 78 | res = self.trading.get_history_kline(stock_code, index_type[index], start_date=pass_day, 79 | end_date=yesterday) 80 | f = open(path + csv_name + '.csv', 'w') 81 | writer = csv.writer(f) 82 | writer.writerows( 83 | [['open', 'close', 'high', 'low', 'volume', 'turnover', 'change_rate', 'last_close', 'time_key']]) 84 | if len(res) > 0: 85 | for x in range(0, len(res)): 86 | data_list_temp = [ 87 | [res['open'][x], res['close'][x], res['high'][x], res['low'][x], res['volume'][x], 88 | res['turnover'][x], res['change_rate'][x], res['last_close'][x], res['time_key'][x]]] 89 | writer.writerows(data_list_temp) 90 | 91 | def get_us_quoty_code_list(self, page): 92 | url = 'https://www.futunn.com/quote-api/quote-v2/get-stock-list' 93 | 94 | params = { 95 | 'marketType': 2, 96 | 'plateType': 1, 97 | 'rankType': 5, 98 | 'page': page, 99 | 'pageSize': 50 100 | } 101 | 102 | headers = { 103 | 'accept': 'application/json, text/plain, */*', 104 | 'accept-language': 'zh-CN,zh;q=0.9', 105 | 'cache-control': 'no-cache', 106 | 'cookie': futu_api[str(page)]['cookie'], 107 | 'dnt': '1', 108 | 'futu-x-csrf-token': 'wPN0j4mwm6Uu4Pp-kJYwkngV', 109 | 'pragma': 'no-cache', 110 | 'priority': 'u=1, i', 111 | 'quote-token': futu_api[str(page)]['quote-token'], 112 | 'referer': 'https://www.futunn.com/quote/us/stock-list/all-us-stocks/top-turnover', 113 | 'sec-ch-ua': '"Chromium";v="124", "Google Chrome";v="124", "Not-A.Brand";v="99"', 114 | 'sec-ch-ua-mobile': '?0', 115 | 'sec-ch-ua-platform': '"macOS"', 116 | 'sec-fetch-dest': 'empty', 117 | 'sec-fetch-mode': 'cors', 118 | 'sec-fetch-site': 'same-origin', 119 | 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36' 120 | } 121 | 122 | response = requests.get(url, headers=headers, params=params) 123 | 124 | return json.loads(response.text) 125 | 126 | 127 | if __name__ == '__main__': 128 | data = Data() 129 | data.down_single_min_data('HK.800000', 0, download_directory) 130 | -------------------------------------------------------------------------------- /engines/trading_engine.py: -------------------------------------------------------------------------------- 1 | from futu import * 2 | from datetime import date 3 | from utils import logger 4 | 5 | from typing import List, Any, Dict 6 | 7 | 8 | class FutuTrade: 9 | 10 | def __init__(self, quote_ctx: OpenQuoteContext, trade_ctx: OpenSecTradeContext): 11 | self.quote_ctx = quote_ctx 12 | self.trade_ctx = trade_ctx 13 | self.logger = logger.get_logger(log_dir=logger.LOG_FILE) 14 | 15 | def get_hk_stocks(self) -> dict: 16 | output_dict = {} 17 | 18 | simple_filter = SimpleFilter() 19 | simple_filter.stock_field = StockField.CUR_PRICE 20 | simple_filter.filter_min = 2 21 | simple_filter.is_no_filter = False 22 | 23 | market_val = SimpleFilter() 24 | market_val.stock_field = StockField.MARKET_VAL 25 | market_val.filter_min = 100000000000 # one billion 26 | market_val.is_no_filter = False 27 | 28 | turnover = AccumulateFilter() 29 | turnover.stock_field = StockField.TURNOVER 30 | turnover.filter_min = 10000000 # one ten million 31 | turnover.is_no_filter = False 32 | 33 | sum_of_business = FinancialFilter() 34 | sum_of_business.stock_field = StockField.SUM_OF_BUSINESS 35 | sum_of_business.filter_min = 10000000 36 | sum_of_business.is_no_filter = False 37 | sum_of_business.quarter = FinancialQuarter.ANNUAL # year 38 | 39 | financial_filter = AccumulateFilter() 40 | financial_filter.stock_field = StockField.VOLUME 41 | financial_filter.filter_min = 200000 # daily volume 42 | financial_filter.is_no_filter = False 43 | 44 | lot_price = SimpleFilter() 45 | lot_price.stock_field = StockField.LOT_PRICE 46 | lot_price.filter_min = 2000 47 | lot_price.is_no_filter = False 48 | 49 | begin_index = 0 50 | 51 | ignore_stock = {} 52 | for i in range(0, len(self.init.ignore_code)): 53 | ignore_stock[self.init.ignore_code[i]] = self.init.ignore_code[i] 54 | 55 | while True: 56 | ret, ls = self.init.quote_ctx.get_stock_filter(market=Market.HK, 57 | filter_list=[simple_filter, market_val, turnover, 58 | sum_of_business, 59 | lot_price, financial_filter], 60 | begin=begin_index) 61 | if ret == RET_OK: 62 | last_page, all_count, ret_list = ls 63 | for item in ret_list: 64 | if item.stock_code in ignore_stock.keys(): 65 | continue 66 | output_dict[item.stock_code] = item.stock_name 67 | begin_index += 200 68 | if begin_index >= all_count: 69 | break 70 | elif ret == RET_ERROR: 71 | self.logger.error(f'get_stock_filter err: \n{output_dict}') 72 | return output_dict 73 | self.logger.info(f'get_stock_filter: \n{output_dict}') 74 | output_dict['HK.HSImain'] = "恒指主连" 75 | output_dict['HK.800000'] = "恒生指数" 76 | output_dict['HK.07226'] = "南方两倍做多恒生科技" 77 | output_dict['HK.07552'] = "南方两倍做空恒生科技" 78 | return output_dict 79 | 80 | def get_history_kline(self, stock_code, ktype, 81 | start_date=str(date.today().year - 2) + '-' + str(date.today().month) + '-' + str( 82 | date.today().day), end_date=str(date.today())): 83 | column_names = ['open', 'close', 'high', 'low', 'volume', 'turnover', 'change_rate', 'last_close', 'time_key'] 84 | history_df = pd.DataFrame(columns=column_names) 85 | ret, data, page_req_key = self.quote_ctx.request_history_kline(stock_code, 86 | start=start_date, 87 | end=end_date, 88 | ktype=ktype, autype=AuType.QFQ, 89 | fields=[KL_FIELD.ALL], 90 | max_count=1000, page_req_key=None, 91 | extended_time=True) 92 | if ret == RET_OK: 93 | history_df = pd.concat([history_df, data], ignore_index=True) 94 | time.sleep(0.6) 95 | else: 96 | self.logger.error('request_history_kline.%s', data) 97 | while page_req_key is not None: 98 | ret, data, page_req_key = self.quote_ctx.request_history_kline(stock_code, 99 | start=start_date, 100 | end=end_date, 101 | ktype=ktype, autype=AuType.QFQ, 102 | fields=[KL_FIELD.ALL], 103 | max_count=1000, 104 | page_req_key=page_req_key, 105 | extended_time=True) 106 | if ret == RET_OK: 107 | history_df = pd.concat([history_df, data], ignore_index=True) 108 | else: 109 | self.logger.error(f'page data, Cannot request_history_kline: {history_df}') 110 | return 111 | 112 | return history_df 113 | 114 | def request_trading_days(self, start_date=str(date.today().year - 2) + '-' + str(date.today().month) + '-' + str( 115 | date.today().day), end_date=str(date.today())) -> List[Any]: 116 | """ 117 | 请求交易日,该交易日是通过自然日剔除周末和节假日得到,未剔除临时休市数据。 118 | :param start_date: 默认从今天往过去2年取交易日 119 | :param end_date: 120 | :return: [{'time': '2020-04-01', 'trade_date_type': 'WHOLE'}, ...] 121 | """ 122 | 123 | ret, data = self.quote_ctx.request_trading_days(market=TradeDateMarket.HK, start=start_date, 124 | end=end_date) 125 | if ret == RET_OK: 126 | l = list() 127 | for i in range(0, len(data)): 128 | l.append(data[i]['time']) 129 | return l 130 | else: 131 | print('error:', data) 132 | 133 | def kline_subscribe(self, stock_list: list, sub_type: list) -> bool: 134 | self.logger.info(f'Subscribing to {len(stock_list)} kline...') 135 | ret_sub, err_message = self.quote_ctx.subscribe(stock_list, sub_type) 136 | if ret_sub != RET_OK: 137 | self.logger.error(f'Cannot subscribe to K-Line: {err_message}') 138 | return False 139 | return ret_sub == RET_OK 140 | --------------------------------------------------------------------------------