feat: 接入 CoinGlass 清算热力图隔离链路
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
Some checks failed
ci / build + vet + guard + race (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
|
||||
"cryptoHermes/internal/entity"
|
||||
)
|
||||
|
||||
const coinglassSource = "coinglass"
|
||||
|
||||
type CoinglassLiqHeatMapRepo struct {
|
||||
pool pgxPool
|
||||
}
|
||||
|
||||
func NewCoinglassLiqHeatMapRepo(pool *pgxpool.Pool) *CoinglassLiqHeatMapRepo {
|
||||
return &CoinglassLiqHeatMapRepo{pool: pool}
|
||||
}
|
||||
|
||||
const cgLiqHeatMapUpsertSQL = `
|
||||
INSERT INTO coinglass_liq_heatmap (
|
||||
source, symbol, interval, timestamp,
|
||||
price_min, price_max, raw_grid,
|
||||
updated_at
|
||||
) VALUES (
|
||||
$1, $2, $3, $4,
|
||||
NULLIF($5, '')::NUMERIC, NULLIF($6, '')::NUMERIC, $7::JSONB,
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (source, symbol, interval, timestamp) DO UPDATE SET
|
||||
price_min = EXCLUDED.price_min,
|
||||
price_max = EXCLUDED.price_max,
|
||||
raw_grid = EXCLUDED.raw_grid,
|
||||
updated_at = now()
|
||||
`
|
||||
|
||||
func (r *CoinglassLiqHeatMapRepo) UpsertMany(ctx context.Context, items []entity.CGLiqHeatMap) error {
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
tx, err := r.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("begin: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
for _, item := range items {
|
||||
if item.Symbol == "" || item.Interval == "" || item.Timestamp == 0 || !json.Valid(item.RawGrid) {
|
||||
continue
|
||||
}
|
||||
if _, err := tx.Exec(ctx, cgLiqHeatMapUpsertSQL,
|
||||
coinglassSource, item.Symbol, item.Interval, item.Timestamp,
|
||||
item.PriceMin, item.PriceMax, string(item.RawGrid),
|
||||
); err != nil {
|
||||
return fmt.Errorf("upsert coinglass liq heatmap %s/%s@%d: %w", item.Symbol, item.Interval, item.Timestamp, err)
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
const cgLiqHeatMapFindSQL = `
|
||||
SELECT symbol, interval, timestamp,
|
||||
COALESCE(price_min::text, ''), COALESCE(price_max::text, ''), raw_grid::text
|
||||
FROM coinglass_liq_heatmap
|
||||
WHERE source = $1 AND symbol = $2 AND interval = $3
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT $4
|
||||
`
|
||||
|
||||
func (r *CoinglassLiqHeatMapRepo) FindRecent(ctx context.Context, symbol, interval string, limit int) ([]entity.CGLiqHeatMap, error) {
|
||||
if limit <= 0 {
|
||||
limit = 288
|
||||
}
|
||||
rows, err := r.pool.Query(ctx, cgLiqHeatMapFindSQL, coinglassSource, symbol, interval, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
out := make([]entity.CGLiqHeatMap, 0, limit)
|
||||
for rows.Next() {
|
||||
var (
|
||||
item entity.CGLiqHeatMap
|
||||
raw string
|
||||
)
|
||||
if err := rows.Scan(&item.Symbol, &item.Interval, &item.Timestamp, &item.PriceMin, &item.PriceMax, &raw); err != nil {
|
||||
return nil, fmt.Errorf("scan: %w", err)
|
||||
}
|
||||
item.RawGrid = json.RawMessage(raw)
|
||||
out = append(out, item)
|
||||
}
|
||||
for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 {
|
||||
out[i], out[j] = out[j], out[i]
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"cryptoHermes/internal/entity"
|
||||
|
||||
"github.com/pashagolub/pgxmock/v4"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCoinglassLiqHeatMapRepo_UpsertMany_HappyPath(t *testing.T) {
|
||||
mock, err := pgxmock.NewPool()
|
||||
require.NoError(t, err)
|
||||
defer mock.Close()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("INSERT INTO coinglass_liq_heatmap").
|
||||
WithArgs("coinglass", "BTCUSDT", "5", int64(1700000000000), "1000.5", "2000.5", `{"liq":[[1,2,3]]}`).
|
||||
WillReturnResult(pgxmock.NewResult("INSERT", 1))
|
||||
mock.ExpectCommit()
|
||||
mock.ExpectRollback()
|
||||
|
||||
r := &CoinglassLiqHeatMapRepo{pool: mock}
|
||||
err = r.UpsertMany(context.Background(), []entity.CGLiqHeatMap{{
|
||||
Symbol: "BTCUSDT",
|
||||
Interval: "5",
|
||||
Timestamp: 1700000000000,
|
||||
PriceMin: "1000.5",
|
||||
PriceMax: "2000.5",
|
||||
RawGrid: []byte(`{"liq":[[1,2,3]]}`),
|
||||
}})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestCoinglassLiqHeatMapRepo_UpsertMany_SkipsInvalidRows(t *testing.T) {
|
||||
mock, err := pgxmock.NewPool()
|
||||
require.NoError(t, err)
|
||||
defer mock.Close()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectCommit()
|
||||
mock.ExpectRollback()
|
||||
|
||||
r := &CoinglassLiqHeatMapRepo{pool: mock}
|
||||
err = r.UpsertMany(context.Background(), []entity.CGLiqHeatMap{
|
||||
{Symbol: "", Interval: "5", Timestamp: 1, RawGrid: []byte(`{}`)},
|
||||
{Symbol: "BTCUSDT", Interval: "", Timestamp: 1, RawGrid: []byte(`{}`)},
|
||||
{Symbol: "BTCUSDT", Interval: "5", Timestamp: 0, RawGrid: []byte(`{}`)},
|
||||
{Symbol: "BTCUSDT", Interval: "5", Timestamp: 1, RawGrid: []byte(`not-json`)},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestCoinglassLiqHeatMapRepo_UpsertMany_ExecError(t *testing.T) {
|
||||
mock, err := pgxmock.NewPool()
|
||||
require.NoError(t, err)
|
||||
defer mock.Close()
|
||||
|
||||
mock.ExpectBegin()
|
||||
mock.ExpectExec("INSERT INTO coinglass_liq_heatmap").
|
||||
WithArgs(pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg(), pgxmock.AnyArg()).
|
||||
WillReturnError(errors.New("db down"))
|
||||
mock.ExpectRollback()
|
||||
|
||||
r := &CoinglassLiqHeatMapRepo{pool: mock}
|
||||
err = r.UpsertMany(context.Background(), []entity.CGLiqHeatMap{{
|
||||
Symbol: "BTCUSDT", Interval: "5", Timestamp: 1, RawGrid: []byte(`{}`),
|
||||
}})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "upsert coinglass liq heatmap")
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestCoinglassLiqHeatMapRepo_FindRecent_HappyPath(t *testing.T) {
|
||||
mock, err := pgxmock.NewPool()
|
||||
require.NoError(t, err)
|
||||
defer mock.Close()
|
||||
|
||||
rows := mock.NewRows([]string{"symbol", "interval", "timestamp", "price_min", "price_max", "raw_grid"}).
|
||||
AddRow("BTCUSDT", "5", int64(2), "100", "200", `{"b":2}`).
|
||||
AddRow("BTCUSDT", "5", int64(1), "90", "210", `{"a":1}`)
|
||||
|
||||
mock.ExpectQuery("SELECT symbol, interval, timestamp").
|
||||
WithArgs("coinglass", "BTCUSDT", "5", 288).
|
||||
WillReturnRows(rows)
|
||||
|
||||
r := &CoinglassLiqHeatMapRepo{pool: mock}
|
||||
got, err := r.FindRecent(context.Background(), "BTCUSDT", "5", 0)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got, 2)
|
||||
require.Equal(t, int64(1), got[0].Timestamp)
|
||||
require.JSONEq(t, `{"b":2}`, string(got[1].RawGrid))
|
||||
require.NoError(t, mock.ExpectationsWereMet())
|
||||
}
|
||||
Reference in New Issue
Block a user