feat: 添加新股通知

This commit is contained in:
lixiangwuxian 2024-11-08 00:42:41 +08:00
parent 2efcdc942b
commit 033e1b041c
4 changed files with 450 additions and 0 deletions

138
handler/newbond/model.go Normal file
View File

@ -0,0 +1,138 @@
package newbond
import (
"database/sql/driver"
"fmt"
"time"
)
type BondData struct {
SecurityCode string `json:"SECURITY_CODE" db:"SecurityCode"`
SecuCode string `json:"SECUCODE" db:"SecuCode"`
TradeMarket string `json:"TRADE_MARKET" db:"TradeMarket"`
SecurityNameAbbr string `json:"SECURITY_NAME_ABBR" db:"SecurityNameAbbr"`
DelistDate *CustomTime `json:"DELIST_DATE" db:"DelistDate"`
ListingDate *CustomTime `json:"LISTING_DATE" db:"ListingDate"`
ConvertStockCode string `json:"CONVERT_STOCK_CODE" db:"ConvertStockCode"`
BondExpire string `json:"BOND_EXPIRE" db:"BondExpire"`
Rating string `json:"RATING" db:"Rating"`
ValueDate CustomTime `json:"VALUE_DATE" db:"ValueDate"`
IssueYear string `json:"ISSUE_YEAR" db:"IssueYear"`
CeaseDate CustomTime `json:"CEASE_DATE" db:"CeaseDate"`
ExpireDate CustomTime `json:"EXPIRE_DATE" db:"ExpireDate"`
PayInterestDay string `json:"PAY_INTEREST_DAY" db:"PayInterestDay"`
InterestRateExplain string `json:"INTEREST_RATE_EXPLAIN" db:"InterestRateExplain"`
BondCombineCode string `json:"BOND_COMBINE_CODE" db:"BondCombineCode"`
ActualIssueScale float64 `json:"ACTUAL_ISSUE_SCALE" db:"ActualIssueScale"`
IssuePrice float64 `json:"ISSUE_PRICE" db:"IssuePrice"`
Remark string `json:"REMARK" db:"Remark"`
ParValue float64 `json:"PAR_VALUE" db:"ParValue"`
IssueObject string `json:"ISSUE_OBJECT" db:"IssueObject"`
RedeemType *string `json:"REDEEM_TYPE" db:"RedeemType"`
ExecuteReasonHS *string `json:"EXECUTE_REASON_HS" db:"ExecuteReasonHS"`
NoticeDateHS *CustomTime `json:"NOTICE_DATE_HS" db:"NoticeDateHS"`
NoticeDateSH *CustomTime `json:"NOTICE_DATE_SH" db:"NoticeDateSH"`
ExecutePriceHS *float64 `json:"EXECUTE_PRICE_HS" db:"ExecutePriceHS"`
ExecutePriceSH *float64 `json:"EXECUTE_PRICE_SH" db:"ExecutePriceSH"`
RecordDateSH *CustomTime `json:"RECORD_DATE_SH" db:"RecordDateSH"`
ExecuteStartDateSH *CustomTime `json:"EXECUTE_START_DATESH" db:"ExecuteStartDateSH"`
ExecuteStartDateHS *CustomTime `json:"EXECUTE_START_DATEHS" db:"ExecuteStartDateHS"`
ExecuteEndDate *CustomTime `json:"EXECUTE_END_DATE" db:"ExecuteEndDate"`
CorreCode string `json:"CORRECODE" db:"CorreCode"`
CorreCodeNameAbbr string `json:"CORRECODE_NAME_ABBR" db:"CorreCodeNameAbbr"`
PublicStartDate CustomTime `json:"PUBLIC_START_DATE" db:"PublicStartDate"`
CorreCodeO string `json:"CORRECODEO" db:"CorreCodeO"`
CorreCodeNameAbbrO string `json:"CORRECODE_NAME_ABBRO" db:"CorreCodeNameAbbrO"`
BondStartDate CustomTime `json:"BOND_START_DATE" db:"BondStartDate"`
SecurityStartDate CustomTime `json:"SECURITY_START_DATE" db:"SecurityStartDate"`
SecurityShortName string `json:"SECURITY_SHORT_NAME" db:"SecurityShortName"`
FirstPerPreplacing float64 `json:"FIRST_PER_PREPLACING" db:"FirstPerPreplacing"`
OnlineGeneralAAU float64 `json:"ONLINE_GENERAL_AAU" db:"OnlineGeneralAAU"`
OnlineGeneralLWR float64 `json:"ONLINE_GENERAL_LWR" db:"OnlineGeneralLWR"`
InitialTransferPrice float64 `json:"INITIAL_TRANSFER_PRICE" db:"InitialTransferPrice"`
TransferEndDate CustomTime `json:"TRANSFER_END_DATE" db:"TransferEndDate"`
TransferStartDate CustomTime `json:"TRANSFER_START_DATE" db:"TransferStartDate"`
ResaleClause string `json:"RESALE_CLAUSE" db:"ResaleClause"`
RedeemClause string `json:"REDEEM_CLAUSE" db:"RedeemClause"`
PartyName string `json:"PARTY_NAME" db:"PartyName"`
ConvertStockPrice interface{} `json:"CONVERT_STOCK_PRICE" db:"ConvertStockPrice"`
TransferPrice float64 `json:"TRANSFER_PRICE" db:"TransferPrice"`
TransferValue float64 `json:"TRANSFER_VALUE" db:"TransferValue"`
CurrentBondPrice interface{} `json:"CURRENT_BOND_PRICE" db:"CurrentBondPrice"`
TransferPremiumRatio float64 `json:"TRANSFER_PREMIUM_RATIO" db:"TransferPremiumRatio"`
ConvertStockPriceHQ *float64 `json:"CONVERT_STOCK_PRICEHQ" db:"ConvertStockPriceHQ"`
Market *string `json:"MARKET" db:"Market"`
ResaleTrigPrice float64 `json:"RESALE_TRIG_PRICE" db:"ResaleTrigPrice"`
RedeemTrigPrice float64 `json:"REDEEM_TRIG_PRICE" db:"RedeemTrigPrice"`
PBVRatio float64 `json:"PBV_RATIO" db:"PBVRatio"`
IBStartDate CustomTime `json:"IB_START_DATE" db:"IBStartDate"`
IBEndDate CustomTime `json:"IB_END_DATE" db:"IBEndDate"`
CashflowDate CustomTime `json:"CASHFLOW_DATE" db:"CashflowDate"`
CouponIR float64 `json:"COUPON_IR" db:"CouponIR"`
ParamName string `json:"PARAM_NAME" db:"ParamName"`
IssueType string `json:"ISSUE_TYPE" db:"IssueType"`
ExecuteReasonSH *string `json:"EXECUTE_REASON_SH" db:"ExecuteReasonSH"`
PaydayNew string `json:"PAYDAYNEW" db:"PaydayNew"`
CurrentBondPriceNew interface{} `json:"CURRENT_BOND_PRICENEW" db:"CurrentBondPriceNew"`
IsConvertStock string `json:"IS_CONVERT_STOCK" db:"IsConvertStock"`
IsRedeem string `json:"IS_REDEEM" db:"IsRedeem"`
IsSellback string `json:"IS_SELLBACK" db:"IsSellback"`
FirstProfit *float64 `json:"FIRST_PROFIT" db:"FirstProfit"`
}
type CustomTime struct {
time.Time
}
// 实现 driver.Valuer 接口
func (ct CustomTime) Value() (driver.Value, error) {
return ct.Time, nil
}
// 实现 sql.Scanner 接口
func (ct *CustomTime) Scan(value interface{}) error {
if value == nil {
ct.Time = time.Time{}
return nil
}
switch v := value.(type) {
case time.Time:
ct.Time = v
return nil
default:
return fmt.Errorf("cannot scan type %T into CustomTime", value)
}
}
// 实现 UnmarshalJSON 方法以支持自定义时间格式
func (ct *CustomTime) UnmarshalJSON(b []byte) error {
str := string(b)
str = str[1 : len(str)-1] // 去掉引号
if str == "null" {
return nil
}
// 使用正确的时间格式进行解析
parsedTime, err := time.Parse("2006-01-02 15:04:05", str)
if err != nil {
return err
}
ct.Time = parsedTime
return nil
}
type BondResponse struct {
Version string `json:"version"`
Result BondResult `json:"result"`
Success bool `json:"success"`
Message string `json:"message"`
Code int `json:"code"`
}
type BondResult struct {
Pages int `json:"pages"`
Data []BondData `json:"data"`
Count int `json:"count"`
}

