core/data/series.go
zhangkun9038@dingtalk.com 9a83607b2c test
2025-02-21 12:51:51 +08:00

567 lines
17 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package core
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"strconv"
"time"
"github.com/go-redis/redis"
logrus "github.com/sirupsen/logrus"
// "phyer.click/sardine/utils"
)
type Series struct {
InstID string `json:"instID"`
Period string `json:"Period"`
Count int `json:"count,number"`
Scale float64 `json:"scale,number"`
LastUpdateTime int64 `json:"lastUpdateTime,number"`
UpdateNickName string
LastCandle1m Candle `json:"lastCandle1m"`
CandleSeries *PixelList `json:"candleSerie"`
Ma7Series *PixelList `json:"ma7Serie"`
Ma30Series *PixelList `json:"ma30Serie"`
}
type SeriesInfo struct {
InstID string `json:"instID"`
Period string `json:"period"`
InsertedNew bool `json:"insertedNew,bool"`
Score float64 `json:"score,number"`
}
type SeriesInfoScore struct {
InstID string `json:"instID"`
Score float64 `json:"score,number"`
}
// TODO
// redis key: verticalReportItem|BTC-USDT|4H-15m|ts:1643002300000
// sortedSet: verticalLimit|2D-4H|rank|sortedSet
type VerticalReportItem struct {
InstID string
Period string
ReportTime int64
LastUpdate int64
LastUpdateTime string
Interval int
TrigerValue float64
AdvUpSellPrice float64
AdvDownSellPrice float64
Rank float64
ShearForce float64
VerticalElevation float64
SecondPeriod string
}
// type Segment struct {
// IndextStart int
// IndexEnd int
//
// }
// 根据instId 和period 从 PlateMap里拿到coaster创建对应的 series,
func (sr *Series) Refresh(cr *Core) error {
curCo, err := cr.GetCoasterFromPlate(sr.InstID, sr.Period)
if err != nil {
return err
}
ma30List := curCo.Ma30List.List
ma30len := len(ma30List)
if ma30len == 0 {
err = errors.New("ma30List is empty:" + sr.InstID + "," + sr.Period)
return err
}
baseMaX := ma30List[ma30len-1]
ma30Pxl, err := curCo.Ma30List.MakePixelList(cr, baseMaX, sr.Scale)
if err != nil {
return err
}
sr.Ma30Series = ma30Pxl
ma7Pxl, err := curCo.Ma7List.MakePixelList(cr, baseMaX, sr.Scale)
if err != nil {
return err
}
sr.Ma7Series = ma7Pxl
curCo.CandleList.RecursiveBubbleS(len(curCo.CandleList.List), "asc")
candlePxl, err := curCo.CandleList.MakePixelList(cr, baseMaX, sr.Scale)
if err != nil {
return err
}
sr.CandleSeries = candlePxl
// bj, _ := json.Marshal(sr.Ma30Series)
// fmt.Println("sr.Ma30Series:", sr.Period, sr.InstID, string(bj))
sr.LastUpdateTime = sr.Ma30Series.LastUpdateTime
// fmt.Println("candlePxl: ", candlePxl)
return nil
}
func (sr *Series) SetToKey(cr *Core) (string, error) {
if sr == nil || sr.CandleSeries == nil {
return "", errors.New("sr.CandlesSeries == nil")
}
sr.CandleSeries.RecursiveBubbleS(sr.CandleSeries.Count, "asc")
sr.CandleSeries.ReIndex()
sr.CandleSeries.RecursiveBubbleS(sr.CandleSeries.Count, "asc")
// sr.CandleSeries.RecursiveBubbleX(sr.CandleSeries.Count, "asc")
sr.Ma7Series.RecursiveBubbleS(sr.CandleSeries.Count, "asc")
// sr.Ma7Series.RecursiveBubbleX(sr.CandleSeries.Count, "asc")
sr.Ma30Series.RecursiveBubbleS(sr.CandleSeries.Count, "asc")
// sr.Ma30Series.RecursiveBubbleX(sr.CandleSeries.Count, "asc")
now := time.Now().UnixMilli()
sr.LastUpdateTime = now
sr.CandleSeries.LastUpdateTime = now
sr.CandleSeries.UpdateNickName = utils.GetRandomString(12)
sr.UpdateNickName = utils.GetRandomString(12)
js, _ := json.Marshal(*sr)
seriesName := sr.InstID + "|" + sr.Period + "|series"
res, err := cr.RedisLocalCli.Set(seriesName, string(js), 0).Result()
if err != nil {
logrus.Panic(utils.GetFuncName(), err, " seriesSetToKey1: instId:", sr.InstID, " period: ", sr.Period, " lastUpdate:", sr.LastUpdateTime, " md5:", utils.Md5V(string(js)))
}
res, err = cr.RedisLocal2Cli.Set(seriesName, string(js), 0).Result()
return res, err
}
func PrintSerieY(cr *Core, list []redis.Z, period string, count int) {
// fmt.Println("PrintSerieY start")
env := os.Getenv("GO_ENV")
isProduction := env == "production"
//TODO 只有非产线环境,才会显示此列表
if !isProduction {
fmt.Println("seriesYTop count:", count, "period:", period, "sort start")
}
seiScrList := []*SeriesInfoScore{}
for _, v := range list {
sei := SeriesInfo{}
seiScr := SeriesInfoScore{}
json.Unmarshal([]byte(v.Member.(string)), &sei)
seiScr.InstID = sei.InstID
seiScr.Score = v.Score
seiScrList = append(seiScrList, &seiScr)
// if k < count {
// if !isProduction {
// fmt.Println("seriesYTop", count, "No.", k+1, "period"+period, "InstID:", sei.InstID, "score:", v.Score)
// }
// 拉扯极限报告
// }
// if k == count+1 {
// if !isProduction {
// fmt.Println("seriesYTop end -------" + "period" + period + "-------------------------------------")
// fmt.Println("seriesYLast start -------" + "period" + period + "-------------------------------------")
// }
// }
// if k > len(list)-count-1 {
// if !isProduction {
// fmt.Println("seriesYLast", count, "No.", k+1, "period"+period, "InstID:", sei.InstID, "score:", v.Score)
// }
// }
}
bj, _ := json.Marshal(seiScrList)
reqBody := bytes.NewBuffer(bj)
cr.Env = os.Getenv("GO_ENV")
cr.FluentBitUrl = os.Getenv("SARDINE_FluentBitUrl")
fullUrl := "http://" + cr.FluentBitUrl + "/seriesY." + period
res, err := http.Post(fullUrl, "application/json", reqBody)
fmt.Println("requested, response:", fullUrl, reqBody, res)
if err != nil {
logrus.Error(err)
}
if !isProduction {
fmt.Println("seriesYLast count:", count, "period:", period, "sort end")
}
}
func (sei *SeriesInfo) Process(cr *Core) {
curSe, err := cr.GetPixelSeries(sei.InstID, sei.Period)
if err != nil {
logrus.Warn("GetPixelSeries: ", err)
return
}
// TODO 金拱门
// list := cr.GetMyCcyBalanceName()
go func(se Series) {
threeSeg := [][]int{[]int{0, 19}, []int{19, 22}, []int{22, 23}}
ma7Seg := se.MakeSegment(cr, 0, 23, threeSeg, "ma7")
go func() {
cr.SegmentItemChan <- ma7Seg
}()
ma30Seg := se.MakeSegment(cr, 0, 23, threeSeg, "ma30")
go func() {
cr.SegmentItemChan <- ma30Seg
}()
}(curSe)
cli := cr.RedisLocalCli
go func(se Series) {
// 拉扯极限报告
willReport := os.Getenv("SARDINE_SERIESTOREPORT") == "true"
logrus.Info("willReport:", willReport)
// fmt.Println("willReport:", willReport)
if !willReport {
return
}
err = curSe.AddToYSorted(cr)
if err != nil {
logrus.Warn("sei addToYSorted err: ", err)
return
}
// 所有维度拉扯极限
go func(se Series) {
if se.InstID != "BTC-USDT" {
return
}
list, err := cli.ZRevRangeWithScores("series|YValue|sortedSet|period"+se.Period, 0, -1).Result()
if err != nil {
fmt.Println("series sorted err", err)
}
PrintSerieY(cr, list, se.Period, 20)
}(se)
}(curSe)
// TODO 刘海儿检测, 监测金拱门中的刘海儿,预警下跌趋势, 其实有没有金拱门并不重要,刘海儿比金拱门更有说服力
go func(se Series) {
// 如何定义刘海目前定义如下3m以上的周期时当7个或小于7个周期内的时间内发生了一次下坠和一次上升下坠幅度达到2%以上并随后的上升中收复了下坠的幅度那么疑似刘海儿发生。用的周期越少越强烈探底和抬升的幅度越大越强烈所处的维度越高越强烈比如15m的没有1H的强烈
// 如果发生在BTC身上那么将影响所有
// se.CheckLiuhai() {
//
// }
}(curSe)
go func(se Series) {
allow := os.Getenv("SARDINE_SERIESINFOTOCHNL") == "true"
if !allow {
return
}
time.Sleep(0 * time.Second)
sei := SeriesInfo{
InstID: curSe.InstID,
Period: curSe.Period,
}
cr.AddToGeneralSeriesInfoChnl(&sei)
}(curSe)
}
//-------------------------------------------------------------------------------
// 拉扯极限相关: 加入seriesY值排行榜, 用于生成拉扯极限
func (srs *Series) AddToYSorted(cr *Core) error {
setName := "series|YValue|sortedSet|period" + srs.Period
srs.CandleSeries.RecursiveBubbleS(srs.CandleSeries.Count, "asc")
length := len(srs.CandleSeries.List)
if length != srs.Count {
err := errors.New("AddToYSorted err: 数据量不够")
return err
}
lastCandlePixel1 := srs.CandleSeries.List[srs.Count-1]
sei := SeriesInfo{
InstID: srs.InstID,
Period: srs.Period,
}
bj, _ := json.Marshal(sei)
// TODO -200 是个无效的值,如果遇到-200就赋予0值这个办法不好后面考虑不用sortedSet而用自定义对象更好些。
if lastCandlePixel1.Y == -200 {
lastCandlePixel1.Y = 0
}
z := redis.Z{
Score: float64(lastCandlePixel1.Y),
Member: string(bj),
}
// TODO ZAdd 有可能由于bug或者key不一样的原因让列表变长需要想办法怎么定期请空
if lastCandlePixel1.Score != 0 {
cr.RedisLocalCli.ZAdd(setName, z).Result()
}
return nil
}
// 垂直极限排名有一定片面性。暂时先不开放。垂直极限推荐最高的,可能是个不太容易📈上来的股票,甚至垃圾股,而且过一会儿可能跌的更多,所以就算使用这个功能,也仅供参考,
func (vir *VerticalReportItem) AddToVeriticalLimitSorted(cr *Core, srs *Series, period2 string) error {
// redis key: verticalReportItem|BTC-USDT|4H-15m|ts:1643002300000
// sortedSet: verticalLimit|2D-4H|rank|sortedSet
setName := "verticalLimit|" + srs.Period + "-" + period2 + "|rank|sortedSet"
tms := strconv.FormatInt(srs.LastUpdateTime, 10)
keyName := "verticalLimit|" + srs.InstID + "|" + srs.Period + "-" + period2 + "|ts:" + tms
z := redis.Z{
Score: float64(srs.LastUpdateTime),
Member: keyName,
}
if vir.Rank != -1 && vir.Rank != 0 {
extt := 48 * time.Hour
ot := time.Now().Add(extt * -1)
oti := ot.UnixMilli()
count, _ := cr.RedisLocalCli.ZRemRangeByScore(setName, "0", strconv.FormatInt(oti, 10)).Result()
if count > 0 {
logrus.Warning("移出过期的引用数量:", setName, count, "ZRemRangeByScore ", setName, 0, strconv.FormatInt(oti, 10))
}
cr.RedisLocalCli.ZAdd(setName, z).Result()
bj, _ := json.Marshal(vir)
cr.RedisLocalCli.Set(keyName, bj, 48*time.Hour).Result()
}
return nil
}
func (vri *VerticalReportItem) Report(cr *Core) error {
dd := DingdingMsg{
Topic: "垂直极限触发",
RobotName: "pengpeng",
AtAll: true,
Ctype: "markdown",
Content: "",
}
ary1 := []string{}
str := "``币名: ``" + vri.InstID + "\n"
str1 := fmt.Sprintln("``基础维度:``", vri.Period)
str2 := fmt.Sprintln("``剪切维度:``", vri.SecondPeriod)
str21 := fmt.Sprintln("``观察周期:``", vri.Interval)
str3 := fmt.Sprintln("``平均仰角:``", vri.VerticalElevation)
str4 := fmt.Sprintln("``剪切力:``", vri.ShearForce)
str5 := fmt.Sprintln("``Rank``", vri.Rank)
score := vri.TrigerValue
str6 := fmt.Sprintln("``触发买入价位:``", score)
str7 := fmt.Sprintln("``建议止盈价位:``", vri.AdvUpSellPrice)
str8 := fmt.Sprintln("``建议止损价位:``", vri.AdvDownSellPrice)
str9 := "----------------------\n"
ary1 = append(ary1, str, str1, str2, str21, str3, str4, str5, str6, str7, str8, str9)
dd.AddItemListGrp("垂直极限", 2, ary1)
ary2 := []string{}
tm := time.Now().Format("01-02:15:04")
rtime := fmt.Sprintln("``报告时间:``", tm)
ctype := fmt.Sprintln("``类型:``", "极限触发,已弃用")
from := "来自: " + os.Getenv("HOSTNAME")
ary2 = append(ary2, rtime, ctype, from)
dd.AddItemListGrp("", 2, ary2)
dd.PostToRobot("pengpeng", cr)
return nil
}
func (vri *VerticalReportItem) Show(cr *Core) error {
ary1 := []string{}
str := "``币名: ``" + vri.InstID + "\n"
str1 := fmt.Sprintln("``基础维度:``", vri.Period)
str2 := fmt.Sprintln("``剪切维度:``", vri.SecondPeriod)
str21 := fmt.Sprintln("``观察周期:``", vri.Interval)
str3 := fmt.Sprintln("``平均仰角:``", vri.VerticalElevation)
str4 := fmt.Sprintln("``剪切力:``", vri.ShearForce)
str5 := fmt.Sprintln("``Rank``", vri.Rank)
score := vri.TrigerValue
str6 := fmt.Sprintln("``触发买入价位:``", score)
str7 := fmt.Sprintln("``建议止盈价位:``", vri.AdvUpSellPrice)
str8 := fmt.Sprintln("``建议止损价位:``", vri.AdvDownSellPrice)
str9 := "----------------------\n"
ary1 = append(ary1, str, str1, str2, str21, str3, str4, str5, str6, str7, str8, str9)
for _, v := range ary1 {
fmt.Println("verticalReportItem: ", v)
}
return nil
}
// TODO 求某个PixelList里两个点之间的仰角从ridx开始往lidx的元素画一条直线的仰角
func (srs *Series) GetElevation(cr *Core, ctype string, lIdx int, rIdx int) (float64, error) {
yj := float64(0)
switch ctype {
case "candle":
{
yj = (srs.CandleSeries.List[rIdx].Y - srs.CandleSeries.List[lIdx].Y) / float64(rIdx-lIdx)
}
case "ma7":
{
yj = (srs.Ma7Series.List[rIdx].Y - srs.Ma7Series.List[lIdx].Y) / float64(rIdx-lIdx)
}
case "ma30":
{
yj = (srs.Ma30Series.List[rIdx].Y - srs.Ma30Series.List[lIdx].Y) / float64(rIdx-lIdx)
}
}
return yj, nil
}
// TODO 求极值,在某个线段上。一个最大值,一个最小值
func (srs *Series) GetExtremum(cr *Core, lIdx int, rIdx int, ctype string) (*Extremum, error) {
ext := Extremum{
Max: &Pixel{},
Min: &Pixel{},
}
switch ctype {
case "candle":
{
done := false
for k, v := range srs.CandleSeries.List {
if k < lIdx {
continue
}
if v.X == 0 && v.Y == 0 {
continue
}
if k > rIdx {
continue
}
if !done {
ext.Max = srs.CandleSeries.List[k]
ext.Min = srs.CandleSeries.List[k]
done = true
}
if v.Y > ext.Max.Y {
ext.Max = v
}
if v.Y < ext.Min.Y {
ext.Min = v
}
}
// ext = nil
}
case "ma7":
{
done := false
for k, v := range srs.Ma7Series.List {
if k < lIdx {
continue
}
if v.X == 0 && v.Y == 0 {
continue
}
if k > rIdx {
continue
}
if !done {
ext.Max = srs.Ma7Series.List[k]
ext.Min = srs.Ma7Series.List[k]
done = true
}
if v.Y > ext.Max.Y {
ext.Max = v
}
if v.Y < ext.Min.Y {
ext.Min = v
}
}
// ext = nil
}
case "ma30":
{
done := false
for k, v := range srs.Ma30Series.List {
if k < lIdx {
continue
}
if v.X == 0 && v.Y == 0 {
continue
}
if k > rIdx {
continue
}
if !done {
ext.Max = srs.Ma30Series.List[k]
ext.Min = srs.Ma30Series.List[k]
done = true
}
if v.Y > ext.Max.Y {
ext.Max = v
}
if v.Y < ext.Min.Y {
ext.Min = v
}
}
// ext = nil
}
}
return &ext, nil
}
// TODO 获取垂直极限列表
// 筛选条件:
//
// 1. 极大值未发生在最后周期的,排除
// 2. n周期内有仰角小于0的排除
// 注意: 仰角极值未必发生在最后一个周期
//
// 对剩下的币种结果,计算:
//
// 1. n周期平均仰角: s
// 2. 最后周期仰角: p
//
// 筛选出最后仰角高于n周期平均仰角的币列表
// 以最后仰角为结果,得到一个值 p
// 对此列表集合得到每个的15分钟维度拉扯极限每个计算后得到一个结果 f,
//
// f值权重更高p值权重降一个量级求出分值用于排名,
//
// rank = 2 * (lcjx * -1) * (1 + avgElevation) * (1 + lastElevation) * (1 + lastElevation)
//
// 存储在sortedSet里命名
// verticalLimit|15m~4H|rank|sortedSet
// return rank, err
func (vir *VerticalReportItem) MakeVerticalLimit(cr *Core, srs *Series, startIdx int, endIdx int, period2 string) (err error) {
count := len(srs.CandleSeries.List) - 1
lastMa30Pixel := srs.Ma30Series.List[count]
// func (srs *Series) GetExtremum(cr *Core, lIdx int, rIdx int, ctype string) (*Extremum, error) {
ext, err := srs.GetExtremum(cr, startIdx, endIdx, "ma30")
if err != nil {
logrus.Warn(utils.GetFuncName(), ":", err)
}
if ext.Max.Score < 1.05*lastMa30Pixel.Score {
lbj, _ := json.Marshal(lastMa30Pixel)
lext, _ := json.Marshal(ext)
err = errors.New(fmt.Sprintln("当前pixel不是极值", " lastMa30Pixel: ", string(lbj), " ext: ", string(lext)))
return err
} else {
err = errors.New(fmt.Sprintln("当前pixel满足极值", lastMa30Pixel))
}
yj, err := srs.GetElevation(cr, "ma30", startIdx, endIdx)
if err != nil {
logrus.Warn(utils.GetFuncName(), ":", err)
}
vir.VerticalElevation = yj
lcjx, _ := LacheJixian(cr, srs, period2)
vir.ShearForce = lcjx
vir.TrigerValue = srs.CandleSeries.List[len(srs.CandleSeries.List)-1].Score
vir.AdvUpSellPrice = vir.TrigerValue * 1.04
vir.AdvDownSellPrice = vir.TrigerValue * 0.98
// 计算rank的公式如下
// rank := 2 * (lcjx * -1) * (1 + avgElevation) * (1 + lastElevation) * (1 + lastElevation)
// vir.Rank = rank
return nil
}
// 计算剪切力
func LacheJixian(cr *Core, srs *Series, period string) (float64, error) {
curSe, _ := cr.GetPixelSeries(srs.InstID, period)
return curSe.CandleSeries.List[len(srs.CandleSeries.List)-1].Y, nil
}
// type SegmentItem struct {
// InstID string
// Period string
// ReportTime int64
// lastUpdate int64
// Interval int
// Direct string // up, down
// VerticalElevation float64
// }