├── utils ├── __init__.py ├── conf.py ├── trading_day.py ├── bark.py ├── chanify.py ├── util.py ├── lark.py ├── ding.py └── wecom.py ├── .dockerignore ├── static ├── set.png ├── test.png ├── yun_fun.png └── database.png ├── .deepsource.toml ├── requirements.txt ├── Dockerfile ├── conf_back.json ├── app.py ├── .gitignore ├── trend.py ├── README.md ├── fund_pegging.py └── mt5.py /utils/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.git 2 | */temp* 3 | */*/temp* 4 | temp? 5 | .idea 6 | README.md -------------------------------------------------------------------------------- /static/set.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoysKang/fund_pegging/HEAD/static/set.png -------------------------------------------------------------------------------- /static/test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoysKang/fund_pegging/HEAD/static/test.png -------------------------------------------------------------------------------- /static/yun_fun.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoysKang/fund_pegging/HEAD/static/yun_fun.png -------------------------------------------------------------------------------- /static/database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JoysKang/fund_pegging/HEAD/static/database.png -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "python" 5 | enabled = true 6 | 7 | [analyzers.meta] 8 | runtime_version = "3.x.x" -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | chinese_calendar==1.5.1 2 | pydantic==1.8.2 3 | easyquotation==0.7.4 4 | requests==2.31.0 5 | akshare==1.1.19 6 | fastapi==0.70.0 7 | uvicorn==0.15.0 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8-slim 2 | 3 | COPY . /code 4 | 5 | WORKDIR /code 6 | 7 | RUN pip install -r requirements.txt \ 8 | && rm -rf ~/.cache/* \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | CMD uvicorn app:app --host 0.0.0.0 --port 9000 -------------------------------------------------------------------------------- /utils/conf.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | def load_json(file_path="../conf.json"): 5 | """加载配置文件""" 6 | try: 7 | with open(file_path, 'r') as file: 8 | return json.load(file) 9 | except Exception as e: 10 | print(e) 11 | return {} 12 | 13 | 14 | # 同一级目录可直接引用 15 | conf = load_json() 16 | -------------------------------------------------------------------------------- /utils/trading_day.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from chinese_calendar import is_workday, is_holiday 4 | 5 | 6 | def is_trading_day(): 7 | """今天是否是交易日""" 8 | today = datetime.date.today() 9 | if is_workday(today) and not is_holiday(today): 10 | return True 11 | 12 | return False 13 | 14 | 15 | if __name__ == '__main__': 16 | print(is_trading_day()) 17 | -------------------------------------------------------------------------------- /utils/bark.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from utils.conf import load_json 3 | 4 | conf = load_json("./conf.json") 5 | 6 | 7 | def send_bark(message): 8 | """使用 bark 发送消息""" 9 | url = f"https://api.day.app/{conf.get('bark_token')}/{message}?group=fundPegging" 10 | print(url) 11 | requests.get(url) 12 | 13 | 14 | if __name__ == '__main__': 15 | conf = load_json("../conf.json") 16 | send_bark("没有有用的信息") 17 | -------------------------------------------------------------------------------- /conf_back.json: -------------------------------------------------------------------------------- 1 | { 2 | "bark_token": "6rqxxx", 3 | "ding_webhook": "https://oapi.dingtalk.com/robot/send?access_token=f01bxxx", 4 | "lark_webhook": "https://open.feishu.cn/open-apis/bot/v2/hook/ec001bxxx", 5 | "wecom_cid": "ww27xxx", 6 | "wecom_aid": "100xxx", 7 | "wecom_secret": "qLw-Xxxx", 8 | "wecom_sendkey": "8d1dxxx", 9 | "wecom_network_url": "https://service-e29818w2-xxx", 10 | "authorization": "secret_mZTk6jxxx", 11 | "databases_id": "a1498dfxxx", 12 | "send_type": "1", 13 | "tq_account": "xxx", 14 | "tq_password": "xxx" 15 | } -------------------------------------------------------------------------------- /utils/chanify.py: -------------------------------------------------------------------------------- 1 | from urllib import request, parse 2 | 3 | from utils.conf import load_json 4 | 5 | conf = load_json("./conf.json") 6 | 7 | 8 | def send_chanify(message=""): 9 | """使用 chanify 发送消息""" 10 | data = parse.urlencode({'title': 'fundPegging', 'text': message, 'sound': 1}).encode() 11 | req = request.Request(f"https://api.chanify.net/v1/sender/{conf.get('chanify_token')}", data=data) 12 | request.urlopen(req) 13 | 14 | 15 | if __name__ == "__main__": 16 | conf = load_json("../conf.json") 17 | message = "明月几时有,把酒问青天。" 18 | send_chanify(message) 19 | -------------------------------------------------------------------------------- /utils/util.py: -------------------------------------------------------------------------------- 1 | from utils.ding import text 2 | from utils.lark import send_lark 3 | from utils.wecom import send_to_wecom_by_txy, send_to_wecom 4 | from utils.bark import send_bark 5 | from utils.chanify import send_chanify 6 | 7 | from utils.conf import load_json 8 | 9 | conf = load_json("./conf.json") 10 | 11 | 12 | def send_to_message(message): 13 | """发送消息""" 14 | send_func = { 15 | "1": "text(message)", 16 | "2": "send_lark(message)", 17 | "3": "send_to_wecom(message)", 18 | "4": "send_to_wecom_by_txy(message)", 19 | "5": "send_bark(message)", 20 | "6": "send_chanify(message)", 21 | } 22 | eval(send_func.get((conf.get("send_type")))) # 发送 23 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | 3 | from fastapi import FastAPI 4 | from pydantic import BaseModel 5 | from fund_pegging import pegging 6 | from mt5 import main as mt5_main 7 | 8 | app = FastAPI() 9 | 10 | 11 | # 定义 /event-invoke 的请求体 12 | class Item(BaseModel): 13 | Type: str 14 | TriggerName: str 15 | Time: str 16 | Message: str 17 | 18 | 19 | @app.post('/event-invoke') 20 | async def invoke(item: Item): 21 | """腾讯事件函数""" 22 | # 运行监听(腾讯云云函数不会等待异步任务结束,所以这里自行等待) 23 | await asyncio.create_task(pegging()) 24 | await asyncio.create_task(mt5_main()) 25 | 26 | return {"message": "Ok!"} 27 | 28 | 29 | @app.get("/") 30 | async def root(): 31 | """连通性检查""" 32 | return {"message": "Hello World"} 33 | -------------------------------------------------------------------------------- /utils/lark.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | from utils.conf import load_json 4 | 5 | conf = load_json("./conf.json") 6 | 7 | 8 | def send_lark(message=""): 9 | """通过飞书群组机器人发送消息""" 10 | url = conf.get("lark_webhook") 11 | 12 | payload = json.dumps({ 13 | "msg_type": "text", 14 | "content": { 15 | "text": message 16 | } 17 | }) 18 | 19 | headers = { 20 | 'Hack-Auth': '2478', 21 | 'Content-Type': 'application/json' 22 | } 23 | 24 | response = requests.request("POST", url, headers=headers, data=payload) 25 | if response.status_code == 200: 26 | print("发送成功") 27 | 28 | 29 | if __name__ == '__main__': 30 | conf = load_json("../conf.json") 31 | send_lark("test") 32 | -------------------------------------------------------------------------------- /utils/ding.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import traceback 3 | from utils.conf import load_json 4 | 5 | conf = load_json("./conf.json") 6 | 7 | # dingding 相关文档:https://developers.dingtalk.com/document/app/custom-robot-access 8 | webhook_url = conf.get('ding_webhook') 9 | 10 | 11 | class FError(Exception): 12 | pass 13 | 14 | 15 | def text(content=""): 16 | """ 17 | 发送 text 类型信息到钉钉 18 | 参数示例: 19 | { 20 | "text": { 21 | "content":"我就是我, @XXX 是不一样的烟火" 22 | }, 23 | "msgtype":"text" 24 | } 25 | """ 26 | # 发送钉钉机器人通知 27 | msg_data = dict(msgtype='text', text=dict(content=content)) 28 | print(webhook_url, "//") 29 | response = requests.post(webhook_url, json=msg_data) 30 | if response.status_code != 200: 31 | print(response.text) 32 | 33 | 34 | def link(content=None): 35 | """ 36 | 发送 link 类型信息到钉钉 37 | 参数示例: 38 | { 39 | "msgtype": "link", 40 | "link": { 41 | "text": "这个即将发布的新版本,创始人xx称它为红树林。而在此之前,每当面临重大升级,产品经理们都会取一个应景的代号,这一次,为什么是红树林", 42 | "title": "时代的火车向前开", 43 | "picUrl": "图片地址", 44 | "messageUrl": "https://www.dingtalk.com/s?__biz=MzA4NjMwMTA2Ng==&mid=2650316842&idx=1&sn=60da3ea2b29f1dcc43a7c8e4a7c97a16&scene=2&srcid=09189AnRJEdIiWVaKltFzNTw&from=timeline&isappinstalled=0&key=&ascene=2&uin=&devicetype=android-23&version=26031933&nettype=WIFI" 45 | } 46 | } 47 | """ 48 | # 发送钉钉机器人通知 49 | if not isinstance(content, dict): 50 | raise FError("content must be a dictionary") 51 | 52 | msg_data = dict(msgtype='link', link=content) 53 | response = requests.post(webhook_url, json=msg_data) 54 | if response.status_code != 200: 55 | print(response.text) 56 | 57 | 58 | if __name__ == '__main__': 59 | conf = load_json("../conf.json") 60 | webhook_url = conf.get('ding_webhook') 61 | text("基金盯盘:我就是我, @XXX 是不一样的烟火") 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IDE 2 | .idea/ 3 | 4 | # Mypy 5 | .mypy_cache/ 6 | 7 | # Elastic Beanstalk Files 8 | .elasticbeanstalk/* 9 | !.elasticbeanstalk/*.cfg.yml 10 | !.elasticbeanstalk/*.global.yml 11 | 12 | # Test generated files 13 | media/images/test_*.png 14 | media/ad_images/*.* 15 | 16 | # Created by https://www.gitignore.io 17 | 18 | ### OSX ### 19 | .DS_Store 20 | .AppleDouble 21 | .LSOverride 22 | 23 | # Icon must end with two \r 24 | Icon 25 | 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear on external disk 31 | .Spotlight-V100 32 | .Trashes 33 | 34 | # Directories potentially created on remote AFP share 35 | .AppleDB 36 | .AppleDesktop 37 | Network Trash Folder 38 | Temporary Items 39 | .apdisk 40 | 41 | 42 | ### Python ### 43 | # Byte-compiled / optimized / DLL files 44 | __pycache__/ 45 | *.py[cod] 46 | 47 | # C extensions 48 | *.so 49 | 50 | # Distribution / packaging 51 | .Python 52 | env/ 53 | build/ 54 | develop-eggs/ 55 | dist/ 56 | downloads/ 57 | eggs/ 58 | lib/ 59 | lib64/ 60 | parts/ 61 | sdist/ 62 | var/ 63 | *.egg-info/ 64 | .installed.cfg 65 | *.egg 66 | 67 | # PyInstaller 68 | # Usually these files are written by a python script from a template 69 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 70 | *.manifest 71 | *.spec 72 | 73 | # Installer logs 74 | pip-log.txt 75 | pip-delete-this-directory.txt 76 | 77 | # Unit test / coverage reports 78 | htmlcov/ 79 | .tox/ 80 | .coverage 81 | .cache 82 | nosetests.xml 83 | coverage.xml 84 | 85 | # Translations 86 | *.mo 87 | *.pot 88 | 89 | # Sphinx documentation 90 | docs/_build/ 91 | 92 | # PyBuilder 93 | target/ 94 | 95 | 96 | ### Django ### 97 | *.log 98 | *.pot 99 | *.pyc 100 | __pycache__/ 101 | local_settings.py 102 | 103 | .env 104 | db.sqlite3 105 | /.python-version 106 | local 107 | runserver_local.sh 108 | *.orig 109 | nohup.out 110 | conf.json -------------------------------------------------------------------------------- /trend.py: -------------------------------------------------------------------------------- 1 | """ 2 | 趋势模型的逻辑(来自微信公众号:量化投基): 3 | 沪深300、中证500、创业板, 4 | 5 | 逻辑: 6 | 每个交易日的 14:30 与自己前 22 个交易日的 14:30 对比,按照涨幅排序,谁的涨势好,满仓买入谁。 7 | 什么时候会清仓? 8 | 当涨幅排序的第一名的涨跌幅为负数时,就会清仓。 9 | 10 | 什么时候会换仓? 11 | 举例,假设今天两点半之前,正在持有的品种是【创业板】,但两点半时,我们拿第一名的涨跌幅和持有的【创业板】进行比较,假设 12 | 第一名【沪深300】的涨跌幅已经远高于正在持有的【创业板】,这时就进行了调仓。 13 | 14 | 策略特点:能抓住大涨,并能躲过大跌,收益高,交易次数少。 15 | 16 | https://cloud.tencent.com/developer/article/1429693 学习资料 17 | https://doc.shinnytech.com/tqsdk/latest/index.html 18 | """ 19 | 20 | import decimal 21 | 22 | import requests 23 | from tqsdk import TqApi, TqAuth 24 | 25 | 26 | # 修改舍入方式为四舍五入 27 | decimal.getcontext().rounding = "ROUND_HALF_UP" 28 | code = { 29 | 'sz399300': 510300, 30 | 'sz399905': 510500, 31 | 'sz399006': 159915 32 | } 33 | 34 | 35 | def get_data(symbol=None): 36 | """获取前 22 个交易的 14:30 数据 和 当前交易日的 14:30 数据的涨跌幅度""" 37 | if symbol is None: 38 | return 0 39 | 40 | response = requests.get( 41 | f'https://money.finance.sina.com.cn/quotes_service/api/json_v2.php/CN_MarketData.getKLineData?symbol={symbol}&scale=30&ma=&datalen=220') 42 | data = response.json() 43 | data = [d for d in data if "14:30:00" in d['day']] 44 | increase = (float(data[-1]['close']) - float(data[-22]['close'])) / float(data[-22]['close']) * 100 45 | increase = float(decimal.Decimal(str(increase)).quantize(decimal.Decimal("0.00"))) 46 | # print(f'{symbol} 涨跌幅度: {increase}') 47 | return {"symbol": symbol, "increase": increase} 48 | 49 | 50 | def get_current_symbol(): 51 | """获取当前持有的品种""" 52 | api = TqApi(auth=TqAuth("joyskang", "kang5113")) 53 | account = api.get_account() 54 | print(account) 55 | 56 | 57 | def main(): 58 | """主函数""" 59 | increases = [get_data('sz399300'), get_data('sz399905'), get_data('sz399006')] 60 | increases.sort(key=lambda k: -k['increase']) 61 | print(f'涨跌幅度排序: {increases}') 62 | 63 | 64 | if __name__ == '__main__': 65 | # main() 66 | get_current_symbol() 67 | 68 | -------------------------------------------------------------------------------- /utils/wecom.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from urllib.parse import quote 4 | from utils.conf import load_json 5 | 6 | conf = load_json("./conf.json") 7 | wecom_cid = conf.get("wecom_cid") # 企业微信自建公司ID 8 | wecom_aid = conf.get("wecom_aid") # 自建应用ID 9 | wecom_secret = conf.get("wecom_secret") # 应用secret 10 | url = f"{conf.get('wecom_network_url')}?sendkey={conf.get('wecom_sendkey')}&msg_type=text" 11 | 12 | 13 | def send_to_wecom(text, wecom_touid='@all'): 14 | """使用 企业微信 openapi 发送消息""" 15 | get_token_url = f"https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={wecom_cid}&corpsecret={wecom_secret}" 16 | response = requests.get(get_token_url).content 17 | access_token = json.loads(response).get('access_token') 18 | if access_token and len(access_token) > 0: 19 | send_msg_url = f'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={access_token}' 20 | data = { 21 | "touser": wecom_touid, 22 | "agentid": wecom_aid, 23 | "msgtype": "text", 24 | "text": { 25 | "content": text 26 | }, 27 | "duplicate_check_interval": 600 28 | } 29 | response = requests.post(send_msg_url, data=json.dumps(data)).content 30 | return response 31 | return False 32 | 33 | 34 | def send_to_wecom_by_txy(message, to_user=''): 35 | """通过腾讯云部署的云函数发送消息 36 | to_user: User1|User2,默认为空,发送给全部 37 | """ 38 | message = quote(message, 'utf-8') 39 | full_url = f"{url}&msg={message}&to_user={to_user}" 40 | response = requests.get(full_url) 41 | if response.status_code == 200: 42 | print("发送成功") 43 | 44 | 45 | if __name__ == '__main__': 46 | conf = load_json("../conf.json") 47 | wecom_cid = conf.get("wecom_cid") # 企业微信自建公司ID 48 | wecom_aid = conf.get("wecom_aid") # 自建应用ID 49 | wecom_secret = conf.get("wecom_secret") # 应用secret 50 | url = f"{conf.get('wecom_network_url')}?sendkey={conf.get('wecom_sendkey')}&msg_type=text" 51 | 52 | # send_to_wecom("推送测试\r\n测试换行") 53 | send_to_wecom_by_txy("推送测试\r\n测试换行") 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fund_pegging 2 | 3 | 基金盯盘 4 | 5 | 6 | ## 食用 7 | 8 | ### 1. 配置 9 | 配置都写在了 conf_back.json 中, 在里边修改配置,然后文件名改为 conf.json 。 10 | 其中发送方式写了四种(1 钉钉 2 飞书 3 企业微信(openapi) 4 企业微信(腾讯云函数)), 11 | 自己使用任选其一就好。 12 | 13 | 配置详解 14 | ```json 15 | { 16 | "ding_webhook": "钉钉webhook(这里用的是关键字加密,关键字:基金盯盘)", 17 | "lark_webhook": "飞书webhook", 18 | "wecom_cid": "企业微信中的企业id", 19 | "wecom_aid": "企业微信中的应用ID", 20 | "wecom_secret": "企业微信中的应用Secret", 21 | "wecom_sendkey": "你自己在腾讯云函数中创建秘钥", 22 | "wecom_network_url": "腾讯云函数中的触发器请求地址", 23 | "authorization": "Notion 秘钥", 24 | "databases_id": "Notion 数据库ID", 25 | "send_type": "默认的消息发送方式" 26 | } 27 | ``` 28 | 29 | 可参考下边的文档: 30 | [获取钉钉webhook文档](https://help.aliyun.com/document_detail/121918.html?utm_content=g_1000230851&spm=5176.20966629.toubu.3.f2991ddcpxxvD1#title-nw5-v22-9in) 31 | [获取飞书webhook文档](https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN) 32 | [企业微信的设置](https://github.com/easychen/wecomchan) 33 | [腾讯云函数搭建](https://github.com/easychen/wecomchan/tree/main/go-scf) 34 | [Notion database_id的获取 & 机器人的创建](https://sspai.com/post/66658) 35 | 36 | 37 | ### 2. Notion 数据库 38 | 数据库创建格式如下([我的数据库地址](https://joys.notion.site/a1498df489724b638c713618806380cf?v=08899d34db7347a38cc1d6d4edea1b76)) 39 | ![](./static/database.png) 40 | 41 | ### 3. 本地启动 42 | 本地启动命令(依赖自行安装) 43 | ```bash 44 | uvicorn app:app --host 0.0.0.0 --port 9000 45 | ``` 46 | 47 | 镜像启动 48 | ```bash 49 | docker build -t fund_pegging . # 制作镜像 50 | docker run -p 9000:9000 fund_pegging 51 | ``` 52 | 53 | curl调用 54 | data-raw 数据模拟的是腾讯云函数定时任务的参数 55 | ```bash 56 | curl --location --request POST 'http://127.0.0.1:9000/event-invoke' \ 57 | --header 'Content-Type: application/json' \ 58 | --data-raw '{ 59 | "Type":"Timer", 60 | "TriggerName":"EveryDay", 61 | "Time":"2019-02-21T11:49:00Z", 62 | "Message":"user define msg body" 63 | }' 64 | ``` 65 | 66 | ### 4. 准备镜像 67 | a. [开通个人镜像仓库](https://cloud.tencent.com/document/product/1141/50332) 68 | b. 提交镜像到腾讯云个人镜像仓库 69 | ```bash 70 | docker build -t fund_pegging . # 进到项目目录中 71 | sudo docker login --username=[username] ccr.ccs.tencentyun.com # 登录到腾讯云 registry 72 | # 上传镜像 73 | docker tag fund_pegging ccr.ccs.tencentyun.com/你的命名空间/fund_pegging:v1 74 | docker push ccr.ccs.tencentyun.com/你的命名空间/fund_pegging:v1 75 | ``` 76 | 77 | ### 5. 使用腾讯云函数部署 78 | ![](./static/yun_fun.png) 79 | 80 | 函数配置 81 | ![](./static/set.png) 82 | 83 | 测试 84 | ![](./static/test.png) -------------------------------------------------------------------------------- /fund_pegging.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | import json 5 | import requests 6 | import akshare as ak 7 | 8 | from utils.trading_day import is_trading_day 9 | from utils.conf import load_json 10 | from utils.util import send_to_message 11 | 12 | conf = load_json("./conf.json") 13 | 14 | # 1 钉钉 2 飞书 3 企业微信(openapi) 4 企业微信(腾讯云函数) 15 | send_func = { 16 | "1": "text(message)", 17 | "2": "send_lark(message)", 18 | "3": "send_to_wecom(message)", 19 | "4": "send_to_wecom_by_txy(message)", 20 | "5": "send_bark(message)", 21 | "6": "send_chanify(message)", 22 | } 23 | 24 | 25 | def get_all_fund_estimation(): 26 | """通过 akshare 获取所有基金(天天基金)的估值 27 | 返回值类型 pandas 28 | """ 29 | return ak.fund_em_value_estimation() 30 | 31 | 32 | def get_data(): 33 | """通过 Notion API 获取指定 database 里的数据""" 34 | headers = { 35 | 'Authorization': conf.get('authorization'), 36 | 'Notion-Version': '2021-05-13', 37 | 'Content-Type': 'application/json', 38 | } 39 | 40 | _data = '{"filter":{"or":[{"and":[{"property":"Status","select":{"equals":"启用"}},{"property":"Type","select":{' \ 41 | '"equals":"场内"}}]},{"and":[{"property":"Status","select":{"equals":"启用"}},{"property":"Type",' \ 42 | '"select":{"equals":"场外"}}]}]}}'.encode() 43 | 44 | response = requests.post( 45 | f'https://api.notion.com/v1/databases/{conf.get("databases_id")}/query', 46 | headers=headers, data=_data) 47 | content = json.loads(response.content) 48 | return {result['properties']['Code']['rich_text'][0]['plain_text']: { 49 | "name": result['properties']['Name']['title'][0]['plain_text'], 50 | "cordon": result['properties']['Decline']['select']['name'], 51 | "communication": "" 52 | } for result in content['results']} 53 | 54 | 55 | async def task(code=None, percent="", content=None): 56 | """处理盯盘任务""" 57 | if "-" not in percent: # 上涨的不处理 58 | return None 59 | 60 | if content is None: 61 | return None 62 | 63 | percent = percent.replace("-", '').replace("%", '') 64 | cordon = content.get('cordon') 65 | if percent < cordon: # 不需要发通知 66 | print(f"{code},{content.get('name')},-{percent}%,-{cordon}%,不需要发通知") 67 | return None 68 | 69 | # message IDE 会提示没有使用,但其实下边的 eval 使用了 70 | message = f"基金盯盘: {content.get('name')} 今日跌幅超过 {cordon}% 警戒线, 当前跌幅 {percent}% , 基金代码 {code} ." 71 | send_to_message(message) 72 | return code 73 | 74 | 75 | async def pegging(): 76 | """主函数""" 77 | # 如果不是交易日,则直接结束 78 | if not is_trading_day(): 79 | print(f"日期:{datetime.date.today()},当前不是交易日") 80 | return 81 | 82 | try: 83 | df = get_all_fund_estimation() 84 | except Exception as e: # 重试 85 | print(e) 86 | print("获取基金估值失败,重试中...") 87 | df = get_all_fund_estimation() 88 | data = get_data() 89 | code_list = data.keys() 90 | 91 | funds = df[df['基金代码'].isin(code_list)].iloc[:, [1, 4]] 92 | results = [] 93 | for _, row in funds.iterrows(): 94 | code = row[0] 95 | results.append(await task(code, row[1], data[code])) # 编码,涨跌幅,警戒线 96 | 97 | if results := list(filter(None, results)): 98 | print(f"日期:{datetime.date.today()},报警的监控条数{len(results)}") 99 | else: 100 | print(f"日期:{datetime.date.today()},没有需要报警的监控") 101 | 102 | 103 | if __name__ == '__main__': 104 | # get_data() 105 | 106 | import asyncio 107 | loop = asyncio.get_event_loop() 108 | result = loop.run_until_complete(pegging()) 109 | loop.close() 110 | -------------------------------------------------------------------------------- /mt5.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | import requests 4 | import datetime 5 | 6 | import akshare as ak 7 | import easyquotation 8 | 9 | from utils.trading_day import is_trading_day 10 | from utils.conf import load_json 11 | from utils.util import send_to_message 12 | 13 | conf = load_json("./conf.json") 14 | 15 | 16 | def mt5_down(close_list, index=4, down=False): 17 | """九转序列判断:连续 9 天收盘价低于前 4 天的收盘价""" 18 | if index == 13: 19 | return down 20 | 21 | if close_list[index] < min(close_list[index-4:index]): # 小于前四日的收盘价 22 | return mt5_up(close_list, index + 1, True) 23 | 24 | return False 25 | 26 | 27 | def mt5_up(close_list, index=4, up=False): 28 | """九转序列判断:连续 9 天收盘价高于前 4 天的收盘价""" 29 | if index == 13: 30 | return up 31 | 32 | if close_list[index] > max(close_list[index-4:index]): # 大于前四日的收盘价 33 | return mt5_up(close_list, index + 1, True) 34 | 35 | return False 36 | 37 | 38 | def get_symbols(): 39 | """通过 Notion API 获取指定 database 里的数据""" 40 | headers = { 41 | 'Authorization': conf.get('authorization'), 42 | 'Notion-Version': '2021-05-13', 43 | 'Content-Type': 'application/json', 44 | } 45 | 46 | _data = '{ "filter": { "and": [{"property":"Status","select":{"equals":"启用"}},{"property":"Type","select":{' \ 47 | '"equals":"股票"}}] } }'.encode() 48 | 49 | response = requests.post( 50 | f'https://api.notion.com/v1/databases/{conf.get("databases_id")}/query', 51 | headers=headers, data=_data) 52 | content = json.loads(response.content) 53 | 54 | return [{'code': r['properties']['Code']['rich_text'][0]['plain_text'], 55 | 'name': r['properties']['Name']['title'][0]['plain_text']} 56 | for r in content['results']] 57 | 58 | 59 | def get_12days_data(symbol='sz300015'): 60 | """获取前12天的数据""" 61 | data = ak.stock_zh_a_daily(symbol).sort_values(by='date', ascending=False).iloc[:12] 62 | return data['close'].tolist()[::-1] 63 | 64 | 65 | async def task(symbol=None, prices=None): 66 | """处理任务""" 67 | if not symbol or not prices: 68 | return None 69 | 70 | code = symbol['code'] 71 | price = prices[re.findall(r"\d+\.?\d*", code)[0]] 72 | close_list = get_12days_data(code) 73 | close_list.append(price) 74 | 75 | # message IDE 会提示没有使用,但其实下边的 eval 使用了 76 | message = "" 77 | time_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%m:%S') 78 | if mt5_up(close_list): 79 | message = f"九转序列盯盘: 股票: {symbol['name']}, 代码: {code}, 时间: {time_str}, 呈卖出结构, 请留意" 80 | 81 | elif mt5_down(close_list): 82 | message = f"九转序列盯盘: 股票: {symbol['name']}, 代码: {code}, 时间: {time_str}, 呈买入结构, 请留意." 83 | 84 | if message: 85 | send_to_message(message) 86 | return code 87 | 88 | 89 | def get_price(symbol_list=None): 90 | """获取 14:30 的价格""" 91 | if symbol_list is None: 92 | return {} 93 | 94 | quotation = easyquotation.use('sina') # 新浪 ['sina'] 腾讯 ['tencent', 'qq'] 95 | quotation.market_snapshot(prefix=False) # prefix 参数指定返回的行情字典中的股票代码 key 是否带 sz/sh 前缀 96 | prices = quotation.stocks(symbol_list) 97 | 98 | for price in prices: 99 | prices[price] = prices[price]['now'] 100 | 101 | return prices 102 | 103 | 104 | async def main(): 105 | """主函数""" 106 | # 如果不是交易日,则直接结束 107 | if not is_trading_day(): 108 | print(f"日期:{datetime.date.today()},当前不是交易日") 109 | return 110 | 111 | symbols = get_symbols() 112 | code_list = [s['code'] for s in symbols] 113 | prices = get_price(code_list) 114 | results = [] 115 | results.extend(await task(symbol, prices) for symbol in symbols) 116 | if results := list(filter(None, results)): 117 | print(f"日期:{datetime.date.today()},报警的监控条数{len(results)}") 118 | else: 119 | print(f"日期:{datetime.date.today()},没有需要报警的监控") 120 | 121 | 122 | if __name__ == "__main__": 123 | import asyncio 124 | 125 | loop = asyncio.get_event_loop() 126 | result = loop.run_until_complete(main()) 127 | loop.close() 128 | --------------------------------------------------------------------------------