View File

@ -0,0 +1,44 @@
package newbond
import (
"git.lxtend.com/qqbot/constants"
"git.lxtend.com/qqbot/handler"
"git.lxtend.com/qqbot/model"
)
func init() {
handler.RegisterHandler("监听新债", listenBond, constants.LEVEL_USER)
handler.RegisterHelpInform("监听新债", "监听新债 开启新债监听")
handler.RegisterHandler("取消监听新债", unListenBond, constants.LEVEL_USER)
handler.RegisterHelpInform("取消监听新债", "取消监听新债 关闭新债监听")
}
func listenBond(msg model.Message) (reply model.Reply) {
if err := AddGroupListen(int(msg.GroupInfo.GroupId)); err != nil {
return model.Reply{
ReplyMsg: "开启新债监听失败,报错: " + err.Error(),
ReferOriginMsg: true,
FromMsg: msg,
}
}
return model.Reply{
ReplyMsg: "开启新债监听成功",
ReferOriginMsg: true,
FromMsg: msg,
}
}
func unListenBond(msg model.Message) (reply model.Reply) {
if err := RemoveGroupListen(int(msg.GroupInfo.GroupId)); err != nil {
return model.Reply{
ReplyMsg: "关闭新债监听失败,报错: " + err.Error(),
ReferOriginMsg: true,
FromMsg: msg,
}
}
return model.Reply{
ReplyMsg: "关闭新债监听成功",
ReferOriginMsg: true,
FromMsg: msg,
}
}

