package types import ( "database/sql" "fmt" "strconv" "strings" "sync" "time" "github.com/slack-go/slack" "git.qtrade.icu/lychiyu/bbgo/pkg/fixedpoint" "git.qtrade.icu/lychiyu/bbgo/pkg/util/templateutil" ) func init() { // make sure we can cast Trade to PlainText _ = PlainText(Trade{}) _ = PlainText(&Trade{}) } type TradeSlice struct { mu sync.Mutex Trades []Trade } func (s *TradeSlice) Copy() []Trade { s.mu.Lock() slice := make([]Trade, len(s.Trades)) copy(slice, s.Trades) s.mu.Unlock() return slice } func (s *TradeSlice) Reverse() { slice := s.Trades for i, j := 0, len(slice)-1; i < j; i, j = i+1, j-1 { slice[i], slice[j] = slice[j], slice[i] } } func (s *TradeSlice) Append(t Trade) { s.mu.Lock() s.Trades = append(s.Trades, t) s.mu.Unlock() } func (s *TradeSlice) Truncate(size int) { s.mu.Lock() if len(s.Trades) > size { s.Trades = s.Trades[len(s.Trades)-1-size:] } s.mu.Unlock() } type Trade struct { // GID is the global ID GID int64 `json:"gid" db:"gid"` // ID is the source trade ID ID uint64 `json:"id" db:"id"` OrderID uint64 `json:"orderID" db:"order_id"` Exchange ExchangeName `json:"exchange" db:"exchange"` Price fixedpoint.Value `json:"price" db:"price"` Quantity fixedpoint.Value `json:"quantity" db:"quantity"` QuoteQuantity fixedpoint.Value `json:"quoteQuantity" db:"quote_quantity"` Symbol string `json:"symbol" db:"symbol"` Side SideType `json:"side" db:"side"` IsBuyer bool `json:"isBuyer" db:"is_buyer"` IsMaker bool `json:"isMaker" db:"is_maker"` Time Time `json:"tradedAt" db:"traded_at"` Fee fixedpoint.Value `json:"fee" db:"fee"` FeeCurrency string `json:"feeCurrency" db:"fee_currency"` FeeProcessing bool `json:"feeProcessing" db:"-"` // FeeDiscounted is an optional field which indicates whether the trade is using the platform fee token for discount. // When FeeDiscounted = true, means the fee is deducted outside the trade // By default, it's set to false. // This is only used by the MAX exchange FeeDiscounted bool `json:"feeDiscounted" db:"-"` IsMargin bool `json:"isMargin" db:"is_margin"` IsFutures bool `json:"isFutures" db:"is_futures"` IsIsolated bool `json:"isIsolated" db:"is_isolated"` // The following fields are null-able fields // StrategyID is the strategy that execute this trade StrategyID sql.NullString `json:"strategyID" db:"strategy"` // PnL is the profit and loss value of the executed trade PnL sql.NullFloat64 `json:"pnl" db:"pnl"` InsertedAt *Time `json:"insertedAt" db:"inserted_at"` } func (trade Trade) CsvHeader() []string { return []string{"id", "order_id", "exchange", "symbol", "price", "quantity", "quote_quantity", "side", "is_buyer", "is_maker", "fee", "fee_currency", "time"} } func (trade Trade) CsvRecords() [][]string { return [][]string{ { strconv.FormatUint(trade.ID, 10), strconv.FormatUint(trade.OrderID, 10), trade.Exchange.String(), trade.Symbol, trade.Price.String(), trade.Quantity.String(), trade.QuoteQuantity.String(), trade.Side.String(), strconv.FormatBool(trade.IsBuyer), strconv.FormatBool(trade.IsMaker), trade.Fee.String(), trade.FeeCurrency, trade.Time.Time().Format(time.RFC1123), }, } } // PositionChange returns the position delta of this trade // BUY trade -> positive quantity // SELL trade -> negative quantity func (trade Trade) PositionChange() fixedpoint.Value { q := trade.Quantity switch trade.Side { case SideTypeSell: return q.Neg() case SideTypeBuy: return q case SideTypeSelf: return fixedpoint.Zero } return fixedpoint.Zero } /*func trimTrailingZero(a string) string { index := strings.Index(a, ".") if index == -1 { return a } var c byte var i int for i = len(a) - 1; i >= 0; i-- { c = a[i] if c == '0' { continue } else if c == '.' { return a[0:i] } else { return a[0 : i+1] } } return a } func trimTrailingZero(a float64) string { return trimTrailingZero(fmt.Sprintf("%f", a)) }*/ // String is for console output func (trade Trade) String() string { return fmt.Sprintf("TRADE %s %s %4s %-4s @ %-6s | AMOUNT %s | FEE %s %s | OrderID %d | TID %d | %s", trade.Exchange.String(), trade.Symbol, trade.Side, trade.Quantity.String(), trade.Price.String(), trade.QuoteQuantity.String(), trade.Fee.String(), trade.FeeCurrency, trade.OrderID, trade.ID, trade.Time.Time().Format(time.StampMilli), ) } // PlainText is used for telegram-styled messages func (trade Trade) PlainText() string { return fmt.Sprintf("Trade %s %s %s %s @ %s, amount %s, fee %s %s", trade.Exchange.String(), trade.Symbol, trade.Side, trade.Quantity.String(), trade.Price.String(), trade.QuoteQuantity.String(), trade.Fee.String(), trade.FeeCurrency) } var slackTradeTextTemplate = ":handshake: Trade {{ .Symbol }} {{ .Side }} {{ .Quantity }} @ {{ .Price }}" func (trade Trade) SlackAttachment() slack.Attachment { var color = "#DC143C" if trade.IsBuyer { color = "#228B22" } liquidity := trade.Liquidity() text := templateutil.Render(slackTradeTextTemplate, trade) footerIcon := ExchangeFooterIcon(trade.Exchange) return slack.Attachment{ Text: text, // Title: ... // Pretext: pretext, Color: color, Fields: []slack.AttachmentField{ {Title: "Exchange", Value: trade.Exchange.String(), Short: true}, {Title: "Price", Value: trade.Price.String(), Short: true}, {Title: "Quantity", Value: trade.Quantity.String(), Short: true}, {Title: "QuoteQuantity", Value: trade.QuoteQuantity.String(), Short: true}, {Title: "Fee", Value: trade.Fee.String(), Short: true}, {Title: "FeeCurrency", Value: trade.FeeCurrency, Short: true}, {Title: "Liquidity", Value: liquidity, Short: true}, {Title: "Order ID", Value: strconv.FormatUint(trade.OrderID, 10), Short: true}, }, FooterIcon: footerIcon, Footer: strings.ToLower(trade.Exchange.String()) + templateutil.Render(" creation time {{ . }}", trade.Time.Time().Format(time.StampMilli)), } } func (trade Trade) Liquidity() (o string) { if trade.IsMaker { o = "MAKER" } else { o = "TAKER" } return o } func (trade Trade) Key() TradeKey { return TradeKey{ Exchange: trade.Exchange, ID: trade.ID, Side: trade.Side, } } type TradeKey struct { Exchange ExchangeName ID uint64 Side SideType } func (k TradeKey) String() string { return k.Exchange.String() + strconv.FormatUint(k.ID, 10) + k.Side.String() }