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))