first version

This commit is contained in:
2026-06-07 21:01:40 +00:00
parent ee7167accf
commit f19bab1100
79 changed files with 10355 additions and 145 deletions
+182
View File
@@ -0,0 +1,182 @@
package tinvest
import (
"context"
"errors"
"sync"
"time"
"github.com/shopspring/decimal"
"overnight-trading-bot/internal/domain"
)
var ErrNotFound = errors.New("not found")
type Gateway interface {
GetInstrument(ctx context.Context, ticker, classCode string) (domain.Instrument, error)
GetCandles(ctx context.Context, instrumentUID string, interval string, from, to time.Time) ([]domain.Candle, error)
GetOrderBook(ctx context.Context, instrumentUID string, depth int32) (domain.OrderBook, error)
GetTradingStatus(ctx context.Context, instrumentUID string) (domain.TradingStatus, error)
PostLimitOrder(ctx context.Context, accountID, instrumentUID string, side domain.Side, lots int64, price decimal.Decimal, clientOrderID string) (domain.Order, error)
CancelOrder(ctx context.Context, accountID, orderID string) error
GetOrderState(ctx context.Context, accountID, orderID string) (domain.Order, error)
GetActiveOrders(ctx context.Context, accountID string) ([]domain.Order, error)
GetPortfolio(ctx context.Context, accountID string) (domain.Portfolio, error)
GetOperations(ctx context.Context, accountID string, from, to time.Time) ([]domain.Operation, error)
GetServerTime(ctx context.Context) (time.Time, error)
}
type FakeGateway struct {
mu sync.Mutex
Instruments map[string]domain.Instrument
Candles map[string][]domain.Candle
OrderBooks map[string]domain.OrderBook
Statuses map[string]domain.TradingStatus
Orders map[string]domain.Order
Portfolio domain.Portfolio
Operations []domain.Operation
ServerTime time.Time
}
func NewFakeGateway() *FakeGateway {
return &FakeGateway{
Instruments: make(map[string]domain.Instrument),
Candles: make(map[string][]domain.Candle),
OrderBooks: make(map[string]domain.OrderBook),
Statuses: make(map[string]domain.TradingStatus),
Orders: make(map[string]domain.Order),
Portfolio: domain.Portfolio{
Equity: decimal.NewFromInt(100_000),
Cash: decimal.NewFromInt(100_000),
CheckedAt: time.Now().UTC(),
},
}
}
func (f *FakeGateway) GetInstrument(_ context.Context, ticker, classCode string) (domain.Instrument, error) {
f.mu.Lock()
defer f.mu.Unlock()
for _, instrument := range f.Instruments {
if instrument.Ticker == ticker && instrument.ClassCode == classCode {
return instrument, nil
}
}
return domain.Instrument{}, ErrNotFound
}
func (f *FakeGateway) GetCandles(_ context.Context, instrumentUID string, _ string, from, to time.Time) ([]domain.Candle, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []domain.Candle
for _, candle := range f.Candles[instrumentUID] {
if !candle.TradeDate.Before(from) && !candle.TradeDate.After(to) {
out = append(out, candle)
}
}
return out, nil
}
func (f *FakeGateway) GetOrderBook(_ context.Context, instrumentUID string, _ int32) (domain.OrderBook, error) {
f.mu.Lock()
defer f.mu.Unlock()
book, ok := f.OrderBooks[instrumentUID]
if !ok {
return domain.OrderBook{}, ErrNotFound
}
return book, nil
}
func (f *FakeGateway) GetTradingStatus(_ context.Context, instrumentUID string) (domain.TradingStatus, error) {
f.mu.Lock()
defer f.mu.Unlock()
status, ok := f.Statuses[instrumentUID]
if !ok {
return domain.TradingStatusNormal, nil
}
return status, nil
}
func (f *FakeGateway) PostLimitOrder(_ context.Context, accountID, instrumentUID string, side domain.Side, lots int64, price decimal.Decimal, clientOrderID string) (domain.Order, error) {
f.mu.Lock()
defer f.mu.Unlock()
order := domain.Order{
ClientOrderID: clientOrderID,
BrokerOrderID: "fake-" + clientOrderID,
AccountIDHash: accountID,
InstrumentUID: instrumentUID,
Side: side,
OrderType: domain.OrderTypeLimit,
LimitPrice: price,
QuantityLots: lots,
Status: domain.OrderStatusSent,
RawStateJSON: "{}",
CreatedAt: time.Now().UTC(),
UpdatedAt: time.Now().UTC(),
}
f.Orders[order.BrokerOrderID] = order
return order, nil
}
func (f *FakeGateway) CancelOrder(_ context.Context, _ string, orderID string) error {
f.mu.Lock()
defer f.mu.Unlock()
order, ok := f.Orders[orderID]
if !ok {
return ErrNotFound
}
order.Status = domain.OrderStatusCancelled
order.UpdatedAt = time.Now().UTC()
f.Orders[orderID] = order
return nil
}
func (f *FakeGateway) GetOrderState(_ context.Context, _ string, orderID string) (domain.Order, error) {
f.mu.Lock()
defer f.mu.Unlock()
order, ok := f.Orders[orderID]
if !ok {
return domain.Order{}, ErrNotFound
}
return order, nil
}
func (f *FakeGateway) GetActiveOrders(_ context.Context, _ string) ([]domain.Order, error) {
f.mu.Lock()
defer f.mu.Unlock()
out := make([]domain.Order, 0)
for _, order := range f.Orders {
if order.Status == domain.OrderStatusSent || order.Status == domain.OrderStatusPartiallyFilled {
out = append(out, order)
}
}
return out, nil
}
func (f *FakeGateway) GetPortfolio(_ context.Context, _ string) (domain.Portfolio, error) {
f.mu.Lock()
defer f.mu.Unlock()
f.Portfolio.CheckedAt = time.Now().UTC()
return f.Portfolio, nil
}
func (f *FakeGateway) GetOperations(_ context.Context, _ string, from, to time.Time) ([]domain.Operation, error) {
f.mu.Lock()
defer f.mu.Unlock()
var out []domain.Operation
for _, op := range f.Operations {
if !op.ExecutedAt.Before(from) && !op.ExecutedAt.After(to) {
out = append(out, op)
}
}
return out, nil
}
func (f *FakeGateway) GetServerTime(context.Context) (time.Time, error) {
f.mu.Lock()
defer f.mu.Unlock()
if f.ServerTime.IsZero() {
return time.Now().UTC(), nil
}
return f.ServerTime, nil
}