gitee.com/quant1x/engine@v1.8.4/datasource/dfcf/notices.go (about) 1 package dfcf 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "gitee.com/quant1x/engine/utils" 7 "gitee.com/quant1x/exchange" 8 "gitee.com/quant1x/gox/exception" 9 "gitee.com/quant1x/gox/http" 10 "gitee.com/quant1x/num" 11 "math" 12 urlpkg "net/url" 13 "strings" 14 ) 15 16 const ( 17 CacheL5KeyNotices = "cache/notices" 18 urlEastmoneyNotices = "https://np-anotice-stock.eastmoney.com/api/security/ann" 19 EastmoneyNoticesPageSize = 100 20 errorBaseNotice = 91000 21 ) 22 23 var ( 24 ErrNoticeBadApi = exception.New(errorBaseNotice, "接口异常") 25 ErrNoticeNotFound = exception.New(errorBaseNotice+1, "没有数据") 26 ) 27 28 var ( 29 // 风险检测的关键词 30 riskKeywords = []string{"立案", "处罚", "冻结", "诉讼", "质押", "仲裁", "持股5%以上股东权益变动", "信用减值", "商誉减值", "重大风险", "退市风险"} 31 ) 32 33 type EMNoticeType = int 34 35 const ( 36 NoticeAll EMNoticeType = iota // 全部 37 NoticeUnused1 // 财务报告 38 NoticeUnused2 // 融资公告 39 NoticeUnused3 // 风险提示 40 NoticeUnused4 // 信息变更 41 NoticeWarning // 重大事项 42 NoticeUnused6 // 资产重组 43 NoticeHolderChange // 持股变动 44 ) 45 46 func GetNoticeType(noticeType EMNoticeType) string { 47 switch noticeType { 48 case NoticeAll: 49 return "全部" 50 case NoticeUnused1: 51 return "财务报告" 52 case NoticeUnused2: 53 return "融资公告" 54 case NoticeUnused3: 55 return "风险提示" 56 case NoticeUnused4: 57 return "信息变更" 58 case NoticeWarning: 59 return "重大事项" 60 case NoticeUnused6: 61 return "资产重组" 62 case NoticeHolderChange: 63 return "持股变动" 64 default: 65 return "其它" 66 } 67 } 68 69 // 公告原始的消息结构 70 type rawNoticePackage struct { 71 Data struct { 72 List []struct { 73 ArtCode string `json:"art_code"` 74 Codes []struct { 75 AnnType string `json:"ann_type"` 76 InnerCode string `json:"inner_code"` 77 MarketCode string `json:"market_code"` 78 ShortName string `json:"short_name"` 79 StockCode string `json:"stock_code"` 80 } `json:"codes"` 81 Columns []struct { 82 ColumnCode string `json:"column_code"` 83 ColumnName string `json:"column_name"` 84 } `json:"columns"` 85 DisplayTime string `json:"display_time"` 86 EiTime string `json:"eiTime"` 87 Language string `json:"language"` 88 NoticeDate string `json:"notice_date"` 89 ProductCode string `json:"product_code"` 90 SortDate string `json:"sort_date"` 91 SourceType string `json:"source_type"` 92 Title string `json:"title"` 93 TitleCh string `json:"title_ch"` 94 TitleEn string `json:"title_en"` 95 } `json:"list"` 96 PageIndex int `json:"page_index"` 97 PageSize int `json:"page_size"` 98 TotalHits int `json:"total_hits"` 99 } `json:"data"` 100 Error string `json:"error"` 101 Success int `json:"success"` 102 } 103 104 // NoticeDetail 公告详情 105 type NoticeDetail struct { 106 Code string `csv:"证券代码" dataframe:"证券代码"` // 证券代码 107 Name string `csv:"证券名称" dataframe:"证券名称"` // 证券名称 108 DisplayTime string `csv:"显示时间" dataframe:"显示时间"` // 显示时间 109 NoticeDate string `csv:"公告时间" dataframe:"公告时间"` // 公告时间 110 Title string `csv:"内容提要" dataframe:"公告标题"` // 公告标题 111 Keywords string `csv:"关键词" dataframe:"关键词"` // 公告关键词 112 Increase int `csv:"增持" dataframe:"增持"` // 增持 113 Reduce int `csv:"减持" dataframe:"减持"` // 减持 114 HolderChange int `csv:"控制人变更" dataframe:"控制人变更"` // 实际控制人变更 115 Risk int `csv:"风险数" dataframe:"监管"` // 风险数 116 } 117 118 // AllNotices 东方财富网-数据中心-公告大全-沪深京 A 股公告 119 // 120 // http://data.eastmoney.com/notices/hsa/5.html 121 // :param symbol: 报告类型; choice of {"全部", "重大事项", "财务报告", "融资公告", "风险提示", "资产重组", "信息变更", "持股变动"} 122 // :type symbol: str 123 // :param date: 制定日期 124 // :type date: str 125 // :return: 沪深京 A 股公告 126 // Deprecated: 弃用 127 func AllNotices(noticeType EMNoticeType, date string, pageNumber ...int) (notices []NoticeDetail, pages int, err error) { 128 pageNo := 1 129 if len(pageNumber) > 0 { 130 pageNo = pageNumber[0] 131 } 132 beginDate := exchange.FixTradeDate(date) 133 endDate := exchange.Today() 134 pageSize := EastmoneyNoticesPageSize 135 params := urlpkg.Values{ 136 "sr": {"-1"}, 137 "page_size": {fmt.Sprintf("%d", pageSize)}, 138 "page_index": {fmt.Sprintf("%d", pageNo)}, 139 "ann_type": {"SHA,CYB,SZA,BJA"}, 140 //"ann_type": {"A"}, 141 //"ann_type": {"SHA,SZA"}, 142 "client_source": {"web"}, 143 "f_node": {fmt.Sprintf("%d", noticeType)}, 144 "s_node": {"0"}, 145 "begin_time": {beginDate}, 146 "end_time": {endDate}, 147 //"cb": {"jQuery112305241416374967685_1683838825141"}, 148 } 149 // Host: np-anotice-stock.eastmoney.com 150 header := map[string]any{ 151 //"User-Agent": config.HTTP_REQUEST_HEADER_USER_AGENT, 152 //"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 153 } 154 url := urlEastmoneyNotices + "?" + params.Encode() 155 //url = "https://np-anotice-stock.eastmoney.com/api/security/ann?cb=jQuery112305241416374967685_1683838825141&sr=-1&page_size=50&page_index=1&ann_type=SHA%2CCYB%2CSZA%2CBJA&client_source=web&f_node=0&s_node=0" 156 data, _, err := http.Request(url, http.MethodGet, "", header) 157 if err != nil { 158 return 159 } 160 //fmt.Println(api.Bytes2String(data)) 161 var raw rawNoticePackage 162 err = json.Unmarshal(data, &raw) 163 if err != nil { 164 return 165 } 166 if raw.Success != 1 || len(raw.Data.List) == 0 { 167 err = ErrNoticeNotFound 168 return 169 } 170 pages = int(math.Ceil(float64(raw.Data.TotalHits) / float64(EastmoneyNoticesPageSize))) 171 172 for _, v := range raw.Data.List { 173 marketCode := exchange.MarketIdShenZhen 174 if len(v.Codes) == 0 || len(v.Columns) == 0 { 175 continue 176 } 177 code := v.Codes[0] 178 mc := strings.TrimSpace(code.MarketCode) 179 marketCode = exchange.MarketType(num.AnyToInt64(mc)) 180 securityCode := exchange.GetSecurityCode(marketCode, strings.TrimSpace(code.StockCode)) 181 securityName := strings.TrimSpace(code.ShortName) 182 //if securityCode == "sz300027" { 183 // fmt.Printf("\n%+v\n", v) 184 //} 185 notice := NoticeDetail{ 186 //Code string `dataframe:"证券代码"` // 证券代码 187 Code: securityCode, 188 //Name string `dataframe:"证券名称"` // 证券名称 189 Name: securityName, 190 //DisplayTime string `dataframe:"显示时间"` // 显示时间 191 DisplayTime: strings.TrimSpace(v.EiTime), 192 //DisplayTime: strings.TrimSpace(v.DisplayTime), 193 //NoticeDate string `dataframe:"公告时间"` // 公告时间 194 NoticeDate: strings.TrimSpace(v.NoticeDate), 195 //Title string `dataframe:"内容提要"` // 公告标题 196 Title: strings.TrimSpace(v.TitleCh), 197 //Keywords string `dataframe:"关键词"` // 公告关键词 198 //Increase int `dataframe:"增持"` // 增持 199 //Reduces int `dataframe:"减持"` // 减持 200 //HolderChange int `dataframe:"控制人变更"` // 实际控制人变更 201 } 202 noticeKeywords := []string{} 203 204 checkRisk := func(content string) { 205 key := "减持" 206 if strings.Contains(content, key) { 207 noticeKeywords = append(noticeKeywords, key) 208 notice.Reduce += 1 209 } 210 key = "增持" 211 if strings.Contains(content, key) { 212 noticeKeywords = append(noticeKeywords, key) 213 notice.Increase += 1 214 } 215 key = "控制人变更" 216 if strings.Contains(content, key) { 217 noticeKeywords = append(noticeKeywords, key) 218 notice.HolderChange += 1 219 } 220 for _, key := range riskKeywords { 221 if strings.Contains(content, key) { 222 noticeKeywords = append(noticeKeywords, key) 223 notice.Risk += 1 224 } 225 } 226 } 227 228 for _, words := range v.Columns { 229 //if securityCode == "sh600730" { 230 // fmt.Println(securityCode, words.ColumnName) 231 //} 232 checkRisk(words.ColumnName) 233 } 234 checkRisk(notice.Title) 235 if len(noticeKeywords) > 0 { 236 notice.Keywords = strings.Join(noticeKeywords, ",") 237 } 238 239 notices = append(notices, notice) 240 } 241 return notices, pages, nil 242 } 243 244 // StockNotices 个股公告 245 func StockNotices(securityCode, beginDate, endDate string, pageNumber ...int) (notices []NoticeDetail, pages int, err error) { 246 pageNo := 1 247 if len(pageNumber) > 0 { 248 pageNo = pageNumber[0] 249 } 250 beginDate = exchange.FixTradeDate(beginDate) 251 if len(endDate) > 0 { 252 endDate = exchange.FixTradeDate(endDate) 253 } else { 254 endDate = exchange.Today() 255 } 256 pageSize := EastmoneyNoticesPageSize 257 params := urlpkg.Values{ 258 "sr": {"-1"}, 259 "page_size": {fmt.Sprintf("%d", pageSize)}, 260 "page_index": {fmt.Sprintf("%d", pageNo)}, 261 //"ann_type": {"SHA,CYB,SZA,BJA"}, 262 "ann_type": {"A"}, 263 //"ann_type": {"SHA,SZA"}, 264 "client_source": {"web"}, 265 "f_node": {"0"}, 266 //"f_node": {fmt.Sprintf("%d", NoticeWarning)}, 267 "s_node": {"0"}, 268 "begin_time": {beginDate}, 269 "end_time": {endDate}, 270 //"cb": {"jQuery112305241416374967685_1683838825141"}, 271 } 272 _, _, symbol := exchange.DetectMarket(securityCode) 273 params.Add("stock_list", symbol) 274 // Host: np-anotice-stock.eastmoney.com 275 header := map[string]any{ 276 //"User-Agent": config.HTTP_REQUEST_HEADER_USER_AGENT, 277 //"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 278 } 279 url := urlEastmoneyNotices + "?" + params.Encode() 280 //url = "https://np-anotice-stock.eastmoney.com/api/security/ann?cb=jQuery112305241416374967685_1683838825141&sr=-1&page_size=50&page_index=1&ann_type=SHA%2CCYB%2CSZA%2CBJA&client_source=web&f_node=0&s_node=0" 281 data, err := http.Get(url, header) 282 if err != nil { 283 return 284 } 285 //fmt.Println(api.Bytes2String(data)) 286 var raw rawNoticePackage 287 err = json.Unmarshal(data, &raw) 288 if err != nil { 289 return 290 } 291 if raw.Success != 1 || len(raw.Data.List) == 0 { 292 err = ErrNoticeNotFound 293 return 294 } 295 //pages = int(math.Ceil(float64(raw.Data.TotalHits) / float64(EastmoneyNoticesPageSize))) 296 pages = utils.GetPages(pageSize, raw.Data.TotalHits) 297 298 for _, v := range raw.Data.List { 299 marketCode := exchange.MarketIdShenZhen 300 if len(v.Codes) == 0 || len(v.Columns) == 0 { 301 continue 302 } 303 code := v.Codes[0] 304 mc := strings.TrimSpace(code.MarketCode) 305 marketCode = exchange.MarketType(num.AnyToInt64(mc)) 306 securityCode := exchange.GetSecurityCode(marketCode, strings.TrimSpace(code.StockCode)) 307 securityName := strings.TrimSpace(code.ShortName) 308 //if securityCode == "sz300027" { 309 // fmt.Printf("\n%+v\n", v) 310 //} 311 notice := NoticeDetail{ 312 //Code string `dataframe:"证券代码"` // 证券代码 313 Code: securityCode, 314 //Name string `dataframe:"证券名称"` // 证券名称 315 Name: securityName, 316 //DisplayTime string `dataframe:"显示时间"` // 显示时间 317 DisplayTime: strings.TrimSpace(v.EiTime), 318 //DisplayTime: strings.TrimSpace(v.DisplayTime), 319 //NoticeDate string `dataframe:"公告时间"` // 公告时间 320 NoticeDate: strings.TrimSpace(v.NoticeDate), 321 //Title string `dataframe:"内容提要"` // 公告标题 322 Title: strings.TrimSpace(v.TitleCh), 323 //Keywords string `dataframe:"关键词"` // 公告关键词 324 //Increase int `dataframe:"增持"` // 增持 325 //Reduces int `dataframe:"减持"` // 减持 326 //HolderChange int `dataframe:"控制人变更"` // 实际控制人变更 327 } 328 noticeKeywords := []string{} 329 // 评估风险 330 checkRisk := func(content string) { 331 key := "减持" 332 if strings.Contains(content, key) { 333 noticeKeywords = append(noticeKeywords, key) 334 notice.Reduce += 1 335 } 336 key = "增持" 337 if strings.Contains(content, key) { 338 noticeKeywords = append(noticeKeywords, key) 339 notice.Increase += 1 340 } 341 key = "控制人变更" 342 if strings.Contains(content, key) { 343 noticeKeywords = append(noticeKeywords, key) 344 notice.HolderChange += 1 345 } 346 for _, key := range riskKeywords { 347 if strings.Contains(content, key) { 348 noticeKeywords = append(noticeKeywords, key) 349 notice.Risk += 1 350 } 351 } 352 } 353 354 for _, words := range v.Columns { 355 //if securityCode == "sh600730" { 356 // fmt.Println(securityCode, words.ColumnName) 357 //} 358 checkRisk(words.ColumnName) 359 } 360 checkRisk(notice.Title) 361 if len(noticeKeywords) > 0 { 362 notice.Keywords = strings.Join(noticeKeywords, ",") 363 } 364 365 notices = append(notices, notice) 366 } 367 return notices, pages, nil 368 } 369 370 //https://emweb.securities.eastmoney.com/pc_hsf10/pages/index.html?type=web&code=SH603045&color=b#/gsds 371 //https://datacenter.eastmoney.com/securities/api/data/get 372 //type: RTP_F10_DETAIL 373 //params: 603045.SH,02 374 //p: 1 375 //source: HSF10 376 //client: PC 377 //v: 07214522120592637 378 379 const ( 380 urlEastmoneyWarning = "https://datacenter.eastmoney.com/securities/api/data/get" 381 ) 382 383 type WarningDetail struct { 384 EventType string `json:"EVENT_TYPE"` // 事件类型 385 SpecificEventType string `json:"SPECIFIC_EVENTTYPE"` // 事件类型 386 NoticeDate string `json:"NOTICE_DATE"` // 公告日期 387 Level1Content string `json:"LEVEL1_CONTENT"` // 1级内容 388 Level2Content []string `json:"LEVEL2_CONTENT"` // 2级内容 389 InfoCode string `json:"INFO_CODE"` // 信息代码 390 } 391 392 type RawWarning struct { 393 Code int `json:"code"` // 状态码 394 Success bool `json:"success"` // 接口是否调用成功 395 Message string `json:"message"` // 状态信息 396 Data [][]WarningDetail `json:"data"` 397 HasNext int `json:"hasNext"` // 是否有下一页 398 } 399 400 // StockWarning 大事提醒 401 func StockWarning(securityCode string, pageNumber ...int) (warning RawWarning, err error) { 402 pageNo := 1 403 if len(pageNumber) > 0 { 404 pageNo = pageNumber[0] 405 } 406 _, flag, code := exchange.DetectMarket(securityCode) 407 flag = strings.ToUpper(flag) 408 // 全部大事, 重大事项, 业绩披露, 利润分配, 交易提示, 交易行为 409 // , 01, 02, 03, 04, 05 410 params := urlpkg.Values{ 411 "type": {"RTP_F10_DETAIL"}, 412 //"params": {fmt.Sprint(code, ".", flag)}, 413 "params": {fmt.Sprint(code, ".", flag, ",02")}, 414 "p": {fmt.Sprintf("%d", pageNo)}, 415 "ann_type": {"A"}, 416 "source": {"HSF10"}, 417 "client": {"PC"}, 418 } 419 // Host: np-anotice-stock.eastmoney.com 420 header := map[string]any{ 421 //"User-Agent": config.HTTP_REQUEST_HEADER_USER_AGENT, 422 //"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", 423 } 424 url := urlEastmoneyWarning + "?" + params.Encode() 425 data, err := http.Get(url, header) 426 if err != nil { 427 return 428 } 429 //fmt.Println(api.Bytes2String(data)) 430 var raw RawWarning 431 err = json.Unmarshal(data, &raw) 432 if err != nil { 433 return 434 } 435 if !raw.Success || len(raw.Data) == 0 { 436 err = ErrNoticeNotFound 437 return 438 } 439 440 return raw, nil 441 } 442 443 // 获取年报披露日期 444 // 445 // event_type: 报表披露, 业绩快报, 业务预告 446 // specific_eventtype: 年报披露, 年报预披露, x季报披露, x季报预披露, 中报披露, 业绩快报, 业绩预告 447 func getAnnualReportDate(year string, events []WarningDetail) (annualReportDate, quarterlyReportDate string) { 448 for _, v := range events { 449 date := exchange.FixTradeDate(v.NoticeDate) 450 tmpYear := date[0:4] 451 if v.EventType != "报表披露" { 452 continue 453 } 454 if len(annualReportDate) == 0 && (v.SpecificEventType == "年报披露" || v.SpecificEventType == "年报预披露") && tmpYear >= year { 455 annualReportDate = date 456 } else if len(quarterlyReportDate) == 0 && strings.HasSuffix(v.SpecificEventType, "季报披露") || strings.HasSuffix(v.SpecificEventType, "季报预披露") { 457 quarterlyReportDate = date 458 } 459 if len(annualReportDate) > 0 && len(quarterlyReportDate) > 0 { 460 break 461 } 462 // 去年的数据略过 463 if tmpYear < year { 464 break 465 } 466 } 467 return 468 } 469 470 // NoticeDateForReport 年报季报披露日期 471 func NoticeDateForReport(code string, date string) (annualReportDate, quarterlyReportDate string) { 472 date = exchange.FixTradeDate(date) 473 year := date[:4] 474 pageNo := 1 475 for { 476 warning, err := StockWarning(code, pageNo) 477 if err != nil { 478 break 479 } 480 for _, events := range warning.Data { 481 tmpYearReportDate, tmpQuarterlyReportDate := getAnnualReportDate(year, events) 482 if len(annualReportDate) == 0 && len(tmpYearReportDate) > 0 { 483 annualReportDate = tmpYearReportDate 484 } 485 if len(quarterlyReportDate) == 0 && len(tmpQuarterlyReportDate) > 0 { 486 quarterlyReportDate = tmpQuarterlyReportDate 487 } 488 if len(annualReportDate) > 0 && len(quarterlyReportDate) > 0 { 489 break 490 } 491 } 492 if len(annualReportDate) > 0 && len(quarterlyReportDate) > 0 { 493 break 494 } 495 if warning.HasNext > 0 { 496 pageNo++ 497 } else { 498 break 499 } 500 } 501 return 502 }