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  }