dev 5
This commit is contained in:
111
go-client/apps/trend/logic/boot.go
Normal file
111
go-client/apps/trend/logic/boot.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"big-qmt/go-client/config"
|
||||
"big-qmt/go-client/libs"
|
||||
"big-qmt/go-client/sdk"
|
||||
)
|
||||
|
||||
func logf(level, format string, args ...any) {
|
||||
log.Printf("[%s] %s", level, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Overview(assets *sdk.Assets, positions []sdk.Position) {
|
||||
fmt.Println("\n" + strings.Repeat("=", 80))
|
||||
fmt.Printf("【时间】%s\n", time.Now().Format("2006-01-02 15:04:05"))
|
||||
fmt.Printf("【配置】account_id: %s host_key: %s buy_value: %.0f\n", config.Account.AccountID, config.Account.HostKey, config.Account.BuyValue)
|
||||
if assets != nil {
|
||||
fmt.Printf("【资金】总资产:%.2f元,可用资金:%.2f元\n", assets.Total, assets.Available)
|
||||
} else {
|
||||
fmt.Println("【资金】查询失败")
|
||||
}
|
||||
fmt.Printf("【持仓】%d只\n", len(positions))
|
||||
fmt.Println(strings.Repeat("=", 80))
|
||||
for _, p := range positions {
|
||||
if p.Volume <= 0 {
|
||||
continue
|
||||
}
|
||||
code := p.StockCode
|
||||
fmt.Printf("【持仓】%s %s 持仓=%d 可用=%d 冻结=%d 在途=%d 昨仓=%d 成本=%.3f 现价=%.3f 市值=%.2f 浮盈=%.2f 盈亏比例=%.2f%%\n",
|
||||
code, p.StockName, p.Volume, p.CanUseVolume, p.FrozenVolume, p.OnRoadVolume, p.YesterdayVolume,
|
||||
p.OpenPrice, p.LastPrice, p.MarketValue, p.FloatProfit, p.ProfitRate*100)
|
||||
}
|
||||
}
|
||||
|
||||
func RunOnce(ctx context.Context, client *sdk.Client, signals []*libs.SignalItem) {
|
||||
if !libs.TradingTime(time.Now()) {
|
||||
return
|
||||
}
|
||||
|
||||
// 1 取消过期订单
|
||||
OrderBook.CancelExpired(client)
|
||||
|
||||
// 2 验证可用资金
|
||||
assets, err := client.Assets(ctx)
|
||||
if err != nil {
|
||||
logf("ERROR", "获取资产失败: %v", err)
|
||||
return
|
||||
}
|
||||
if assets.Available < assets.Total*config.Account.MinCashRatio {
|
||||
logf("INFO", "资金总闸:可用金额太少,禁止开新仓")
|
||||
return
|
||||
}
|
||||
|
||||
// 3 获取大盘状态
|
||||
IsAllow := libs.MarketAllowOpen()
|
||||
|
||||
// 4 获取持仓
|
||||
var allCodes []string
|
||||
pos_codes, positions, err := client.Positions(ctx)
|
||||
if err != nil {
|
||||
logf("ERROR", "获取持仓失败: %v", err)
|
||||
return
|
||||
}
|
||||
allCodes = append(allCodes, pos_codes...)
|
||||
|
||||
// 5 验证有效开仓信号
|
||||
allowOpen := make([]*libs.SignalItem, 0)
|
||||
for _, item := range signals {
|
||||
if !slices.Contains(pos_codes, item.Code) {
|
||||
allowOpen = append(allowOpen, item)
|
||||
}
|
||||
}
|
||||
allowOpen = SignalFilter(allowOpen, config.Account.SignalAllow)
|
||||
|
||||
// 6 获取行情tick
|
||||
ticks, err := client.FullTick(ctx, allCodes)
|
||||
if err != nil {
|
||||
logf("ERROR", "获取行情失败: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 7 执行开仓:有开仓信号 && 大盘指数允许开仓
|
||||
if len(allowOpen) > 0 && IsAllow {
|
||||
openSignal(client, ticks, allowOpen)
|
||||
}
|
||||
|
||||
// 8 持仓计算
|
||||
managePositions(client, ticks, positions, IsAllow)
|
||||
}
|
||||
|
||||
func SignalFilter(in []*libs.SignalItem, name []string) []*libs.SignalItem {
|
||||
newSignalItem := make([]*libs.SignalItem, 0)
|
||||
if len(name) == 0 {
|
||||
return newSignalItem
|
||||
}
|
||||
for _, i := range in {
|
||||
for _, n := range name {
|
||||
if i.SignalKey == n {
|
||||
newSignalItem = append(newSignalItem, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
return newSignalItem
|
||||
}
|
||||
41
go-client/apps/trend/logic/open.go
Normal file
41
go-client/apps/trend/logic/open.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"big-qmt/go-client/config"
|
||||
"big-qmt/go-client/libs"
|
||||
"big-qmt/go-client/sdk"
|
||||
)
|
||||
|
||||
func openSignal(client *sdk.Client, ticks map[string]sdk.Tick, openSignals []*libs.SignalItem) {
|
||||
for _, item := range openSignals {
|
||||
// 是否有锁
|
||||
if OrderBook.IsLock("BUY", item.Code) {
|
||||
continue
|
||||
}
|
||||
// 验证价格
|
||||
price := ticks[item.Code].LastPrice
|
||||
if price <= 0 {
|
||||
continue
|
||||
}
|
||||
// 防止接飞刀
|
||||
if !OpenWatch.Triggered("开仓", item.Code, price) {
|
||||
continue
|
||||
}
|
||||
// 计算开仓数量
|
||||
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
|
||||
if volume <= 0 {
|
||||
continue
|
||||
}
|
||||
// 开仓
|
||||
orderID := NewOrderID("base")
|
||||
if !OrderBook.Place(client, sdk.OpBuy, item.Code, volume, orderID) {
|
||||
continue
|
||||
}
|
||||
// 保存状态
|
||||
QuantState.Set(&StateItem{Code: item.Code, BaseOrderId: orderID, BaseQty: volume, BaseCost: price, BaseStatus: StatusIng})
|
||||
if err := QuantState.Save(); err != nil {
|
||||
logf("ERROR", "%v", err)
|
||||
}
|
||||
logf("INFO", "[ZT][开仓] %s 买入 %d 股", item.Code, volume)
|
||||
}
|
||||
}
|
||||
154
go-client/apps/trend/logic/order.go
Normal file
154
go-client/apps/trend/logic/order.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"big-qmt/go-client/sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
STOCK_DIRECTION = 48
|
||||
STOCK_SIDE_BUY = 48
|
||||
STOCK_SIDE_SELL = 49
|
||||
OffsetFlag = map[string]string{"23": "BUY", "24": "SELL", "48": "BUY", "49": "SELL"}
|
||||
OrderTimeout = 5 * time.Minute
|
||||
|
||||
OrderBook *Books
|
||||
)
|
||||
|
||||
type OrderItem struct {
|
||||
ID string
|
||||
Code string
|
||||
Side string
|
||||
Remark string
|
||||
Status string
|
||||
CreatedAt time.Time
|
||||
Volume int
|
||||
}
|
||||
|
||||
type Books struct {
|
||||
mu sync.Mutex
|
||||
Data map[string]*OrderItem
|
||||
Index []string
|
||||
}
|
||||
|
||||
func NewOrderBook() {
|
||||
OrderBook = &Books{Data: make(map[string]*OrderItem), Index: make([]string, 0)}
|
||||
}
|
||||
|
||||
func NewOrderID(leg string) string {
|
||||
var random [6]byte
|
||||
_, _ = rand.Read(random[:])
|
||||
tag := fmt.Sprintf("zt-%s-%s", leg, hex.EncodeToString(random[:]))
|
||||
if len(tag) > 24 {
|
||||
return tag[:24]
|
||||
}
|
||||
return tag
|
||||
}
|
||||
|
||||
func (o *Books) IsLock(side, code string) bool {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
keyStr := fmt.Sprintf("%s-%s", side, code)
|
||||
return slices.Contains(o.Index, keyStr)
|
||||
}
|
||||
|
||||
func (o *Books) Refresh(client *sdk.Client) error {
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
raw, err := client.TradeDetailData(context.Background(), "order")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var idx []string
|
||||
orders := make(map[string]*OrderItem)
|
||||
for _, row := range raw {
|
||||
keyStr, item := parseOrder(row)
|
||||
orders[keyStr] = item
|
||||
idx = append(idx, keyStr)
|
||||
}
|
||||
o.Data = orders
|
||||
o.Index = idx
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Books) CancelExpired(client *sdk.Client) error {
|
||||
ctx := context.Background()
|
||||
err := o.Refresh(client)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[委托] 查询失败: %v", err)
|
||||
}
|
||||
for _, order := range o.Data {
|
||||
if order.CreatedAt.IsZero() || time.Since(order.CreatedAt) <= OrderTimeout {
|
||||
continue
|
||||
}
|
||||
if order.ID != "" {
|
||||
rs, err := client.CanCancelOrder(ctx, order.ID)
|
||||
if err != nil {
|
||||
logf("ERROR", "[委托] 撤销失败:%v", err)
|
||||
continue
|
||||
} else {
|
||||
logf("INFO", "[委托] 撤销成功:%v", rs)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *Books) Place(client *sdk.Client, op int, code string, volume int, sn string) bool {
|
||||
if _, err := client.PassorderLatestTagged(context.Background(), op, code, volume, sn); err != nil {
|
||||
logf("ERROR", "[委托] %s 下单失败: %v", code, err)
|
||||
return false
|
||||
}
|
||||
|
||||
o.mu.Lock()
|
||||
defer o.mu.Unlock()
|
||||
|
||||
keyStr := fmt.Sprintf("%s-%s", OffsetFlag[strconv.Itoa(op)], code)
|
||||
o.Index = append(o.Index, keyStr)
|
||||
|
||||
logf("INFO", "[委托] 下单已提交 %d %s %d股", op, code, volume)
|
||||
return true
|
||||
}
|
||||
|
||||
func parseOrder(row map[string]string) (string, *OrderItem) {
|
||||
left, _ := strconv.Atoi(row["m_nVolumeTotal"])
|
||||
traded, _ := strconv.Atoi(row["m_nVolumeTraded"])
|
||||
volume := left + traded
|
||||
|
||||
item := &OrderItem{
|
||||
ID: row["m_strOrderSysID"],
|
||||
Code: row["m_strInstrumentID"],
|
||||
Side: OffsetFlag[row["m_nOffsetFlag"]],
|
||||
Remark: row["m_strRemark"],
|
||||
Status: row["m_nOrderStatus"],
|
||||
Volume: volume,
|
||||
CreatedAt: time.Unix(parseTimestamp(row), 0),
|
||||
}
|
||||
keyStr := fmt.Sprintf("%s-%s", item.Side, item.Code)
|
||||
return keyStr, item
|
||||
}
|
||||
|
||||
func parseTimestamp(row map[string]string) int64 {
|
||||
ts, _ := strconv.ParseInt(row["m_nOrderTime"], 10, 64)
|
||||
if ts > 1e11 {
|
||||
return ts / 1000
|
||||
}
|
||||
if ts > 0 {
|
||||
return ts
|
||||
}
|
||||
date := row["m_strInsertDate"]
|
||||
clock := strings.ReplaceAll(row["m_strInsertTime"], ":", "")
|
||||
clock = strings.Repeat("0", max(0, 6-len(clock))) + clock
|
||||
t, _ := time.ParseInLocation("20060102150405", date+clock, time.Local)
|
||||
return t.Unix()
|
||||
}
|
||||
155
go-client/apps/trend/logic/positions.go
Normal file
155
go-client/apps/trend/logic/positions.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"math"
|
||||
"sync"
|
||||
|
||||
"big-qmt/go-client/config"
|
||||
"big-qmt/go-client/libs"
|
||||
"big-qmt/go-client/sdk"
|
||||
)
|
||||
|
||||
const (
|
||||
legBase = "base"
|
||||
legAdded = "add"
|
||||
)
|
||||
|
||||
var (
|
||||
peakMu sync.Mutex
|
||||
peakGrids = make(map[string]int)
|
||||
)
|
||||
|
||||
func managePositions(client *sdk.Client, ticks map[string]sdk.Tick, positions []sdk.Position, marketOK bool) {
|
||||
for _, pos := range positions {
|
||||
item, err := QuantState.Get(code)
|
||||
if err != nil || item.BaseStatus == StatusIng || item.AddedStatus == StatusIng || position.Volume != item.BaseQty+item.AddedQty {
|
||||
continue
|
||||
}
|
||||
price := ticks[code].LastPrice
|
||||
if price <= 0 {
|
||||
continue
|
||||
}
|
||||
if item.AddedQty > 0 {
|
||||
pnl := (price - item.AddedCost) / item.AddedCost * 100
|
||||
if shouldSell(code, legAdded, pnl) {
|
||||
sell(client, item, position.CanUseVolume, item.AddedQty, legAdded, pnl)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pnl := (price - item.BaseCost) / item.BaseCost * 100
|
||||
if shouldSell(code, legBase, pnl) {
|
||||
sell(client, item, position.CanUseVolume, item.BaseQty, legBase, pnl)
|
||||
} else if pnl <= config.Account.LossTriggerPct {
|
||||
buyAdded(client, item, price, marketOK, budget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func syncAdded(item *StateItem, position sdk.Position) {
|
||||
addedQty := position.Volume - item.BaseQty
|
||||
if addedQty <= 0 {
|
||||
item.BaseQty = position.Volume
|
||||
item.BaseCost = position.OpenPrice
|
||||
item.AddedQty = 0
|
||||
item.AddedCost = 0
|
||||
item.AddedStatus = StatusNone
|
||||
peakMu.Lock()
|
||||
delete(peakGrids, item.Code+"|"+legAdded)
|
||||
peakMu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
item.AddedQty = addedQty
|
||||
totalCost := position.OpenPrice * float64(position.Volume)
|
||||
baseCost := item.BaseCost * float64(item.BaseQty)
|
||||
item.AddedCost = math.Max(0, (totalCost-baseCost)/float64(addedQty))
|
||||
item.AddedStatus = StatusOk
|
||||
}
|
||||
|
||||
func buyAdded(client *sdk.Client, item *StateItem, price float64, marketOK bool, budget *float64) {
|
||||
if !marketOK || !PosbuyWatch.Triggered("补仓", item.Code, price) {
|
||||
return
|
||||
}
|
||||
volume := libs.CalcBuyVolume(price, config.Account.BuyValue)
|
||||
amount := price * float64(volume)
|
||||
if amount > *budget || orderBusy(item.Code, "BUY") {
|
||||
return
|
||||
}
|
||||
|
||||
orderID := NewOrderID(legAdded)
|
||||
if !OrderBook.Place(client, sdk.OpBuy, item.Code, volume, orderID) {
|
||||
return
|
||||
}
|
||||
item.AddedOrderId = orderID
|
||||
item.AddedNum++
|
||||
item.AddedQty = volume
|
||||
item.AddedCost = price
|
||||
item.AddedStatus = StatusIng
|
||||
QuantState.Set(item)
|
||||
*budget -= amount
|
||||
}
|
||||
|
||||
func sell(client *sdk.Client, item *StateItem, usable, volume int, leg string, pnl float64) {
|
||||
volume -= volume % 100
|
||||
if volume <= 0 || usable < volume || orderBusy(item.Code, "SELL") {
|
||||
return
|
||||
}
|
||||
orderID := NewOrderID(leg)
|
||||
if !OrderBook.Place(client, sdk.OpSell, item.Code, volume, orderID) {
|
||||
return
|
||||
}
|
||||
if leg == legAdded {
|
||||
item.AddedOrderId = orderID
|
||||
item.AddedStatus = StatusIng
|
||||
} else {
|
||||
item.BaseOrderId = orderID
|
||||
item.BaseStatus = StatusIng
|
||||
}
|
||||
QuantState.Set(item)
|
||||
logf("INFO", "[止盈] %s 卖出%d股,盈利=%.2f%%", item.Code, volume, pnl)
|
||||
}
|
||||
|
||||
func orderBusy(code, side string) bool {
|
||||
OrderBook.mu.Lock()
|
||||
defer OrderBook.mu.Unlock()
|
||||
order := OrderBook.Data[side+"-"+code]
|
||||
if order == nil {
|
||||
return false
|
||||
}
|
||||
switch order.Status {
|
||||
case "48", "49", "50", "51", "52", "55":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func shouldSell(code, leg string, pnl float64) bool {
|
||||
if pnl < config.Account.MinProfitPct {
|
||||
return false
|
||||
}
|
||||
grid := int(math.Floor(pnl / config.Account.GridStepPct))
|
||||
key := code + "|" + leg
|
||||
peakMu.Lock()
|
||||
defer peakMu.Unlock()
|
||||
peak, tracked := peakGrids[key]
|
||||
if !tracked || grid > peak {
|
||||
peakGrids[key] = grid
|
||||
return false
|
||||
}
|
||||
return grid < peak
|
||||
}
|
||||
|
||||
func forget(code string) {
|
||||
OpenWatch.mu.Lock()
|
||||
delete(OpenWatch.Data, code)
|
||||
OpenWatch.mu.Unlock()
|
||||
PosbuyWatch.mu.Lock()
|
||||
delete(PosbuyWatch.Data, code)
|
||||
PosbuyWatch.mu.Unlock()
|
||||
peakMu.Lock()
|
||||
delete(peakGrids, code+"|"+legBase)
|
||||
delete(peakGrids, code+"|"+legAdded)
|
||||
peakMu.Unlock()
|
||||
}
|
||||
152
go-client/apps/trend/logic/state.go
Normal file
152
go-client/apps/trend/logic/state.go
Normal file
@@ -0,0 +1,152 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
"big-qmt/go-client/config"
|
||||
"big-qmt/go-client/sdk"
|
||||
)
|
||||
|
||||
var (
|
||||
StatusNone = ""
|
||||
StatusIng = "ING" // 处理中
|
||||
StatusOk = "OK" // 成功
|
||||
QuantState *State
|
||||
)
|
||||
|
||||
type State struct {
|
||||
AbsPath string
|
||||
mu sync.Mutex
|
||||
Items map[string]*StateItem
|
||||
Codes []string
|
||||
}
|
||||
|
||||
type StateItem struct {
|
||||
Code string `json:"code"`
|
||||
BaseOrderId string `json:"base_order_id"`
|
||||
BaseQty int `json:"base_qty"`
|
||||
BaseCost float64 `json:"base_cost"`
|
||||
BaseStatus string `json:"base_status,omitempty"`
|
||||
AddedOrderId string `json:"added_order_id"`
|
||||
AddedNum int `json:"add_num"`
|
||||
AddedQty int `json:"add_qty"`
|
||||
AddedCost float64 `json:"add_cost"`
|
||||
AddedStatus string `json:"added_status,omitempty"`
|
||||
}
|
||||
|
||||
func InitState(sn string) error {
|
||||
absPath := path.Join(config.Global.QMTDataDir, fmt.Sprintf("%s_%s_state.json", sn, config.Account.AccountID))
|
||||
items, err := loadStateFile(absPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var codes []string
|
||||
for code, _ := range items {
|
||||
codes = append(codes, code)
|
||||
}
|
||||
|
||||
QuantState = &State{
|
||||
AbsPath: absPath,
|
||||
Items: items,
|
||||
Codes: codes,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadStateFile(fp string) (map[string]*StateItem, error) {
|
||||
raw, err := os.ReadFile(fp)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("[状态] 读取失败: %v", err)
|
||||
}
|
||||
var items map[string]*StateItem
|
||||
if err := json.Unmarshal(raw, &items); err != nil {
|
||||
return nil, fmt.Errorf("[状态] 解析失败:%s", err)
|
||||
}
|
||||
return items, nil
|
||||
|
||||
}
|
||||
|
||||
func SyncPositions(positions []sdk.Position) error {
|
||||
for _, pos := range positions {
|
||||
code := pos.StockCode
|
||||
if code == "" || pos.Volume <= 0 || pos.OpenPrice <= 0 {
|
||||
continue
|
||||
}
|
||||
if !slices.Contains(QuantState.Codes, code) {
|
||||
item := &StateItem{
|
||||
Code: code,
|
||||
BaseQty: pos.Volume,
|
||||
BaseCost: pos.OpenPrice,
|
||||
BaseStatus: StatusOk,
|
||||
}
|
||||
QuantState.Append(item)
|
||||
logf("WARNING", "[状态] %s 首次接管为底仓 数量=%d 成本=%.2f", code, pos.Volume, pos.OpenPrice)
|
||||
}
|
||||
}
|
||||
|
||||
return QuantState.Save()
|
||||
}
|
||||
|
||||
func (s *State) Append(i *StateItem) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.Items[i.Code] = i
|
||||
s.Codes = append(s.Codes, i.Code)
|
||||
}
|
||||
|
||||
func (s *State) Get(code string) (*StateItem, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if i, ok := s.Items[code]; ok {
|
||||
return i, nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("%s not found.", code)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) Set(i *StateItem) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, ok := s.Items[i.Code]; !ok {
|
||||
s.Codes = append(s.Codes, i.Code)
|
||||
}
|
||||
s.Items[i.Code] = i
|
||||
}
|
||||
|
||||
func (s *State) Delete(code string) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
delete(s.Items, code)
|
||||
if index := slices.Index(s.Codes, code); index >= 0 {
|
||||
s.Codes = slices.Delete(s.Codes, index, index+1)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *State) Save() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// 写入AbsPath文件
|
||||
f, err := os.OpenFile(s.AbsPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("[状态] 打开文件失败: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encoder := json.NewEncoder(f)
|
||||
encoder.SetIndent("", " ")
|
||||
if err := encoder.Encode(s.Items); err != nil {
|
||||
return fmt.Errorf("[状态] 写入失败: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
66
go-client/apps/trend/logic/watch.go
Normal file
66
go-client/apps/trend/logic/watch.go
Normal file
@@ -0,0 +1,66 @@
|
||||
package logic
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
WatchExpireTime = 5 * time.Minute
|
||||
WatchReThreshold = 0.61
|
||||
|
||||
OpenWatch *WatchMu
|
||||
PosbuyWatch *WatchMu
|
||||
)
|
||||
|
||||
type dipWatch struct {
|
||||
LastClose float64
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
type WatchMu struct {
|
||||
mu sync.Mutex
|
||||
Data map[string]dipWatch
|
||||
}
|
||||
|
||||
func InitWatch() {
|
||||
OpenWatch = &WatchMu{
|
||||
Data: make(map[string]dipWatch),
|
||||
}
|
||||
PosbuyWatch = &WatchMu{
|
||||
Data: make(map[string]dipWatch),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WatchMu) Triggered(tag, code string, price float64) bool {
|
||||
if price <= 0 {
|
||||
return false
|
||||
}
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
now := time.Now()
|
||||
watch, ok := w.Data[code]
|
||||
if !ok || now.After(watch.ExpiresAt) || now.Equal(watch.ExpiresAt) {
|
||||
w.Data[code] = dipWatch{LastClose: price, ExpiresAt: now.Add(WatchExpireTime)}
|
||||
logf("INFO", "[%s-观察] %s 现价=%.2f", tag, code, price)
|
||||
return false
|
||||
}
|
||||
if price < watch.LastClose {
|
||||
watch.LastClose = price
|
||||
watch.ExpiresAt = now.Add(WatchExpireTime)
|
||||
w.Data[code] = watch
|
||||
logf("INFO", "[%s-下跌] %s 刷新低点=%.2f", tag, code, price)
|
||||
return false
|
||||
}
|
||||
rebound := (price - watch.LastClose) / watch.LastClose * 100
|
||||
if rebound <= 0 {
|
||||
return false
|
||||
}
|
||||
if rebound < WatchReThreshold {
|
||||
logf("INFO", "[%s-等待] %s 反弹=%.2f%% 阈值=%.2f%%", tag, code, rebound, WatchReThreshold)
|
||||
return false
|
||||
}
|
||||
delete(w.Data, code)
|
||||
logf("INFO", "[%s-触发] %s 反弹=%.2f%% 低点=%.2f", tag, code, rebound, watch.LastClose)
|
||||
return true
|
||||
}
|
||||
Reference in New Issue
Block a user