diff --git a/Readme.md b/Readme.md index 54ffc27..fd165d2 100644 --- a/Readme.md +++ b/Readme.md @@ -1,4 +1,4 @@ -# 股海风云V1.96 +# 股海风云V2.0 适配真寻的炒股小游戏,使用金币为真寻经济系统的金币,使用的是真实数据 @@ -7,11 +7,16 @@ 注意:现阶段来说,这个插件对于玩过股市的人来说基本是必赚的,还请注意限制杠杆以控制盈利幅度 ### 更新内容 +v2.0 +* 将所有信息全部图片化(除了适配版的`我的持仓`指令,不过我觉得这个指令其实没人在用) +* 修正变更杠杆时剩余资金显示不正确的问题 +* 持仓页面添加持仓所属人的信息 + v1.99 * 添加`查看股票`指令,功能等同于之前的`买股票 代码 0` * 股票k线默认改为东方财富通,更加专业,且支持基金显示,如果感觉不习惯的可以在配置内调回来 * 表格版查看持仓添加持仓收益总计功能 -* 接下来7天内我都有事,有bug也修不了。所以这个版本不敢直接更新,放在release里了,代码仓库依然为1.96 +* 接下来7天内我都有事,有bug也修不了。所以这个版本不敢直接更新,放在名为`v1.99`的压缩包里了,代码仓库依然为1.96 v1.96 * 收益又被我搞坏了,已经修好 diff --git a/__init__.py b/__init__.py index bd3a940..aa17074 100644 --- a/__init__.py +++ b/__init__.py @@ -2,12 +2,14 @@ from nonebot import on_command from nonebot.adapters.onebot.v11 import MessageEvent, GroupMessageEvent, Message, Bot, MessageSegment -from nonebot.params import CommandArg +from nonebot.params import CommandArg, ArgPlainText, Arg from nonebot.permission import SUPERUSER from nonebot.typing import T_State +from nonebot.matcher import Matcher + from services.log import logger from configs.config import Config -from .utils import get_stock_img, send_forward_msg_group, convert_stocks_to_md_table, fill_stock_id +from .utils import get_stock_img, send_forward_msg_group, convert_stocks_to_md_table, fill_stock_id, get_stock_img_v2 from ..nonebot_plugin_htmlrender import text_to_pic, md_to_pic from .data_source import ( @@ -55,13 +57,19 @@ Q: 我是超级新手,怎么玩? A: 可以先输入‘买入躺平基金 x’ x为仓位数 最高10 可不填 + + Q: 股票代码是从哪里来的? + A: 需要从现实中的股市提取 + + Q: 为什么允许非整数的手数/是否应该加入汇率 + A: 当基金玩的 ———————————————— - 强制清仓+qq号 管理专用指令,爆仓人不愿意清仓就对他使用这个吧 + 强制清仓+qq号(不是at是qq号) 管理专用指令,爆仓人不愿意清仓就对他使用这个吧 """.strip() __plugin_des__ = "谁才是股市传奇?" __plugin_type__ = ("群内小游戏",) __plugin_cmd__ = ["买股票 代码 金额]", "卖股票 代码 仓位(十分制)", "我的持仓", "强制清仓"] -__plugin_version__ = 1.96 +__plugin_version__ = 2.0 __plugin_author__ = "XiaoR" __plugin_settings__ = { "level": 5, @@ -84,6 +92,11 @@ "value": False, "help": "如果我的持仓功能报错,且看了issue还是改不好,就把这个改成true", "default_value": False, + }, + "IMAGE_MODE": { + "value": 2, + "help": "1:股票提示图为百度股市通,比较新人 2:股票提示图为分时+日k且支持基金", + "default_value": 2, } } @@ -94,16 +107,17 @@ look_stock = on_command("查看持仓", aliases={"偷看持仓", "他的持仓"}, priority=5, block=True) revert_stock = on_command("反转持仓", priority=5, block=True) help_stock = on_command("关于股海风云", priority=5, block=True) +query_stock = on_command("查看股票", priority=5, block=True) @buy_stock.handle() async def _(event: MessageEvent, arg: Message = CommandArg()): if not isinstance(event, GroupMessageEvent): - await buy_stock.finish("这个游戏只能在群里玩哦") + await buy_stock.finish(await to_pic_msg("这个游戏只能在群里玩哦")) msg = arg.extract_plain_text().strip().split() if len(msg) < 1: - await buy_stock.finish(MessageSegment.image(await text_to_pic( - f"格式错误,请输入\n买股票 股票代码 金额 杠杆层数(可选)\n如 买股票 600888 1000 5", width=300))) + await buy_stock.finish(await to_pic_msg( + f"格式错误,请输入\n买股票 股票代码 金额 杠杆层数(可选)\n如 买股票 600888 1000 5", width=300)) if msg[0] == '躺平' or msg[0] == '躺平基金': # 买入躺平基金的特殊逻辑 await buy_lazy_handle(buy_stock, msg, event) return @@ -122,11 +136,11 @@ async def buy_handle(bot, msg, event): # 第三个参数是杠杆 # 最大杠杆比率 if cost == 0 and len(msg) == 2: # 专门用来看行情,但是加上杠杆参数就是改杠杆了 - await bot.send(MessageSegment.image(await text_to_pic(f"你看了看,但没有买", width=300))) - await bot.finish(await get_stock_img(origin_stock_id, stock_id, True)) + await bot.send(await to_pic_msg(f"你看了看,但没有买", width=300)) + await bot.finish(await get_stock_img_(origin_stock_id, stock_id)) if cost < 0: if cost < -max_gearing: - await bot.finish(MessageSegment.image(await text_to_pic(f"想做空的话\n请使用负数的杠杆率哦", width=300))) + await bot.finish(await to_pic_msg(f"想做空的话\n请使用负数的杠杆率哦", width=300)) else: # 这个人输入了买股票xxxx -10 (-10应该是杠杆倍率而不是cost) gearing = cost cost = 10 @@ -135,55 +149,55 @@ async def buy_handle(bot, msg, event): if gearing > max_gearing: if -max_gearing <= cost <= max_gearing: # 防呆,这人把输入参数顺序搞反了 cost, gearing = gearing, cost - await bot.send(MessageSegment.image(await text_to_pic( - f"你的杠杆和花费金币参数顺序反了,已经帮你修好了", width=300))) + await bot.send(await to_pic_msg( + f"你的杠杆和花费金币参数顺序反了,已经帮你修好了", width=300)) else: - await bot.send(MessageSegment.image(await text_to_pic( - f"最高杠杆只能到{max_gearing}倍,\n已经修正为{max_gearing}倍", width=300))) + await bot.send(await to_pic_msg( + f"最高杠杆只能到{max_gearing}倍,\n已经修正为{max_gearing}倍", width=300)) gearing = max_gearing if gearing < -max_gearing: - await bot.send(MessageSegment.image(await text_to_pic( - f"最高杠杆只能到-{max_gearing}倍,\n已经修正为-{max_gearing}倍", width=300))) + await bot.send(await to_pic_msg( + f"最高杠杆只能到-{max_gearing}倍,\n已经修正为-{max_gearing}倍", width=300)) gearing = -max_gearing result = await buy_stock_action(event.user_id, event.group_id, stock_id, gearing, cost) - await bot.send(MessageSegment.image(await text_to_pic(result, width=300))) - await bot.finish(await get_stock_img(origin_stock_id, stock_id)) + await bot.send(await to_pic_msg(result, width=300)) + await bot.finish(await get_stock_img_(origin_stock_id, stock_id)) @sell_stock.handle() async def _( event: MessageEvent, - state: T_State, arg: Message = CommandArg()): if not isinstance(event, GroupMessageEvent): - await buy_stock.finish("这个游戏只能在群里玩哦") + await sell_stock.finish(await to_pic_msg("这个游戏只能在群里玩哦")) msg = arg.extract_plain_text().strip().split() if len(msg) < 1: - await sell_stock.finish(MessageSegment.image(await text_to_pic( - "格式错误,请输入\n卖股票 股票代码 [仓位(不填默认为十)]\n如 卖股票 601919 10", width=300))) + await sell_stock.finish(await to_pic_msg( + "格式错误,请输入\n卖股票 股票代码 [仓位(不填默认为十)]\n如 卖股票 601919 10", width=300)) stock_id = fill_stock_id(msg[0]) if len(msg) == 1: percent = 10 else: percent = round(int(msg[1]), 2) if percent > 10: - await sell_stock.send("不能卖十成以上的仓位哦,已经帮你全卖了") + await sell_stock.send(await to_pic_msg("不能卖十成以上的仓位哦,已经帮你全卖了")) percent = 10 if percent <= 0: - await sell_stock.finish("卖的仓位太低了!") + await sell_stock.finish(await to_pic_msg("卖的仓位太低了!")) if msg[0] == '躺平' or msg[0] == '躺平基金': # 卖出躺平基金的特殊逻辑 await sell_lazy_handle(buy_stock, percent, event) return result = await sell_stock_action(event.user_id, event.group_id, stock_id, percent) - await sell_stock.send(MessageSegment.image(await text_to_pic(result, width=300))) + await sell_stock.send(await to_pic_msg(result, width=300)) origin_stock_id = stock_id[2:] - await sell_stock.finish(await get_stock_img(origin_stock_id, stock_id)) + await sell_stock.finish(await get_stock_img_(origin_stock_id, stock_id)) @my_stock.handle() -async def _(event: MessageEvent, bot: Bot, state: T_State, arg: Message = CommandArg()): +async def _(event: MessageEvent, bot: Bot): if not isinstance(event, GroupMessageEvent): - await buy_stock.finish("这个游戏只能在群里玩哦") + await my_stock.finish(await to_pic_msg("这个游戏只能在群里玩哦")) + username = await get_username(bot, event.group_id, event.user_id) if Config.get_config("stock_legend", "WIN_FIT", False): my_stocks = await get_stock_list_action_for_win(event.user_id, event.group_id) @@ -191,32 +205,31 @@ async def _(event: MessageEvent, bot: Bot, state: T_State, arg: Message = Comman else: my_stocks = await get_stock_list_action(event.user_id, event.group_id) if not my_stocks: - await sell_stock.finish(MessageSegment.image(await text_to_pic("你还什么都没买呢!", width=300)), at_sender=True) - txt = convert_stocks_to_md_table(my_stocks) + await sell_stock.finish(await to_pic_msg(f"{username}你还什么都没买呢!", width=300)) + txt = convert_stocks_to_md_table(username, my_stocks) logger.info(txt) await sell_stock.finish(MessageSegment.image(await md_to_pic(f"{txt}", width=1000)), at_sender=True) @look_stock.handle() -async def _(event: MessageEvent, bot: Bot, args: Message = CommandArg(), arg: Message = CommandArg()): +async def _(event: MessageEvent, bot: Bot, args: Message = CommandArg()): if not isinstance(event, GroupMessageEvent): - await buy_stock.finish("这个游戏只能在群里玩哦") + await look_stock.finish(await to_pic_msg("这个游戏只能在群里玩哦")) + look_qq = event.user_id for arg in args: if arg.type == "at": look_qq = arg.data.get("qq", "") - if not look_qq: - look_qq = event.user_id - + username = await get_username(bot, event.group_id, look_qq) if Config.get_config("stock_legend", "WIN_FIT", False): my_stocks = await get_stock_list_action_for_win(event.user_id, event.group_id) await send_forward_msg_group(bot, event, "真寻炒股小助手", my_stocks if my_stocks else ["仓位是空的"]) else: my_stocks = await get_stock_list_action(look_qq, event.group_id) if not my_stocks: - await sell_stock.finish(MessageSegment.image(await text_to_pic("他的仓位是空的", width=300)), at_sender=True) - txt = convert_stocks_to_md_table(my_stocks) + await sell_stock.finish(await to_pic_msg(f"{username}的仓位是空的", width=300)) + txt = convert_stocks_to_md_table(username, my_stocks) logger.info(txt) await sell_stock.finish(MessageSegment.image(await md_to_pic(f"{txt}", width=1200)), at_sender=True) @@ -226,26 +239,30 @@ async def _(event: MessageEvent, bot: Bot, args: Message = CommandArg(), arg: Me @clear_stock.handle() async def _(event: MessageEvent, arg: Message = CommandArg()): if not isinstance(event, GroupMessageEvent): - await buy_stock.finish("这个游戏只能在群里玩哦") + await clear_stock.finish(await to_pic_msg("这个游戏只能在群里玩哦")) msg = arg.extract_plain_text().strip().split() if len(msg) < 1: - await buy_stock.finish("格式错误,请输入强制清仓 qq号") + await buy_stock.finish(await to_pic_msg("格式错误,请输入强制清仓 qq号")) cnt = await force_clear_action(int(msg[0]), event.group_id) - await buy_stock.finish(MessageSegment.image(await text_to_pic(f"{msg[0]}的{cnt}仓位都被卖了", width=300))) + await buy_stock.finish(await to_pic_msg(f"{msg[0]}的{cnt}仓位都被卖了", width=300)) @revert_stock.handle() async def _(event: MessageEvent, arg: Message = CommandArg()): if not isinstance(event, GroupMessageEvent): - await revert_stock.finish("这个游戏只能在群里玩哦") + await revert_stock.finish(await to_pic_msg("这个游戏只能在群里玩哦")) msg = arg.extract_plain_text().strip().split() if len(msg) < 1: - await revert_stock.finish("格式错误,请输入反转持仓 股票代码") + await revert_stock.finish(await to_pic_msg("格式错误,请输入反转持仓 股票代码")) stock_id = fill_stock_id(msg[0]) msg = await revert_stock_action(event.user_id, event.group_id, stock_id) - await revert_stock.finish(msg) + await revert_stock.finish(await to_pic_msg(msg)) + + +async def to_pic_msg(msg, width=300): + return MessageSegment.image(await text_to_pic(msg, width=width)) @help_stock.handle() @@ -253,18 +270,44 @@ async def _(): await help_stock.finish( """作者:小r 说明:这个插件可以帮多年后的你省很多钱!练习到每天盈利5%+就可以去玩真正的股市了 -版本:v1.96 +版本:v2.0 查看是否有更新:https://github.com/RShock/zhenxun_plugin_stock_legend""") # 躺平基金是给不会炒股的人(以及周六日)玩的基金,每天收益为1.5%(默认) # 虽然看起来很高但是实际上30天也就1.56倍,可以接受 -async def buy_lazy_handle(bot, msg, event) -> str: +async def buy_lazy_handle(bot, msg, event) -> None: cost = 10 if len(msg) <= 1 else float(msg[1]) await bot.finish(MessageSegment.image( await text_to_pic( await buy_lazy_stock_action(event.user_id, event.group_id, cost), width=300))) -async def sell_lazy_handle(bot, percent, event) -> str: - await bot.finish(await sell_lazy_stock_action(event.user_id, event.group_id, percent)) +async def sell_lazy_handle(bot, percent, event) -> None: + tmp = await sell_lazy_stock_action(event.user_id, event.group_id, percent) + await bot.finish(await to_pic_msg(tmp, width=400)) + + +@query_stock.handle() +async def _(event: MessageEvent, arg: Message = CommandArg()): + if not isinstance(event, GroupMessageEvent): + await revert_stock.finish(await to_pic_msg("这个游戏只能在群里玩哦")) + + msg = arg.extract_plain_text().strip().split() + if len(msg) < 1: + await revert_stock.finish(await to_pic_msg("格式错误,请输入查看股票 股票/基金代码", width=300)) + await query_stock.send(await to_pic_msg("正在查询...", width=200)) + stock_id = fill_stock_id(msg[0]) + await query_stock.finish(await get_stock_img_(msg[0], stock_id)) + + +async def get_stock_img_(origin_stock_id, stock_id): + if Config.get_config("stock_legend", "IMAGE_MODE", 2) == 2: + return await get_stock_img_v2(origin_stock_id, stock_id) + else: + return await get_stock_img(origin_stock_id, stock_id) + + +async def get_username(bot, group_id, user_id): + user_name = await bot.get_group_member_info(group_id=group_id, user_id=user_id) + return user_name["card"] or user_name["nickname"] diff --git a/data_source.py b/data_source.py index 36616db..6246e35 100644 --- a/data_source.py +++ b/data_source.py @@ -79,7 +79,7 @@ async def buy_stock_action(user_id: int, group_id: int, stock_id: str, gearing: f'当前持仓价值 {round((query.number * price - query.cost) * query.gearing + query.cost, 2)}\n' \ f'当前持仓成本 {round(query.cost, 2)}\n' \ f'杠杆比率 {query.gearing}\n' \ - f'剩余资金 {have_gold - cost}' + f'剩余资金 {round(have_gold - origin_cost)}' else: return f"成功购买了 {round(num / 100, 2)} 手 {name}\n" \ f"现价 {price}亓\n" \ @@ -87,7 +87,7 @@ async def buy_stock_action(user_id: int, group_id: int, stock_id: str, gearing: f"当前持仓价值 {round((query.number * price - query.cost) * query.gearing + query.cost, 2)}\n" \ f"当前持仓成本 {round(query.cost, 2)}\n" \ f"杠杆比率 {query.gearing}\n" \ - f"剩余资金 {have_gold - cost}" + f"剩余资金 {round(have_gold - cost)}" # 快速清仓指令 diff --git a/utils.py b/utils.py index b3c8919..2e97858 100644 --- a/utils.py +++ b/utils.py @@ -1,15 +1,20 @@ import time import urllib.request +from pathlib import Path from nonebot.adapters.onebot.v11 import Bot, GroupMessageEvent +from playwright.async_api import async_playwright from rfc3986.compat import to_str from configs.config import Config from configs.path_config import IMAGE_PATH +from utils.message_builder import image from .stock_model import StockDB from services import logger from utils.http_utils import AsyncPlaywright +import re + # 股票名称: infolist[1] # 股票代码: infolist[2] @@ -20,19 +25,34 @@ # 成交额(万):infolist[7] # 第一个参数是股票原始ID,第二个是加工后的(增加了2个字母的前缀) # 百度股市通能获取所有截图 -def get_stock_info(num: str) -> list: - if num == '躺平基金': +def get_stock_info(stock_id: str) -> list: + if stock_id == '躺平基金': return ['躺平基金', '躺平基金', 1, 1, 1, 1, 1, 1] - if not num.isascii() or not num.isprintable(): + if not stock_id.isascii() or not stock_id.isprintable(): return [] - f = urllib.request.urlopen('http://qt.gtimg.cn/q=s_' + to_str(num)) + p = re.compile(r'[j|J]\d+') # 日股代码正则 + if p.match(stock_id): + return get_jp_stock_info(stock_id) + f = urllib.request.urlopen('http://qt.gtimg.cn/q=s_' + to_str(stock_id)) # return like: v_s_sz000858="51~五 粮 液~000858~18.10~0.01~0.06~94583~17065~~687.07"; - strGB:str = f.readline().decode('gb2312') + strGB: str = f.readline().decode('gb2312') f.close() infolist = strGB[strGB.find("\""):-3] return infolist.split('~') +def get_jp_stock_info(jp_stock_id): + url = 'https://histock.tw/stock/module/stockdata.aspx?no=J7951' + # Request Data + data = dict( + # 参数 + no='J7951' + ) + response = requests.post(url, data) + print(response) # 请求状态 + print(response.content) # 返回结果 + + # 判断是不是a股,因为上海深圳股票有涨跌停 def is_a_stock(stock_id): return stock_id.startswith("sh") or stock_id.startswith("sz") @@ -150,9 +170,10 @@ def to_json(stock): ) -def convert_stocks_to_md_table(stocks): - result = "|名称 |代码|持仓数量|现价|成本|杠杆比例|花费|当前价值|建仓时间|\n" \ - "| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n" +def convert_stocks_to_md_table(username, stocks): + result = f'### {username}的持仓\n'\ + '|名称|代码|持仓数量|现价|成本|杠杆比例|花费|当前价值|建仓时间|\n' \ + '| --- | --- | --- | --- | --- | --- | --- | --- | --- |\n' def to_md(s): # 染色 @@ -164,8 +185,20 @@ def to_md(s): return f"|{s['name']}|{s['code']}|{s['number']}|{s['price_now']}|{s['price_cost']}|{s['gearing']}" \ f"|{s['cost']}|{s['value']}({s['rate']})|{s['create_time']}|\n" + total_value = 0 + total_cost = 0 for stock in stocks: + total_value += float(stock['value']) + total_cost += float(stock['cost']) result += to_md(stock) + dif = round(total_value - total_cost, 1) + if dif >= 0: + dif = f"{dif}" + else: + dif = f"{dif}" + total_value = round(total_value, 2) + total_cost = round(total_cost, 2) + result += f"|总计||||||{total_cost}|{total_value}|{dif}|" return result @@ -199,3 +232,52 @@ def get_tang_ping_earned(stock: StockDB, percent: float) -> (int, float, int): tang_ping = float(Config.get_config("stock_legend", "TANG_PING", 5)) rate = ((1 + tang_ping) ** day) # 翻倍数 return day, rate, round(stock.number * rate * percent / 10) + + +# 采用东财 图像更专业 +async def get_stock_img_v2(origin_stock_id: str, stock_id: str, is_detail: bool = False): + is_fund = False # 基金特判 + if len(origin_stock_id) == 5 and origin_stock_id.isdigit(): + url = f"http://quote.eastmoney.com/hk/{origin_stock_id}.html" + tar = "//div[contains(@class,'quote3l')][2]//div[@class='quote3l_c']" + elif stock_id.startswith("us"): + url = f"http://quote.eastmoney.com/us/{origin_stock_id}.html" + tar = "//div[contains(@class,'quote3l')][2]//div[@class='quote3l_c']" + elif origin_stock_id == "IXIC": # 纳斯达克指数 还有很多同类指数实在是搞不过来 建议直接去买对应基金 + url = "https://gushitong.baidu.com/index/us-IXIC" + tar = ".fac" + # 国债r001系列(购买这个系列完全是作弊,不禁止的原因是,希望有人能通过这个游戏学习股市,最后发现这个(直接看这段文字的不算数)) + # 真发现了,可以先约定不许买 + elif origin_stock_id.startswith('13'): + url = f"http://quote.eastmoney.com/bond/{stock_id}.html" + tar = "//div[contains(@class,'quote2l_cr2_m')]" + elif stock_id.startswith('jj'): # 基金 + url = f"https://fund.eastmoney.com/{stock_id[2:]}.html" + is_fund = True + else: # 其他ab股 + url = f"http://quote.eastmoney.com/{stock_id}.html" + tar = "//div[@id='js_box']" + async with async_playwright() as pw: + browser = await pw.chromium.launch( + headless=True, + ) + + page = await browser.new_page() + logger.info(url) + await page.goto(url) + + path = f"{IMAGE_PATH}/stock_legend/stockImg_{stock_id}_{time.time()}.png" + if is_fund: + viewport_size = dict(width=1200, height=3400) + await page.set_viewport_size(viewport_size) + tmp = page.locator("#hq_ip_tips >> text=立即开启") + if tmp: + await tmp.click() + await page.wait_for_timeout(1000) + await page.screenshot(path=path, timeout=10000, clip={"x": 0, "width": 780, "y": 700, "height": 2400}) + else: + page = await page.wait_for_selector(tar, timeout=10000) + await page.screenshot(path=path, timeout=10000) + + await browser.close() + return image(Path(path))