161 lines
5.2 KiB
Go
161 lines
5.2 KiB
Go
package tinvest
|
|
|
|
import (
|
|
"context"
|
|
"time"
|
|
|
|
pb "github.com/russianinvestments/invest-api-go-sdk/proto"
|
|
"github.com/shopspring/decimal"
|
|
"google.golang.org/protobuf/types/known/timestamppb"
|
|
|
|
"overnight-trading-bot/internal/domain"
|
|
"overnight-trading-bot/internal/money"
|
|
)
|
|
|
|
const sandboxEndpoint = "sandbox-invest-public-api.tinkoff.ru:443"
|
|
|
|
type SandboxGateway struct {
|
|
*RealGateway
|
|
sandboxPB pb.SandboxServiceClient
|
|
}
|
|
|
|
func NewSandboxGateway(ctx context.Context, opts Options) (*SandboxGateway, error) {
|
|
opts.Endpoint = sandboxEndpoint
|
|
realGateway, err := NewRealGateway(ctx, opts)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &SandboxGateway{
|
|
RealGateway: realGateway,
|
|
sandboxPB: pb.NewSandboxServiceClient(realGateway.client.Conn),
|
|
}, nil
|
|
}
|
|
|
|
func (g *SandboxGateway) PostLimitOrder(ctx context.Context, accountID, instrumentUID string, side domain.Side, lots int64, price decimal.Decimal, clientOrderID string) (domain.Order, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return domain.Order{}, err
|
|
}
|
|
direction := pb.OrderDirection_ORDER_DIRECTION_BUY
|
|
if side == domain.SideSell {
|
|
direction = pb.OrderDirection_ORDER_DIRECTION_SELL
|
|
}
|
|
quotation, err := money.DecimalToQuotation(price)
|
|
if err != nil {
|
|
return domain.Order{}, err
|
|
}
|
|
resp, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (*pb.PostOrderResponse, error) {
|
|
return retryValue(callCtx, g.retryAttempts, g.retryBackoff, func() (*pb.PostOrderResponse, error) {
|
|
return g.sandboxPB.PostSandboxOrder(callCtx, &pb.PostOrderRequest{
|
|
InstrumentId: instrumentUID,
|
|
Quantity: lots,
|
|
Price: quotation,
|
|
Direction: direction,
|
|
AccountId: accountID,
|
|
OrderType: pb.OrderType_ORDER_TYPE_LIMIT,
|
|
OrderId: clientOrderID,
|
|
TimeInForce: pb.TimeInForceType_TIME_IN_FORCE_DAY,
|
|
PriceType: pb.PriceType_PRICE_TYPE_CURRENCY,
|
|
})
|
|
})
|
|
})
|
|
if err != nil {
|
|
return domain.Order{}, err
|
|
}
|
|
return orderFromPostResponse(resp, accountID, clientOrderID, side, price), nil
|
|
}
|
|
|
|
func (g *SandboxGateway) CancelOrder(ctx context.Context, accountID, orderID string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
_, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (struct{}, error) {
|
|
return struct{}{}, withRetry(callCtx, g.retryAttempts, g.retryBackoff, func() error {
|
|
_, err := g.sandboxPB.CancelSandboxOrder(callCtx, &pb.CancelOrderRequest{
|
|
AccountId: accountID,
|
|
OrderId: orderID,
|
|
})
|
|
return err
|
|
})
|
|
})
|
|
return err
|
|
}
|
|
|
|
func (g *SandboxGateway) GetOrderState(ctx context.Context, accountID, orderID string) (domain.Order, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return domain.Order{}, err
|
|
}
|
|
resp, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (*pb.OrderState, error) {
|
|
return retryValue(callCtx, g.retryAttempts, g.retryBackoff, func() (*pb.OrderState, error) {
|
|
return g.sandboxPB.GetSandboxOrderState(callCtx, &pb.GetOrderStateRequest{
|
|
AccountId: accountID,
|
|
OrderId: orderID,
|
|
PriceType: pb.PriceType_PRICE_TYPE_CURRENCY,
|
|
})
|
|
})
|
|
})
|
|
if err != nil {
|
|
return domain.Order{}, err
|
|
}
|
|
return orderFromState(resp, accountID), nil
|
|
}
|
|
|
|
func (g *SandboxGateway) GetActiveOrders(ctx context.Context, accountID string) ([]domain.Order, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (*pb.GetOrdersResponse, error) {
|
|
return retryValue(callCtx, g.retryAttempts, g.retryBackoff, func() (*pb.GetOrdersResponse, error) {
|
|
return g.sandboxPB.GetSandboxOrders(callCtx, &pb.GetOrdersRequest{AccountId: accountID})
|
|
})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
states := resp.GetOrders()
|
|
out := make([]domain.Order, 0, len(states))
|
|
for _, state := range states {
|
|
out = append(out, orderFromState(state, accountID))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func (g *SandboxGateway) GetPortfolio(ctx context.Context, accountID string) (domain.Portfolio, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return domain.Portfolio{}, err
|
|
}
|
|
resp, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (*pb.PortfolioResponse, error) {
|
|
return retryValue(callCtx, g.retryAttempts, g.retryBackoff, func() (*pb.PortfolioResponse, error) {
|
|
currency := pb.PortfolioRequest_RUB
|
|
return g.sandboxPB.GetSandboxPortfolio(callCtx, &pb.PortfolioRequest{
|
|
AccountId: accountID,
|
|
Currency: ¤cy,
|
|
})
|
|
})
|
|
})
|
|
if err != nil {
|
|
return domain.Portfolio{}, err
|
|
}
|
|
return portfolioFromResponse(resp, func(instrumentUID string) (int64, error) {
|
|
return g.resolveInstrumentLot(ctx, instrumentUID)
|
|
})
|
|
}
|
|
|
|
func (g *SandboxGateway) GetOperations(ctx context.Context, accountID string, from, to time.Time) ([]domain.Operation, error) {
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := requestWithTimeout(ctx, g.requestTimeout, func(callCtx context.Context) (*pb.OperationsResponse, error) {
|
|
return retryValue(callCtx, g.retryAttempts, g.retryBackoff, func() (*pb.OperationsResponse, error) {
|
|
return g.sandboxPB.GetSandboxOperations(callCtx, &pb.OperationsRequest{
|
|
AccountId: accountID,
|
|
From: timestamppb.New(from),
|
|
To: timestamppb.New(to),
|
|
})
|
|
})
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return operationsFromResponse(resp), nil
|
|
}
|