267
handler/newbond/service.go Normal file
View File

@ -0,0 +1,267 @@
package newbond
import (
"database/sql"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"git.lxtend.com/qqbot/action"
"git.lxtend.com/qqbot/model"
"git.lxtend.com/qqbot/sqlite3"
)
func init() {
createNewBondListenGroupTableSQL := `
CREATE TABLE IF NOT EXISTS new_bond_listen_group (
group_id INT PRIMARY KEY
);
`
createNewBondInfoTableSQL := `CREATE TABLE BondData (
SecurityCode VARCHAR(10) NOT NULL,
SecuCode VARCHAR(15) NOT NULL,
TradeMarket VARCHAR(10) NOT NULL,
SecurityNameAbbr VARCHAR(50) NOT NULL,
DelistDate DATETIME NULL,
ListingDate DATETIME NULL,
ConvertStockCode VARCHAR(10) NOT NULL,
BondExpire VARCHAR(5) NOT NULL,
Rating VARCHAR(5) NOT NULL,
ValueDate DATETIME NOT NULL,
IssueYear VARCHAR(4) NOT NULL,
CeaseDate DATETIME NOT NULL,
ExpireDate DATETIME NOT NULL,
PayInterestDay VARCHAR(10) NOT NULL,
InterestRateExplain TEXT NOT NULL,
BondCombineCode VARCHAR(20) NOT NULL,
ActualIssueScale DECIMAL(10, 2) NOT NULL,
IssuePrice DECIMAL(10, 2) NOT NULL,
Remark TEXT NOT NULL,
ParValue DECIMAL(10, 2) NOT NULL,
IssueObject TEXT NOT NULL,
RedeemType VARCHAR(50) NULL,
ExecuteReasonHS VARCHAR(255) NULL,
NoticeDateHS DATETIME NULL,
NoticeDateSH DATETIME NULL,
ExecutePriceHS DECIMAL(10, 2) NULL,
ExecutePriceSH DECIMAL(10, 2) NULL,
RecordDateSH DATETIME NULL,
ExecuteStartDateSH DATETIME NULL,
ExecuteStartDateHS DATETIME NULL,
ExecuteEndDate DATETIME NULL,
CorreCode VARCHAR(10) NOT NULL,
CorreCodeNameAbbr VARCHAR(50) NOT NULL,
PublicStartDate DATETIME NOT NULL,
CorreCodeO VARCHAR(10) NOT NULL,
CorreCodeNameAbbrO VARCHAR(50) NOT NULL,
BondStartDate DATETIME NOT NULL,
SecurityStartDate DATETIME NOT NULL,
SecurityShortName VARCHAR(50) NOT NULL,
FirstPerPreplacing DECIMAL(10, 4) NOT NULL,
OnlineGeneralAAU DECIMAL(10, 2) NOT NULL,
OnlineGeneralLWR DECIMAL(20, 10) NOT NULL,
InitialTransferPrice DECIMAL(10, 2) NOT NULL,
TransferEndDate DATETIME NOT NULL,
TransferStartDate DATETIME NOT NULL,
ResaleClause TEXT NOT NULL,
RedeemClause TEXT NOT NULL,
PartyName VARCHAR(100) NOT NULL,
ConvertStockPrice DECIMAL(10, 2) NOT NULL,
TransferPrice DECIMAL(10, 2) NOT NULL,
TransferValue DECIMAL(10, 4) NOT NULL,
CurrentBondPrice VARCHAR(10) NOT NULL,
TransferPremiumRatio DECIMAL(10, 2) NOT NULL,
ConvertStockPriceHQ DECIMAL(10, 2) NULL,
Market VARCHAR(50) NULL,
ResaleTrigPrice DECIMAL(10, 2) NOT NULL,
RedeemTrigPrice DECIMAL(10, 2) NOT NULL,
PBVRatio DECIMAL(10, 2) NOT NULL,
IBStartDate DATETIME NOT NULL,
IBEndDate DATETIME NOT NULL,
CashflowDate DATETIME NOT NULL,
CouponIR DECIMAL(10, 2) NOT NULL,
ParamName TEXT NOT NULL,
IssueType VARCHAR(10) NOT NULL,
ExecuteReasonSH VARCHAR(255) NULL,
PaydayNew VARCHAR(10) NOT NULL,
CurrentBondPriceNew DECIMAL(10, 2) NOT NULL,
IsConvertStock VARCHAR(10) NOT NULL,
IsRedeem VARCHAR(10) NOT NULL,
IsSellback VARCHAR(10) NOT NULL,
FirstProfit DECIMAL(10, 2) NULL,
PRIMARY KEY (SecurityCode)
);
`
sqlite3.TryCreateTable(createNewBondListenGroupTableSQL)
sqlite3.TryCreateTable(createNewBondInfoTableSQL)
go RoundCheckNewBond()
}
func GetBondsData() ([]BondData, error) {
url := "https://datacenter-web.eastmoney.com/api/data/v1/get?sortColumns=PUBLIC_START_DATE%2CSECURITY_CODE&sortTypes=-1%2C-1&pageSize=50&pageNumber=1&reportName=RPT_BOND_CB_LIST&columns=ALL&quoteColumns=f2~01~CONVERT_STOCK_CODE~CONVERT_STOCK_PRICE%2Cf235~10~SECURITY_CODE~TRANSFER_PRICE%2Cf236~10~SECURITY_CODE~TRANSFER_VALUE%2Cf2~10~SECURITY_CODE~CURRENT_BOND_PRICE%2Cf237~10~SECURITY_CODE~TRANSFER_PREMIUM_RATIO%2Cf239~10~SECURITY_CODE~RESALE_TRIG_PRICE%2Cf240~10~SECURITY_CODE~REDEEM_TRIG_PRICE%2Cf23~01~CONVERT_STOCK_CODE~PBV_RATIO&quoteType=0&source=WEB&client=WEB"
resp, err := http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("status code %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var apiResponse BondResponse
err = json.Unmarshal(body, &apiResponse)
if err != nil {
return nil, err
}
return apiResponse.Result.Data, nil
}
func AddGroupListen(groupID int) error {
tx, err := sqlite3.GetTran()
if err != nil {
return err
}
defer tx.Rollback()
tx.Exec("INSERT INTO new_bond_listen_group (group_id) VALUES (?)", groupID)
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func RemoveGroupListen(groupID int) error {
tx, err := sqlite3.GetTran()
if err != nil {
return err
}
defer tx.Rollback()
tx.Exec("DELETE FROM new_bond_listen_group WHERE group_id = ?", groupID)
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func BondDataExists(securityCode string) (bool, error) {
db := sqlite3.GetDB()
var count int
if err := db.Get(&count, "SELECT COUNT(*) FROM BondData WHERE SecurityCode = ?", securityCode); err != nil {
return false, err
}
return count > 0, nil
}
func AddBondData(data BondData) error {
tx, err := sqlite3.GetTran()
if err != nil {
return err
}
defer tx.Rollback()
query := `INSERT INTO BondData (
SecurityCode, SecuCode, TradeMarket, SecurityNameAbbr, DelistDate,
ListingDate, ConvertStockCode, BondExpire, Rating, ValueDate,
IssueYear, CeaseDate, ExpireDate, PayInterestDay, InterestRateExplain,
BondCombineCode, ActualIssueScale, IssuePrice, Remark, ParValue,
IssueObject, RedeemType, ExecuteReasonHS, NoticeDateHS, NoticeDateSH,
ExecutePriceHS, ExecutePriceSH, RecordDateSH, ExecuteStartDateSH, ExecuteStartDateHS,
ExecuteEndDate, CorreCode, CorreCodeNameAbbr, PublicStartDate, CorreCodeO,
CorreCodeNameAbbrO, BondStartDate, SecurityStartDate, SecurityShortName, FirstPerPreplacing,
OnlineGeneralAAU, OnlineGeneralLWR, InitialTransferPrice, TransferEndDate, TransferStartDate,
ResaleClause, RedeemClause, PartyName, ConvertStockPrice, TransferPrice,
TransferValue, CurrentBondPrice, TransferPremiumRatio, ConvertStockPriceHQ, Market,
ResaleTrigPrice, RedeemTrigPrice, PBVRatio, IBStartDate, IBEndDate,
CashflowDate, CouponIR, ParamName, IssueType, ExecuteReasonSH,
PaydayNew, CurrentBondPriceNew, IsConvertStock, IsRedeem, IsSellback,
FirstProfit
) VALUES (
:SecurityCode, :SecuCode, :TradeMarket, :SecurityNameAbbr, :DelistDate,
:ListingDate, :ConvertStockCode, :BondExpire, :Rating, :ValueDate,
:IssueYear, :CeaseDate, :ExpireDate, :PayInterestDay, :InterestRateExplain,
:BondCombineCode, :ActualIssueScale, :IssuePrice, :Remark, :ParValue,
:IssueObject, :RedeemType, :ExecuteReasonHS, :NoticeDateHS, :NoticeDateSH,
:ExecutePriceHS, :ExecutePriceSH, :RecordDateSH, :ExecuteStartDateSH, :ExecuteStartDateHS,
:ExecuteEndDate, :CorreCode, :CorreCodeNameAbbr, :PublicStartDate, :CorreCodeO,
:CorreCodeNameAbbrO, :BondStartDate, :SecurityStartDate, :SecurityShortName, :FirstPerPreplacing,
:OnlineGeneralAAU, :OnlineGeneralLWR, :InitialTransferPrice, :TransferEndDate, :TransferStartDate,
:ResaleClause, :RedeemClause, :PartyName, :ConvertStockPrice, :TransferPrice,
:TransferValue, :CurrentBondPrice, :TransferPremiumRatio, :ConvertStockPriceHQ, :Market,
:ResaleTrigPrice, :RedeemTrigPrice, :PBVRatio, :IBStartDate, :IBEndDate,
:CashflowDate, :CouponIR, :ParamName, :IssueType, :ExecuteReasonSH,
:PaydayNew, :CurrentBondPriceNew, :IsConvertStock, :IsRedeem, :IsSellback,
:FirstProfit
)`
_, err = tx.NamedExec(query, data)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
func GetGroupListens() ([]int, error) {
db := sqlite3.GetDB()
var groupIDs []int
if err := db.Select(&groupIDs, "SELECT group_id FROM new_bond_listen_group"); err != nil && err != sql.ErrNoRows {
return nil, err
}
return groupIDs, nil
}
func RoundCheckNewBond() {
time.Sleep(5 * time.Second)
for !action.ActionManager.Started() {
time.Sleep(5 * time.Second)
}
once := true
for {
if !once {
time.Sleep(5 * time.Minute)
}
bonds, err := GetBondsData()
if bonds == nil || err != nil {
fmt.Println("Error getting bonds data:", err)
continue
}
groups, err := GetGroupListens()
if err != nil {
fmt.Println("Error getting group listens:", err)
continue
}
for _, bond := range bonds {
exists, err := BondDataExists(bond.SecurityCode)
if err != nil {
fmt.Println("Error checking bond data exists:", err)
continue
}
if !exists && bond.ListingDate == nil {
for _, group := range groups {
msg := model.Reply{
ReplyMsg: fmt.Sprintf("号外号外,%s开始申购了", bond.SecurityNameAbbr),
ReferOriginMsg: false,
FromMsg: model.Message{
GroupInfo: model.GroupInfo{
GroupId: int64(group),
},
},
}
action.ActionManager.SendMsg(msg)
}
}
AddBondData(bond)
}
once = false
}
}

View File

@ -12,6 +12,7 @@ import (
_ "git.lxtend.com/qqbot/handler/help"
_ "git.lxtend.com/qqbot/handler/jrrp"
_ "git.lxtend.com/qqbot/handler/kw"
_ "git.lxtend.com/qqbot/handler/newbond"
_ "git.lxtend.com/qqbot/handler/restart"
_ "git.lxtend.com/qqbot/handler/roll"
_ "git.lxtend.com/qqbot/handler/scoresaber"