From 9e38758c9d4c479fae52656415f4e6706e323c6d Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 15 Jun 2023 19:48:07 +0300 Subject: [PATCH 01/57] add order book bot --- examples/ob_bot/cmd/main.go | 150 ++++++++++++++ examples/ob_bot/internal/bot/bot.go | 187 ++++++++++++++++++ examples/ob_bot/internal/bot/executor.go | 142 +++++++++++++ examples/ob_bot/internal/bot/model.go | 18 ++ .../ob_bot/internal/bot/strategy_on_ob.go | 67 +++++++ 5 files changed, 564 insertions(+) create mode 100644 examples/ob_bot/cmd/main.go create mode 100644 examples/ob_bot/internal/bot/bot.go create mode 100644 examples/ob_bot/internal/bot/executor.go create mode 100644 examples/ob_bot/internal/bot/model.go create mode 100644 examples/ob_bot/internal/bot/strategy_on_ob.go diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go new file mode 100644 index 0000000..7c6a07f --- /dev/null +++ b/examples/ob_bot/cmd/main.go @@ -0,0 +1,150 @@ +package main + +import ( + "context" + "errors" + "fmt" + "github.com/tinkoff/invest-api-go-sdk/examples/ob_bot/internal/bot" + "github.com/tinkoff/invest-api-go-sdk/investgo" + pb "github.com/tinkoff/invest-api-go-sdk/proto" + "go.uber.org/zap" + "log" + "os" + "os/signal" + "strings" + "syscall" + "time" +) + +func main() { + // загружаем конфигурацию для сдк из .yaml файла + sdkConfig, err := investgo.LoadConfig("config.yaml") + if err != nil { + log.Fatalf("config loading error %v", err.Error()) + } + + sigs := make(chan os.Signal) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + // сдк использует для внутреннего логирования investgo.Logger + // для примера передадим uber.zap + prod, _ := zap.NewProduction() + defer func() { + err := prod.Sync() + if err != nil { + log.Printf("Prod.Sync %v", err.Error()) + } + }() + if err != nil { + log.Fatalf("logger creating error %v", err) + } + logger := prod.Sugar() + // создаем клиента для investAPI, он позволяет создавать нужные сервисы и уже + // через них вызывать нужные методы + client, err := investgo.NewClient(ctx, sdkConfig, logger) + if err != nil { + logger.Fatalf("client creating error %v", err.Error()) + } + defer func() { + logger.Infof("closing client connection") + err := client.Stop() + if err != nil { + logger.Errorf("client shutdown error %v", err.Error()) + } + }() + + // для создания стратеги нужно ее сконфигурировать, для этого получим список идентификаторов инструментов, + // которыми предстоит торговать + insrtumentsService := client.NewInstrumentsServiceClient() + // получаем список акций доступных для торговли через investAPI + instrumentsResp, err := insrtumentsService.Shares(pb.InstrumentStatus_INSTRUMENT_STATUS_BASE) + if err != nil { + logger.Errorf(err.Error()) + } + // слайс идентификаторов торговых инструментов + instrumentIds := make([]string, 0, 300) + instrumentspb := instrumentsResp.GetInstruments() + for _, instrument := range instrumentspb { + if len(instrumentIds) > 99 { + break + } + if strings.Compare(instrument.GetExchange(), "MOEX") == 0 { + instrumentIds = append(instrumentIds, instrument.GetUid()) + } + } + logger.Infof("got %v instruments\n", len(instrumentIds)) + + instruments := instrumentIds + // instruments := []string{"6afa6f80-03a7-4d83-9cf0-c19d7d021f76", "e6123145-9665-43e0-8413-cd61b8aa9b13"} + + // конфиг стратегии бота на стакане + orderBookConfig := bot.OrderBookStrategyConfig{ + Instruments: instruments, + Depth: 20, + BuyRatio: 0.5, + SellRatio: 0.5, + } + + // дедлайн для интрадей торговли + //dd, err := tradingDeadLine(client, "MOEX") + //if err != nil { + // logger.Fatalf(err.Error()) + //} + + // создание и запуск бота + botOnOrderBook, err := bot.NewBot(ctx, time.Now().Add(time.Hour), sdkConfig, logger, orderBookConfig) + if err != nil { + logger.Fatalf("bot on order book creating fail %v", err.Error()) + } + + go func() { + <-sigs + fmt.Println("try to stop") + botOnOrderBook.Stop() + }() + + err = botOnOrderBook.Run() + if err != nil { + logger.Errorf(err.Error()) + } +} + +// tradingDeadLine - возвращает дедлайн торгов на сегодня, или ошибку если торговля на бирже сейчас недоступна +func tradingDeadLine(client *investgo.Client, exchange string) (time.Time, error) { + from := time.Now() + // так как основная сессия на бирже не больше 9 часов + to := from.Add(time.Hour * 9) + + instrumentsService := client.NewInstrumentsServiceClient() + resp, err := instrumentsService.TradingSchedules(exchange, from, to) + if err != nil { + return time.Time{}, err + } + + var deadLine time.Time + exchanges := resp.GetExchanges() + for _, exch := range exchanges { + // если нужная биржа + if strings.Compare(exch.GetExchange(), exchange) == 0 { + for _, day := range exch.GetDays() { + // если день совпадает с днем запроса + if from.Day() == day.GetDate().AsTime().Day() { + switch { + // выходной + case !day.GetIsTradingDay(): + return time.Time{}, errors.New("trading isn't available today") + // основная сессия либо еще не началась, либо уже закончилась + case from.Before(day.GetStartTime().AsTime().Local()) || from.After(day.GetEndTime().AsTime().Local()): + return time.Time{}, errors.New("from don't belong trading time") + // положительный случай, возвращаем остаток до конца основной сессии + case from.After(day.GetStartTime().AsTime().Local()) && from.Before(day.GetEndTime().AsTime().Local()): + deadLine = day.GetEndTime().AsTime().Local() + } + } + } + } + } + return deadLine, nil +} diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go new file mode 100644 index 0000000..303886e --- /dev/null +++ b/examples/ob_bot/internal/bot/bot.go @@ -0,0 +1,187 @@ +package bot + +import ( + "context" + "github.com/tinkoff/invest-api-go-sdk/investgo" + pb "github.com/tinkoff/invest-api-go-sdk/proto" + "sync" + "time" +) + +const QUANTITY = 1 + +type Bot struct { + StrategyConfig OrderBookStrategyConfig + Client *investgo.Client + + ctx context.Context + cancelClient context.CancelFunc + cancelBot context.CancelFunc + + executor *Executor +} + +func NewBot(ctx context.Context, dd time.Time, sdkConf investgo.Config, l investgo.Logger, config OrderBookStrategyConfig) (*Bot, error) { + // контекст для клиента с дедлайном + clientCtx, cancelClient := context.WithDeadline(ctx, dd) + // контекст для бота - потомок контекста клиента + botCtx, cancelBot := context.WithCancel(clientCtx) + c, err := investgo.NewClient(clientCtx, sdkConf, l) + if err != nil { + cancelClient() + cancelBot() + return nil, err + } + return &Bot{ + Client: c, + StrategyConfig: config, + ctx: botCtx, + cancelBot: cancelBot, + cancelClient: cancelClient, + }, nil +} + +func (b *Bot) Run() error { + defer func() { + err := b.shutdown() + if err != nil { + b.Client.Logger.Errorf("bot shutdown: %v", err.Error()) + } + }() + + wg := &sync.WaitGroup{} + + instruments := make(map[string]Instrument, len(b.StrategyConfig.Instruments)) + for _, instrument := range b.StrategyConfig.Instruments { + instruments[instrument] = Instrument{quantity: QUANTITY} + } + lastPrices := make(map[string]float64, len(b.StrategyConfig.Instruments)) + + executor := NewExecutor(b.Client, instruments, lastPrices) + b.executor = executor + + // создаем стратегию + s := NewOrderBookStrategy(b.StrategyConfig, executor) + + // инфраструктура для работы стратегии: запрос, получение, преобразование рыночных данных + MarketDataStreamService := b.Client.NewMarketDataStreamClient() + stream, err := MarketDataStreamService.MarketDataStream() + if err != nil { + return err + } + pbOrderBooks, err := stream.SubscribeOrderBook(s.config.Instruments, s.config.Depth) + if err != nil { + return err + } + + lastPricesChan, err := stream.SubscribeLastPrice(s.config.Instruments) + if err != nil { + return err + } + + wg.Add(1) + go func() { + defer wg.Done() + err := stream.Listen() + if err != nil { + b.Client.Logger.Errorf(err.Error()) + } + }() + + orderBooks := make(chan OrderBook) + // defer close(orderBooks) + + // чтение из стрима + wg.Add(1) + go func(ctx context.Context) { + defer func() { + close(orderBooks) + wg.Done() + }() + for { + select { + case <-ctx.Done(): + return + case ob, ok := <-pbOrderBooks: + if !ok { + return + } + orderBooks <- transformOrderBook(ob) + case lp, ok := <-lastPricesChan: + if !ok { + return + } + // обновление данных в мапе последних цен + lastPrices[lp.GetInstrumentUid()] = lp.GetPrice().ToFloat() + } + } + }(b.ctx) + + // данные готовы, далее идет принятие решения и возможное выставление торгового поручения + wg.Add(1) + go func(ctx context.Context) { + defer wg.Done() + err := s.HandleOrderBooks(ctx, orderBooks) + if err != nil { + b.Client.Logger.Errorf(err.Error()) + } + }(b.ctx) + + wg.Wait() + + return nil +} + +func (b *Bot) shutdown() error { + // TODO positions sell and client shutdown + return b.Client.Stop() +} + +// Stop - принудительное завершение работы бота +func (b *Bot) Stop() { + // в конце убиваем клиента + defer b.cancelClient() + // сначала завершаем работу стратегии (всех рутин от бота) + b.Client.Logger.Infof("Stop bot on order book...") + b.cancelBot() + err := b.executor.SellOut() + if err != nil { + b.Client.Logger.Errorf(err.Error()) + } + // TODO graceful stop +} + +func (b *Bot) BackTest() error { + // качаем из бд стаканы сбера и испытываем стратегию + return nil +} + +// Преобразование стакана в нужный формат +func transformOrderBook(input *pb.OrderBook) OrderBook { + depth := input.GetDepth() + bids := make([]Order, 0, depth) + asks := make([]Order, 0, depth) + for _, o := range input.GetBids() { + bids = append(bids, Order{ + Price: o.GetPrice().ToFloat(), + Quantity: o.GetQuantity(), + }) + } + for _, o := range input.GetAsks() { + asks = append(asks, Order{ + Price: o.GetPrice().ToFloat(), + Quantity: o.GetQuantity(), + }) + } + return OrderBook{ + Figi: input.GetFigi(), + InstrumentUid: input.GetInstrumentUid(), + Depth: depth, + IsConsistent: input.GetIsConsistent(), + TimeUnix: input.GetTime().AsTime().Unix(), + LimitUp: input.GetLimitUp().ToFloat(), + LimitDown: input.GetLimitDown().ToFloat(), + Bids: bids, + Asks: asks, + } +} diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go new file mode 100644 index 0000000..a238d0a --- /dev/null +++ b/examples/ob_bot/internal/bot/executor.go @@ -0,0 +1,142 @@ +package bot + +import ( + "fmt" + "github.com/tinkoff/invest-api-go-sdk/investgo" + pb "github.com/tinkoff/invest-api-go-sdk/proto" +) + +const MIN_PROFIT = 1 + +type Instrument struct { + quantity int64 + inStock bool + buyPrice float64 +} + +type Executor struct { + instruments map[string]Instrument + + // lastPrices - read only for executor + lastPrices map[string]float64 + + client *investgo.Client + ordersService *investgo.OrdersServiceClient +} + +func NewExecutor(c *investgo.Client, ids map[string]Instrument, lp map[string]float64) *Executor { + return &Executor{ + instruments: ids, + lastPrices: lp, + client: c, + ordersService: c.NewOrdersServiceClient(), + } +} + +func (e *Executor) Buy(id string) error { + currentInstrument := e.instruments[id] + if currentInstrument.inStock { + return nil + } + resp, err := e.ordersService.Buy(&investgo.PostOrderRequestShort{ + InstrumentId: id, + Quantity: currentInstrument.quantity, + Price: nil, + AccountId: e.client.Config.AccountId, + OrderType: pb.OrderType_ORDER_TYPE_MARKET, + OrderId: investgo.CreateUid(), + }) + if err != nil { + return err + } + if resp.GetExecutionReportStatus() == pb.OrderExecutionReportStatus_EXECUTION_REPORT_STATUS_FILL { + currentInstrument.inStock = true + currentInstrument.buyPrice = resp.GetExecutedOrderPrice().ToFloat() + } + e.instruments[id] = currentInstrument + e.client.Logger.Infof("Buy with %v, price %v", resp.GetFigi(), resp.GetExecutedOrderPrice().ToFloat()) + return nil +} + +func (e *Executor) Sell(id string) error { + currentInstrument := e.instruments[id] + if !currentInstrument.inStock { + return nil + } + if profitable := e.isProfitable(id); !profitable { + return nil + } + + resp, err := e.ordersService.Sell(&investgo.PostOrderRequestShort{ + InstrumentId: id, + Quantity: currentInstrument.quantity, + Price: nil, + AccountId: e.client.Config.AccountId, + OrderType: pb.OrderType_ORDER_TYPE_MARKET, + OrderId: investgo.CreateUid(), + }) + if err != nil { + return err + } + if resp.GetExecutionReportStatus() == pb.OrderExecutionReportStatus_EXECUTION_REPORT_STATUS_FILL { + currentInstrument.inStock = false + e.client.Logger.Infof("profit = %.9f", resp.GetExecutedOrderPrice().ToFloat()-currentInstrument.buyPrice) + } + e.client.Logger.Infof("Sell with %v, price %v", resp.GetFigi(), resp.GetExecutedOrderPrice().ToFloat()) + e.instruments[id] = currentInstrument + return nil +} + +func (e *Executor) isProfitable(id string) bool { + return (e.lastPrices[id] - e.instruments[id].buyPrice) > MIN_PROFIT +} + +func (e *Executor) possibleToBuy() { + +} + +func (e *Executor) possibleToSell() { + +} + +// SellOut - продать все текущие позиции +func (e *Executor) SellOut() error { + operationsService := e.client.NewOperationsServiceClient() + resp, err := operationsService.GetPositions(e.client.Config.AccountId) + if err != nil { + return err + } + // TODO for futures and options + securities := resp.GetSecurities() + for _, security := range securities { + if balance := security.GetBalance(); balance < 0 { + resp, err := e.ordersService.Buy(&investgo.PostOrderRequestShort{ + InstrumentId: security.GetInstrumentUid(), + Quantity: -balance, + Price: nil, + AccountId: e.client.Config.AccountId, + OrderType: pb.OrderType_ORDER_TYPE_MARKET, + OrderId: investgo.CreateUid(), + }) + if err != nil { + fmt.Println(investgo.MessageFromHeader(resp.GetHeader())) + return err + } + } else { + resp, err := e.ordersService.Sell(&investgo.PostOrderRequestShort{ + InstrumentId: security.GetInstrumentUid(), + Quantity: balance, + Price: nil, + AccountId: e.client.Config.AccountId, + OrderType: pb.OrderType_ORDER_TYPE_MARKET, + OrderId: investgo.CreateUid(), + }) + if err != nil { + fmt.Println(investgo.MessageFromHeader(resp.GetHeader())) + return err + } + } + + } + return nil +} diff --git a/examples/ob_bot/internal/bot/model.go b/examples/ob_bot/internal/bot/model.go new file mode 100644 index 0000000..f2a448d --- /dev/null +++ b/examples/ob_bot/internal/bot/model.go @@ -0,0 +1,18 @@ +package bot + +type Order struct { + Price float64 `json:"Price"` + Quantity int64 `json:"Quantity"` +} + +type OrderBook struct { + Figi string `json:"Figi"` + InstrumentUid string `json:"InstrumentUid"` + Depth int32 `json:"Depth"` + IsConsistent bool `json:"IsConsistent"` + TimeUnix int64 `json:"TimeUnix"` + LimitUp float64 `json:"LimitUp"` + LimitDown float64 `json:"LimitDown"` + Bids []Order `json:"Bids"` + Asks []Order `json:"Asks"` +} diff --git a/examples/ob_bot/internal/bot/strategy_on_ob.go b/examples/ob_bot/internal/bot/strategy_on_ob.go new file mode 100644 index 0000000..6e80c5d --- /dev/null +++ b/examples/ob_bot/internal/bot/strategy_on_ob.go @@ -0,0 +1,67 @@ +package bot + +import ( + "context" +) + +type OrderBookStrategyConfig struct { + Instruments []string + Depth int32 + // Если кол-во бид/аск больше чем BuyRatio - покупаем + BuyRatio float64 + // Если кол-во бид/аск меньше чем SellRatio - продаем + SellRatio float64 +} + +type OrderBookStrategy struct { + config OrderBookStrategyConfig + executor *Executor +} + +func NewOrderBookStrategy(config OrderBookStrategyConfig, e *Executor) *OrderBookStrategy { + return &OrderBookStrategy{ + config: config, + executor: e, + } +} + +// HandleOrderBooks - нужно вызвать асинхронно, будет писать в канал id инструментов, которые нужно купить или продать +func (o *OrderBookStrategy) HandleOrderBooks(ctx context.Context, orderBooks chan OrderBook) error { + for { + select { + case <-ctx.Done(): + return nil + case ob, ok := <-orderBooks: + if !ok { + return nil + } + ratio := o.checkRatio(ob) + if ratio > o.config.BuyRatio { + err := o.executor.Buy(ob.InstrumentUid) + if err != nil { + return err + } + } else if 1/ratio > o.config.SellRatio { + err := o.executor.Sell(ob.InstrumentUid) + if err != nil { + return err + } + } + } + } +} + +// checkRate - возвращает значения коэффициента count(ask) / count(bid) +func (o *OrderBookStrategy) checkRatio(ob OrderBook) float64 { + sell := ordersCount(ob.Asks) + buy := ordersCount(ob.Bids) + return float64(buy) / float64(sell) +} + +func ordersCount(o []Order) int64 { + var count int64 + for _, order := range o { + count += order.Quantity + } + return count +} From 68a51259106e021e610ca83ab8179d83a20a08db Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 19 Jun 2023 15:55:52 +0300 Subject: [PATCH 02/57] add stop bot for sellout case --- examples/ob_bot/cmd/main.go | 24 ++++----- examples/ob_bot/internal/bot/bot.go | 51 ++++++++++++++----- .../ob_bot/internal/bot/strategy_on_ob.go | 6 +++ 3 files changed, 56 insertions(+), 25 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index 7c6a07f..fa15091 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -3,7 +3,6 @@ package main import ( "context" "errors" - "fmt" "github.com/tinkoff/invest-api-go-sdk/examples/ob_bot/internal/bot" "github.com/tinkoff/invest-api-go-sdk/investgo" pb "github.com/tinkoff/invest-api-go-sdk/proto" @@ -67,7 +66,7 @@ func main() { instrumentIds := make([]string, 0, 300) instrumentspb := instrumentsResp.GetInstruments() for _, instrument := range instrumentspb { - if len(instrumentIds) > 99 { + if len(instrumentIds) > 19 { break } if strings.Compare(instrument.GetExchange(), "MOEX") == 0 { @@ -81,27 +80,28 @@ func main() { // конфиг стратегии бота на стакане orderBookConfig := bot.OrderBookStrategyConfig{ - Instruments: instruments, - Depth: 20, - BuyRatio: 0.5, - SellRatio: 0.5, + Instruments: instruments, + Depth: 20, + BuyRatio: 0.5, + SellRatio: 0.5, + SellOut: true, + SellOutAhead: 10 * time.Minute, } // дедлайн для интрадей торговли - //dd, err := tradingDeadLine(client, "MOEX") - //if err != nil { - // logger.Fatalf(err.Error()) - //} + dd, err := tradingDeadLine(client, "MOEX") + if err != nil { + logger.Fatalf(err.Error()) + } // создание и запуск бота - botOnOrderBook, err := bot.NewBot(ctx, time.Now().Add(time.Hour), sdkConfig, logger, orderBookConfig) + botOnOrderBook, err := bot.NewBot(ctx, dd, sdkConfig, logger, orderBookConfig) if err != nil { logger.Fatalf("bot on order book creating fail %v", err.Error()) } go func() { <-sigs - fmt.Println("try to stop") botOnOrderBook.Stop() }() diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 303886e..367e249 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -21,11 +21,21 @@ type Bot struct { executor *Executor } +// NewBot - Создание экземпляра бота на стакане +// dd - дедлайн работы бота для интрадей торговли +// каждый бот создает своего клиента для работы с investAPI func NewBot(ctx context.Context, dd time.Time, sdkConf investgo.Config, l investgo.Logger, config OrderBookStrategyConfig) (*Bot, error) { - // контекст для клиента с дедлайном + // Контекст для клиента с дедлайном. cancelClient - принудительно завершает работу клиента и бота, после отмены контекста + // клиента невозможно выполнить никакие запросы к апи. clientCtx, cancelClient := context.WithDeadline(ctx, dd) - // контекст для бота - потомок контекста клиента + // Контекст для бота - потомок контекста клиента. cancelBot - завершает работу стратегии и чтение из стрима, т.е + // всей функции Run, но при этом остается активным клиент для апи. botCtx, cancelBot := context.WithCancel(clientCtx) + // если нужно выходить из позиций, то бот будет завершать свою работу раньше чем дедлайн + if config.SellOut { + botCtx, cancelBot = context.WithDeadline(botCtx, dd.Add(-config.SellOutAhead)) + } + c, err := investgo.NewClient(clientCtx, sdkConf, l) if err != nil { cancelClient() @@ -41,6 +51,7 @@ func NewBot(ctx context.Context, dd time.Time, sdkConf investgo.Config, l invest }, nil } +// Run - Запуск бота func (b *Bot) Run() error { defer func() { err := b.shutdown() @@ -96,6 +107,7 @@ func (b *Bot) Run() error { go func(ctx context.Context) { defer func() { close(orderBooks) + stream.Stop() wg.Done() }() for { @@ -127,28 +139,41 @@ func (b *Bot) Run() error { } }(b.ctx) - wg.Wait() + // Заверешение работы бота по его контексту: вызов Stop() или отмена по дедлайну + for { + select { + case <-b.ctx.Done(): + b.Client.Logger.Infof("stop bot on order book...") + + if b.StrategyConfig.SellOut { + b.Client.Logger.Infof("start positions sell out...") + err := b.executor.SellOut() + if err != nil { + return err + } + } + + stream.Stop() + break + } + break + } + + wg.Wait() return nil } func (b *Bot) shutdown() error { // TODO positions sell and client shutdown + // если Run завершился с ошибкой, то эта функция отменит контекст клиента, внутри бота и закроет соединение + b.cancelClient() return b.Client.Stop() } -// Stop - принудительное завершение работы бота +// Stop - Принудительное завершение работы бота, если = true, то бот выходит из всех активных позиций по счету func (b *Bot) Stop() { - // в конце убиваем клиента - defer b.cancelClient() - // сначала завершаем работу стратегии (всех рутин от бота) - b.Client.Logger.Infof("Stop bot on order book...") b.cancelBot() - err := b.executor.SellOut() - if err != nil { - b.Client.Logger.Errorf(err.Error()) - } - // TODO graceful stop } func (b *Bot) BackTest() error { diff --git a/examples/ob_bot/internal/bot/strategy_on_ob.go b/examples/ob_bot/internal/bot/strategy_on_ob.go index 6e80c5d..30ded66 100644 --- a/examples/ob_bot/internal/bot/strategy_on_ob.go +++ b/examples/ob_bot/internal/bot/strategy_on_ob.go @@ -2,6 +2,7 @@ package bot import ( "context" + "time" ) type OrderBookStrategyConfig struct { @@ -11,6 +12,11 @@ type OrderBookStrategyConfig struct { BuyRatio float64 // Если кол-во бид/аск меньше чем SellRatio - продаем SellRatio float64 + // SellOut - если true, то по достижению дедлайна бот выходит из всех активных позиций + SellOut bool + // (Дедлайн интрадей торговли - SellOutAheadMin) - это момент времени, когда бот начнет продавать + // все активные позиции + SellOutAhead time.Duration } type OrderBookStrategy struct { From 0bd64387e6cf6a030f6180528c1f04d5e79ef09f Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 19 Jun 2023 16:09:14 +0300 Subject: [PATCH 03/57] add lotBy methods --- investgo/instruments.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/investgo/instruments.go b/investgo/instruments.go index e1ac7a2..f6bc7ae 100644 --- a/investgo/instruments.go +++ b/investgo/instruments.go @@ -371,6 +371,24 @@ func (is *InstrumentsServiceClient) InstrumentByPositionUid(id string) (*Instrum return is.instrumentBy(id, pb.InstrumentIdType_INSTRUMENT_ID_TYPE_POSITION_UID, "") } +// LotByUid - Метод получения лотности инструмента по его Uid +func (is *InstrumentsServiceClient) LotByUid(uid string) (int64, error) { + resp, err := is.InstrumentByUid(uid) + if err != nil { + return 0, err + } + return int64(resp.GetInstrument().GetLot()), nil +} + +// LotByFigi - Метод получения лотности инструмента по его FIGI +func (is *InstrumentsServiceClient) LotByFigi(figi string) (int64, error) { + resp, err := is.InstrumentByFigi(figi) + if err != nil { + return 0, err + } + return int64(resp.GetInstrument().GetLot()), nil +} + func (is *InstrumentsServiceClient) instrumentBy(id string, idType pb.InstrumentIdType, classCode string) (*InstrumentResponse, error) { var header, trailer metadata.MD resp, err := is.pbClient.GetInstrumentBy(is.ctx, &pb.InstrumentRequest{ From b76484646b053af79762ca087dcb17fe5bd46738 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 19 Jun 2023 17:04:03 +0300 Subject: [PATCH 04/57] fix balance in sell out --- examples/ob_bot/internal/bot/executor.go | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index a238d0a..63f6908 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -106,13 +106,20 @@ func (e *Executor) SellOut() error { if err != nil { return err } + instrumentsService := e.client.NewInstrumentsServiceClient() // TODO for futures and options securities := resp.GetSecurities() for _, security := range securities { - if balance := security.GetBalance(); balance < 0 { + balance := security.GetBalance() + lot, err := instrumentsService.LotByUid(security.GetInstrumentUid()) + if err != nil { + return err + } + balanceInLots := balance / lot + if balance < 0 { resp, err := e.ordersService.Buy(&investgo.PostOrderRequestShort{ InstrumentId: security.GetInstrumentUid(), - Quantity: -balance, + Quantity: -balanceInLots, Price: nil, AccountId: e.client.Config.AccountId, OrderType: pb.OrderType_ORDER_TYPE_MARKET, @@ -125,7 +132,7 @@ func (e *Executor) SellOut() error { } else { resp, err := e.ordersService.Sell(&investgo.PostOrderRequestShort{ InstrumentId: security.GetInstrumentUid(), - Quantity: balance, + Quantity: balanceInLots, Price: nil, AccountId: e.client.Config.AccountId, OrderType: pb.OrderType_ORDER_TYPE_MARKET, From 750069dced03388ea801fe0eb61a09b5dfb323e8 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 19 Jun 2023 17:48:06 +0300 Subject: [PATCH 05/57] add lot field in Executor instrument --- examples/ob_bot/internal/bot/bot.go | 15 ++++++++-- examples/ob_bot/internal/bot/executor.go | 37 ++++++++++-------------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 367e249..5d830c2 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -62,9 +62,20 @@ func (b *Bot) Run() error { wg := &sync.WaitGroup{} + instrumentService := b.Client.NewInstrumentsServiceClient() instruments := make(map[string]Instrument, len(b.StrategyConfig.Instruments)) for _, instrument := range b.StrategyConfig.Instruments { - instruments[instrument] = Instrument{quantity: QUANTITY} + // в данном случае ключ это uid, поэтому используем LotByUid() + lot, err := instrumentService.LotByUid(instrument) + if err != nil { + return err + } + instruments[instrument] = Instrument{ + quantity: QUANTITY, + inStock: false, + buyPrice: 0, + lot: lot, + } } lastPrices := make(map[string]float64, len(b.StrategyConfig.Instruments)) @@ -139,7 +150,7 @@ func (b *Bot) Run() error { } }(b.ctx) - // Заверешение работы бота по его контексту: вызов Stop() или отмена по дедлайну + // Завершение работы бота по его контексту: вызов Stop() или отмена по дедлайну for { select { case <-b.ctx.Done(): diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 63f6908..0b867bb 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -1,7 +1,6 @@ package bot import ( - "fmt" "github.com/tinkoff/invest-api-go-sdk/investgo" pb "github.com/tinkoff/invest-api-go-sdk/proto" ) @@ -12,6 +11,7 @@ type Instrument struct { quantity int64 inStock bool buyPrice float64 + lot int64 } type Executor struct { @@ -20,16 +20,18 @@ type Executor struct { // lastPrices - read only for executor lastPrices map[string]float64 - client *investgo.Client - ordersService *investgo.OrdersServiceClient + client *investgo.Client + ordersService *investgo.OrdersServiceClient + operationsService *investgo.OperationsServiceClient } func NewExecutor(c *investgo.Client, ids map[string]Instrument, lp map[string]float64) *Executor { return &Executor{ - instruments: ids, - lastPrices: lp, - client: c, - ordersService: c.NewOrdersServiceClient(), + instruments: ids, + lastPrices: lp, + client: c, + ordersService: c.NewOrdersServiceClient(), + operationsService: c.NewOperationsServiceClient(), } } @@ -91,8 +93,8 @@ func (e *Executor) isProfitable(id string) bool { return (e.lastPrices[id] - e.instruments[id].buyPrice) > MIN_PROFIT } -func (e *Executor) possibleToBuy() { - +func (e *Executor) possibleToBuy(id string) { + // required := e.instruments[id].quantity * } func (e *Executor) possibleToSell() { @@ -101,22 +103,15 @@ func (e *Executor) possibleToSell() { // SellOut - продать все текущие позиции func (e *Executor) SellOut() error { - operationsService := e.client.NewOperationsServiceClient() - resp, err := operationsService.GetPositions(e.client.Config.AccountId) + resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) if err != nil { return err } - instrumentsService := e.client.NewInstrumentsServiceClient() // TODO for futures and options securities := resp.GetSecurities() for _, security := range securities { - balance := security.GetBalance() - lot, err := instrumentsService.LotByUid(security.GetInstrumentUid()) - if err != nil { - return err - } - balanceInLots := balance / lot - if balance < 0 { + balanceInLots := security.GetBalance() / e.instruments[security.GetInstrumentUid()].lot + if balanceInLots < 0 { resp, err := e.ordersService.Buy(&investgo.PostOrderRequestShort{ InstrumentId: security.GetInstrumentUid(), Quantity: -balanceInLots, @@ -126,7 +121,7 @@ func (e *Executor) SellOut() error { OrderId: investgo.CreateUid(), }) if err != nil { - fmt.Println(investgo.MessageFromHeader(resp.GetHeader())) + e.client.Logger.Errorf(investgo.MessageFromHeader(resp.GetHeader())) return err } } else { @@ -139,7 +134,7 @@ func (e *Executor) SellOut() error { OrderId: investgo.CreateUid(), }) if err != nil { - fmt.Println(investgo.MessageFromHeader(resp.GetHeader())) + e.client.Logger.Errorf(investgo.MessageFromHeader(resp.GetHeader())) return err } } From cd217c167a3ef34451edfebb48f64022b446b7c7 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 19 Jun 2023 18:05:26 +0300 Subject: [PATCH 06/57] add currency field in Executor instrument --- examples/ob_bot/internal/bot/bot.go | 5 +++-- examples/ob_bot/internal/bot/executor.go | 10 +++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 5d830c2..c692f1b 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -66,7 +66,7 @@ func (b *Bot) Run() error { instruments := make(map[string]Instrument, len(b.StrategyConfig.Instruments)) for _, instrument := range b.StrategyConfig.Instruments { // в данном случае ключ это uid, поэтому используем LotByUid() - lot, err := instrumentService.LotByUid(instrument) + resp, err := instrumentService.InstrumentByUid(instrument) if err != nil { return err } @@ -74,7 +74,8 @@ func (b *Bot) Run() error { quantity: QUANTITY, inStock: false, buyPrice: 0, - lot: lot, + lot: resp.GetInstrument().GetLot(), + currency: resp.GetInstrument().GetCurrency(), } } lastPrices := make(map[string]float64, len(b.StrategyConfig.Instruments)) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 0b867bb..d66a973 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -9,9 +9,11 @@ const MIN_PROFIT = 1 type Instrument struct { quantity int64 + lot int32 + currency string + inStock bool buyPrice float64 - lot int64 } type Executor struct { @@ -94,7 +96,9 @@ func (e *Executor) isProfitable(id string) bool { } func (e *Executor) possibleToBuy(id string) { - // required := e.instruments[id].quantity * + //required := float64(e.instruments[id].quantity) * float64(e.instruments[id].lot) * e.lastPrices[id] + //resp, err := e.operationsService.GetPortfolio(e.client.Config.AccountId, pb.PortfolioRequest_RUB) + //err. } func (e *Executor) possibleToSell() { @@ -110,7 +114,7 @@ func (e *Executor) SellOut() error { // TODO for futures and options securities := resp.GetSecurities() for _, security := range securities { - balanceInLots := security.GetBalance() / e.instruments[security.GetInstrumentUid()].lot + balanceInLots := security.GetBalance() / int64(e.instruments[security.GetInstrumentUid()].lot) if balanceInLots < 0 { resp, err := e.ordersService.Buy(&investgo.PostOrderRequestShort{ InstrumentId: security.GetInstrumentUid(), From f9121938c7656e1ab7a7c60d2e11aafd4c08a7d5 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 19 Jun 2023 18:40:38 +0300 Subject: [PATCH 07/57] add possibleToBuy method --- examples/ob_bot/internal/bot/executor.go | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index d66a973..4c0c4cd 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -42,6 +42,9 @@ func (e *Executor) Buy(id string) error { if currentInstrument.inStock { return nil } + if !e.possibleToBuy(id) { + return nil + } resp, err := e.ordersService.Buy(&investgo.PostOrderRequestShort{ InstrumentId: id, Quantity: currentInstrument.quantity, @@ -95,10 +98,23 @@ func (e *Executor) isProfitable(id string) bool { return (e.lastPrices[id] - e.instruments[id].buyPrice) > MIN_PROFIT } -func (e *Executor) possibleToBuy(id string) { - //required := float64(e.instruments[id].quantity) * float64(e.instruments[id].lot) * e.lastPrices[id] - //resp, err := e.operationsService.GetPortfolio(e.client.Config.AccountId, pb.PortfolioRequest_RUB) - //err. +func (e *Executor) possibleToBuy(id string) bool { + // требуемая сумма для покупки + // кол-во лотов * лотность * стоимость 1 инструмента + required := float64(e.instruments[id].quantity) * float64(e.instruments[id].lot) * e.lastPrices[id] + resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) + if err != nil { + e.client.Logger.Errorf(err.Error()) + } + money := resp.GetMoney() + var moneyInFloat float64 + for _, m := range money { + if m.GetCurrency() == e.instruments[id].currency { + moneyInFloat = m.ToFloat() + } + } + // TODO сравнение дробных чисел + return moneyInFloat > required } func (e *Executor) possibleToSell() { From a1ba6689d0869b8b0b2379c0860b898036c27bf5 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 19 Jun 2023 19:03:29 +0300 Subject: [PATCH 08/57] add min profit to strategy config --- examples/ob_bot/cmd/main.go | 1 + examples/ob_bot/internal/bot/bot.go | 3 ++- examples/ob_bot/internal/bot/executor.go | 10 ++++++---- examples/ob_bot/internal/bot/strategy_on_ob.go | 2 ++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index fa15091..7388f6e 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -84,6 +84,7 @@ func main() { Depth: 20, BuyRatio: 0.5, SellRatio: 0.5, + MinProfit: 0.5, SellOut: true, SellOutAhead: 10 * time.Minute, } diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index c692f1b..e7c4b37 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -8,6 +8,7 @@ import ( "time" ) +// QUANTITY - Кол-во лотов инструментов, которыми торгует бот const QUANTITY = 1 type Bot struct { @@ -80,7 +81,7 @@ func (b *Bot) Run() error { } lastPrices := make(map[string]float64, len(b.StrategyConfig.Instruments)) - executor := NewExecutor(b.Client, instruments, lastPrices) + executor := NewExecutor(b.Client, instruments, lastPrices, b.StrategyConfig.MinProfit) b.executor = executor // создаем стратегию diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 4c0c4cd..ddc562e 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -5,8 +5,6 @@ import ( pb "github.com/tinkoff/invest-api-go-sdk/proto" ) -const MIN_PROFIT = 1 - type Instrument struct { quantity int64 lot int32 @@ -18,6 +16,7 @@ type Instrument struct { type Executor struct { instruments map[string]Instrument + minProfit float64 // lastPrices - read only for executor lastPrices map[string]float64 @@ -27,21 +26,24 @@ type Executor struct { operationsService *investgo.OperationsServiceClient } -func NewExecutor(c *investgo.Client, ids map[string]Instrument, lp map[string]float64) *Executor { +func NewExecutor(c *investgo.Client, ids map[string]Instrument, lp map[string]float64, minProfit float64) *Executor { return &Executor{ instruments: ids, lastPrices: lp, client: c, ordersService: c.NewOrdersServiceClient(), operationsService: c.NewOperationsServiceClient(), + minProfit: minProfit, } } func (e *Executor) Buy(id string) error { currentInstrument := e.instruments[id] + // если этот инструмент уже куплен ботом if currentInstrument.inStock { return nil } + // если не хватает средств для покупки if !e.possibleToBuy(id) { return nil } @@ -95,7 +97,7 @@ func (e *Executor) Sell(id string) error { } func (e *Executor) isProfitable(id string) bool { - return (e.lastPrices[id] - e.instruments[id].buyPrice) > MIN_PROFIT + return ((e.lastPrices[id]-e.instruments[id].buyPrice)/e.instruments[id].buyPrice)*100 > e.minProfit } func (e *Executor) possibleToBuy(id string) bool { diff --git a/examples/ob_bot/internal/bot/strategy_on_ob.go b/examples/ob_bot/internal/bot/strategy_on_ob.go index 30ded66..887ff58 100644 --- a/examples/ob_bot/internal/bot/strategy_on_ob.go +++ b/examples/ob_bot/internal/bot/strategy_on_ob.go @@ -12,6 +12,8 @@ type OrderBookStrategyConfig struct { BuyRatio float64 // Если кол-во бид/аск меньше чем SellRatio - продаем SellRatio float64 + // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки + MinProfit float64 // SellOut - если true, то по достижению дедлайна бот выходит из всех активных позиций SellOut bool // (Дедлайн интрадей торговли - SellOutAheadMin) - это момент времени, когда бот начнет продавать From bdb0f58784af8216ad6509354df89390c2f3adc6 Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 21 Jun 2023 11:09:07 +0300 Subject: [PATCH 09/57] add consts --- examples/ob_bot/cmd/main.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index 7388f6e..b082c7c 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -15,6 +15,11 @@ import ( "time" ) +const ( + SHARES_NUM = 20 + EXCHANGE = "MOEX" +) + func main() { // загружаем конфигурацию для сдк из .yaml файла sdkConfig, err := investgo.LoadConfig("config.yaml") @@ -62,14 +67,15 @@ func main() { if err != nil { logger.Errorf(err.Error()) } - // слайс идентификаторов торговых инструментов + // слайс идентификаторов торговых инструментов instrument_uid + // акции с московской биржи instrumentIds := make([]string, 0, 300) instrumentspb := instrumentsResp.GetInstruments() for _, instrument := range instrumentspb { - if len(instrumentIds) > 19 { + if len(instrumentIds) > SHARES_NUM-1 { break } - if strings.Compare(instrument.GetExchange(), "MOEX") == 0 { + if strings.Compare(instrument.GetExchange(), EXCHANGE) == 0 { instrumentIds = append(instrumentIds, instrument.GetUid()) } } From 149732645d375774aa3a9d79fbf6c5f1f7fa757d Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 21 Jun 2023 11:09:56 +0300 Subject: [PATCH 10/57] add profit as return value in executor.Sell() --- examples/ob_bot/internal/bot/executor.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index ddc562e..0c58041 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -67,13 +67,13 @@ func (e *Executor) Buy(id string) error { return nil } -func (e *Executor) Sell(id string) error { +func (e *Executor) Sell(id string) (float64, error) { currentInstrument := e.instruments[id] if !currentInstrument.inStock { - return nil + return 0, nil } if profitable := e.isProfitable(id); !profitable { - return nil + return 0, nil } resp, err := e.ordersService.Sell(&investgo.PostOrderRequestShort{ @@ -85,15 +85,16 @@ func (e *Executor) Sell(id string) error { OrderId: investgo.CreateUid(), }) if err != nil { - return err + return 0, err } + var profit float64 if resp.GetExecutionReportStatus() == pb.OrderExecutionReportStatus_EXECUTION_REPORT_STATUS_FILL { currentInstrument.inStock = false - e.client.Logger.Infof("profit = %.9f", resp.GetExecutedOrderPrice().ToFloat()-currentInstrument.buyPrice) + profit = resp.GetExecutedOrderPrice().ToFloat() - currentInstrument.buyPrice } e.client.Logger.Infof("Sell with %v, price %v", resp.GetFigi(), resp.GetExecutedOrderPrice().ToFloat()) e.instruments[id] = currentInstrument - return nil + return profit, nil } func (e *Executor) isProfitable(id string) bool { From 1aca2199d6fece0fa04932ab6f216ed43b3030c7 Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 21 Jun 2023 11:10:16 +0300 Subject: [PATCH 11/57] delete strategy struct --- examples/ob_bot/internal/bot/bot.go | 70 +++++++++++++++-- .../ob_bot/internal/bot/strategy_on_ob.go | 75 ------------------- 2 files changed, 64 insertions(+), 81 deletions(-) delete mode 100644 examples/ob_bot/internal/bot/strategy_on_ob.go diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index e7c4b37..644f562 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -11,6 +11,22 @@ import ( // QUANTITY - Кол-во лотов инструментов, которыми торгует бот const QUANTITY = 1 +type OrderBookStrategyConfig struct { + Instruments []string + Depth int32 + // Если кол-во бид/аск больше чем BuyRatio - покупаем + BuyRatio float64 + // Если кол-во бид/аск меньше чем SellRatio - продаем + SellRatio float64 + // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки + MinProfit float64 + // SellOut - если true, то по достижению дедлайна бот выходит из всех активных позиций + SellOut bool + // (Дедлайн интрадей торговли - SellOutAheadMin) - это момент времени, когда бот начнет продавать + // все активные позиции + SellOutAhead time.Duration +} + type Bot struct { StrategyConfig OrderBookStrategyConfig Client *investgo.Client @@ -84,21 +100,18 @@ func (b *Bot) Run() error { executor := NewExecutor(b.Client, instruments, lastPrices, b.StrategyConfig.MinProfit) b.executor = executor - // создаем стратегию - s := NewOrderBookStrategy(b.StrategyConfig, executor) - // инфраструктура для работы стратегии: запрос, получение, преобразование рыночных данных MarketDataStreamService := b.Client.NewMarketDataStreamClient() stream, err := MarketDataStreamService.MarketDataStream() if err != nil { return err } - pbOrderBooks, err := stream.SubscribeOrderBook(s.config.Instruments, s.config.Depth) + pbOrderBooks, err := stream.SubscribeOrderBook(b.StrategyConfig.Instruments, b.StrategyConfig.Depth) if err != nil { return err } - lastPricesChan, err := stream.SubscribeLastPrice(s.config.Instruments) + lastPricesChan, err := stream.SubscribeLastPrice(b.StrategyConfig.Instruments) if err != nil { return err } @@ -146,7 +159,7 @@ func (b *Bot) Run() error { wg.Add(1) go func(ctx context.Context) { defer wg.Done() - err := s.HandleOrderBooks(ctx, orderBooks) + err := b.HandleOrderBooks(ctx, orderBooks) if err != nil { b.Client.Logger.Errorf(err.Error()) } @@ -194,6 +207,51 @@ func (b *Bot) BackTest() error { return nil } +// HandleOrderBooks - нужно вызвать асинхронно, будет писать в канал id инструментов, которые нужно купить или продать +func (b *Bot) HandleOrderBooks(ctx context.Context, orderBooks chan OrderBook) error { + var totalProfit float64 + defer b.Client.Logger.Infof("total profit = %.9f", totalProfit) + for { + select { + case <-ctx.Done(): + return nil + case ob, ok := <-orderBooks: + if !ok { + return nil + } + ratio := b.checkRatio(ob) + if ratio > b.StrategyConfig.BuyRatio { + err := b.executor.Buy(ob.InstrumentUid) + if err != nil { + return err + } + } else if 1/ratio > b.StrategyConfig.SellRatio { + profit, err := b.executor.Sell(ob.InstrumentUid) + if err != nil { + return err + } + b.Client.Logger.Infof("profit = %.9f", profit) + totalProfit += profit + } + } + } +} + +// checkRate - возвращает значения коэффициента count(ask) / count(bid) +func (b *Bot) checkRatio(ob OrderBook) float64 { + sell := ordersCount(ob.Asks) + buy := ordersCount(ob.Bids) + return float64(buy) / float64(sell) +} + +func ordersCount(o []Order) int64 { + var count int64 + for _, order := range o { + count += order.Quantity + } + return count +} + // Преобразование стакана в нужный формат func transformOrderBook(input *pb.OrderBook) OrderBook { depth := input.GetDepth() diff --git a/examples/ob_bot/internal/bot/strategy_on_ob.go b/examples/ob_bot/internal/bot/strategy_on_ob.go deleted file mode 100644 index 887ff58..0000000 --- a/examples/ob_bot/internal/bot/strategy_on_ob.go +++ /dev/null @@ -1,75 +0,0 @@ -package bot - -import ( - "context" - "time" -) - -type OrderBookStrategyConfig struct { - Instruments []string - Depth int32 - // Если кол-во бид/аск больше чем BuyRatio - покупаем - BuyRatio float64 - // Если кол-во бид/аск меньше чем SellRatio - продаем - SellRatio float64 - // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки - MinProfit float64 - // SellOut - если true, то по достижению дедлайна бот выходит из всех активных позиций - SellOut bool - // (Дедлайн интрадей торговли - SellOutAheadMin) - это момент времени, когда бот начнет продавать - // все активные позиции - SellOutAhead time.Duration -} - -type OrderBookStrategy struct { - config OrderBookStrategyConfig - executor *Executor -} - -func NewOrderBookStrategy(config OrderBookStrategyConfig, e *Executor) *OrderBookStrategy { - return &OrderBookStrategy{ - config: config, - executor: e, - } -} - -// HandleOrderBooks - нужно вызвать асинхронно, будет писать в канал id инструментов, которые нужно купить или продать -func (o *OrderBookStrategy) HandleOrderBooks(ctx context.Context, orderBooks chan OrderBook) error { - for { - select { - case <-ctx.Done(): - return nil - case ob, ok := <-orderBooks: - if !ok { - return nil - } - ratio := o.checkRatio(ob) - if ratio > o.config.BuyRatio { - err := o.executor.Buy(ob.InstrumentUid) - if err != nil { - return err - } - } else if 1/ratio > o.config.SellRatio { - err := o.executor.Sell(ob.InstrumentUid) - if err != nil { - return err - } - } - } - } -} - -// checkRate - возвращает значения коэффициента count(ask) / count(bid) -func (o *OrderBookStrategy) checkRatio(ob OrderBook) float64 { - sell := ordersCount(ob.Asks) - buy := ordersCount(ob.Bids) - return float64(buy) / float64(sell) -} - -func ordersCount(o []Order) int64 { - var count int64 - for _, order := range o { - count += order.Quantity - } - return count -} From 3aa81bdb215045ba40fe2b97eb52948eabe2d21a Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 14:54:12 +0300 Subject: [PATCH 12/57] delete client creating in bot --- examples/ob_bot/cmd/main.go | 4 +- examples/ob_bot/internal/bot/bot.go | 83 +++++++----------------- examples/ob_bot/internal/bot/executor.go | 12 +++- 3 files changed, 37 insertions(+), 62 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index b082c7c..246dbcb 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -96,13 +96,13 @@ func main() { } // дедлайн для интрадей торговли - dd, err := tradingDeadLine(client, "MOEX") + dd, err := tradingDeadLine(client, EXCHANGE) if err != nil { logger.Fatalf(err.Error()) } // создание и запуск бота - botOnOrderBook, err := bot.NewBot(ctx, dd, sdkConfig, logger, orderBookConfig) + botOnOrderBook, err := bot.NewBot(ctx, client, dd, orderBookConfig) if err != nil { logger.Fatalf("bot on order book creating fail %v", err.Error()) } diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 644f562..6c879f6 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -11,16 +11,19 @@ import ( // QUANTITY - Кол-во лотов инструментов, которыми торгует бот const QUANTITY = 1 +// OrderBookStrategyConfig - Конфигурация стратегии на стакане type OrderBookStrategyConfig struct { + // Instruments - слайс идентификаторов инструментов Instruments []string - Depth int32 + // Depth - Глубина стакана + Depth int32 // Если кол-во бид/аск больше чем BuyRatio - покупаем BuyRatio float64 // Если кол-во бид/аск меньше чем SellRatio - продаем SellRatio float64 // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки MinProfit float64 - // SellOut - если true, то по достижению дедлайна бот выходит из всех активных позиций + // SellOut - Если true, то по достижению дедлайна бот выходит из всех активных позиций SellOut bool // (Дедлайн интрадей торговли - SellOutAheadMin) - это момент времени, когда бот начнет продавать // все активные позиции @@ -31,9 +34,8 @@ type Bot struct { StrategyConfig OrderBookStrategyConfig Client *investgo.Client - ctx context.Context - cancelClient context.CancelFunc - cancelBot context.CancelFunc + ctx context.Context + cancelBot context.CancelFunc executor *Executor } @@ -41,46 +43,28 @@ type Bot struct { // NewBot - Создание экземпляра бота на стакане // dd - дедлайн работы бота для интрадей торговли // каждый бот создает своего клиента для работы с investAPI -func NewBot(ctx context.Context, dd time.Time, sdkConf investgo.Config, l investgo.Logger, config OrderBookStrategyConfig) (*Bot, error) { - // Контекст для клиента с дедлайном. cancelClient - принудительно завершает работу клиента и бота, после отмены контекста - // клиента невозможно выполнить никакие запросы к апи. - clientCtx, cancelClient := context.WithDeadline(ctx, dd) - // Контекст для бота - потомок контекста клиента. cancelBot - завершает работу стратегии и чтение из стрима, т.е - // всей функции Run, но при этом остается активным клиент для апи. - botCtx, cancelBot := context.WithCancel(clientCtx) +func NewBot(ctx context.Context, c *investgo.Client, dd time.Time, config OrderBookStrategyConfig) (*Bot, error) { + botCtx, cancelBot := context.WithDeadline(ctx, dd) // если нужно выходить из позиций, то бот будет завершать свою работу раньше чем дедлайн if config.SellOut { botCtx, cancelBot = context.WithDeadline(botCtx, dd.Add(-config.SellOutAhead)) } - c, err := investgo.NewClient(clientCtx, sdkConf, l) - if err != nil { - cancelClient() - cancelBot() - return nil, err - } return &Bot{ Client: c, StrategyConfig: config, ctx: botCtx, cancelBot: cancelBot, - cancelClient: cancelClient, }, nil } // Run - Запуск бота func (b *Bot) Run() error { - defer func() { - err := b.shutdown() - if err != nil { - b.Client.Logger.Errorf("bot shutdown: %v", err.Error()) - } - }() - wg := &sync.WaitGroup{} instrumentService := b.Client.NewInstrumentsServiceClient() instruments := make(map[string]Instrument, len(b.StrategyConfig.Instruments)) + for _, instrument := range b.StrategyConfig.Instruments { // в данном случае ключ это uid, поэтому используем LotByUid() resp, err := instrumentService.InstrumentByUid(instrument) @@ -126,14 +110,12 @@ func (b *Bot) Run() error { }() orderBooks := make(chan OrderBook) - // defer close(orderBooks) + defer close(orderBooks) // чтение из стрима wg.Add(1) go func(ctx context.Context) { defer func() { - close(orderBooks) - stream.Stop() wg.Done() }() for { @@ -166,47 +148,29 @@ func (b *Bot) Run() error { }(b.ctx) // Завершение работы бота по его контексту: вызов Stop() или отмена по дедлайну - for { - select { - case <-b.ctx.Done(): - b.Client.Logger.Infof("stop bot on order book...") + <-b.ctx.Done() + b.Client.Logger.Infof("stop bot on order book...") - if b.StrategyConfig.SellOut { - b.Client.Logger.Infof("start positions sell out...") - err := b.executor.SellOut() - if err != nil { - return err - } - } - - stream.Stop() + // стримы работают на контексте клиента, завершать их нужно явно + stream.Stop() - break + if b.StrategyConfig.SellOut { + b.Client.Logger.Infof("start positions sell out...") + err := b.executor.SellOut() + if err != nil { + return err } - break } wg.Wait() return nil } -func (b *Bot) shutdown() error { - // TODO positions sell and client shutdown - // если Run завершился с ошибкой, то эта функция отменит контекст клиента, внутри бота и закроет соединение - b.cancelClient() - return b.Client.Stop() -} - // Stop - Принудительное завершение работы бота, если = true, то бот выходит из всех активных позиций по счету func (b *Bot) Stop() { b.cancelBot() } -func (b *Bot) BackTest() error { - // качаем из бд стаканы сбера и испытываем стратегию - return nil -} - // HandleOrderBooks - нужно вызвать асинхронно, будет писать в канал id инструментов, которые нужно купить или продать func (b *Bot) HandleOrderBooks(ctx context.Context, orderBooks chan OrderBook) error { var totalProfit float64 @@ -230,11 +194,14 @@ func (b *Bot) HandleOrderBooks(ctx context.Context, orderBooks chan OrderBook) e if err != nil { return err } - b.Client.Logger.Infof("profit = %.9f", profit) - totalProfit += profit + if profit > 0 { + b.Client.Logger.Infof("profit = %.9f", profit) + totalProfit += profit + } } } } + } // checkRate - возвращает значения коэффициента count(ask) / count(bid) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 0c58041..cb92918 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -133,7 +133,16 @@ func (e *Executor) SellOut() error { // TODO for futures and options securities := resp.GetSecurities() for _, security := range securities { - balanceInLots := security.GetBalance() / int64(e.instruments[security.GetInstrumentUid()].lot) + var lot int64 + instrument, ok := e.instruments[security.GetInstrumentUid()] + if !ok { + // если бот не открывал эту позицию, он не будет ее закрывать + e.client.Logger.Infof("%v not found in executor instruments map", security.GetInstrumentUid()) + continue + } else { + lot = int64(instrument.lot) + } + balanceInLots := security.GetBalance() / lot if balanceInLots < 0 { resp, err := e.ordersService.Buy(&investgo.PostOrderRequestShort{ InstrumentId: security.GetInstrumentUid(), @@ -161,7 +170,6 @@ func (e *Executor) SellOut() error { return err } } - } return nil } From 80d753c35fde449ba5fed355862179f2b5b3cbf6 Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 16:27:51 +0300 Subject: [PATCH 13/57] add comments and README.md --- examples/ob_bot/README.md | 43 ++++++++++++++++++++++++ examples/ob_bot/cmd/main.go | 14 ++++---- examples/ob_bot/internal/bot/bot.go | 31 +++++++++-------- examples/ob_bot/internal/bot/executor.go | 30 +++++++++++------ 4 files changed, 86 insertions(+), 32 deletions(-) create mode 100644 examples/ob_bot/README.md diff --git a/examples/ob_bot/README.md b/examples/ob_bot/README.md new file mode 100644 index 0000000..b3d0a97 --- /dev/null +++ b/examples/ob_bot/README.md @@ -0,0 +1,43 @@ +## Робот на стакане + +### Стратегия +Робот отслеживает "стакан". Если лотов в заявках на покупку больше, чем в лотах на продажу в BuyRatio раз, +то поступает сигнал на покупку, в противном случае, если лотов в заявках на продажу больше, чем в лотах на покупку +в SellRatio раз - поступает сигнал на продажу + +#### Конфигурация +```go +type OrderBookStrategyConfig struct { + // Instruments - слайс идентификаторов инструментов + Instruments []string + // Depth - Глубина стакана + Depth int32 + // Если кол-во бид/аск больше чем BuyRatio - покупаем + BuyRatio float64 + // Если кол-во бид/аск меньше чем SellRatio - продаем + SellRatio float64 + // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки + MinProfit float64 + // SellOut - Если true, то по достижению дедлайна бот выходит из всех активных позиций + SellOut bool + // (Дедлайн интрадей торговли - SellOutAheadMin) - это момент времени, когда бот начнет продавать + // все активные позиции + SellOutAhead time.Duration +} +``` + +### Исполнитель +Под стратегию написан простейший исполнитель, который выставляет рыночные поручения. +Пока реализована возможность открывать только long позиции. + +**Покупка** + +Заявка на покупку *не* выставляется если: +* Позиция уже открыта +* На счету недостаточно денежных средств + +**Продажа** + +Заявка на продажу *не* выставляется если: +* Позиция не открыта +* Цена открытия позиции меньше цены последней сделки по этому инструменту \ No newline at end of file diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index 246dbcb..5628f38 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -16,8 +16,10 @@ import ( ) const ( - SHARES_NUM = 20 - EXCHANGE = "MOEX" + // SHARES_NUM - Количество акций для торгов + SHARES_NUM = 50 + // EXCHANGE - Биржа на которой будет работать бот + EXCHANGE = "MOEX" ) func main() { @@ -70,13 +72,13 @@ func main() { // слайс идентификаторов торговых инструментов instrument_uid // акции с московской биржи instrumentIds := make([]string, 0, 300) - instrumentspb := instrumentsResp.GetInstruments() - for _, instrument := range instrumentspb { + shares := instrumentsResp.GetInstruments() + for _, share := range shares { if len(instrumentIds) > SHARES_NUM-1 { break } - if strings.Compare(instrument.GetExchange(), EXCHANGE) == 0 { - instrumentIds = append(instrumentIds, instrument.GetUid()) + if strings.Compare(share.GetExchange(), EXCHANGE) == 0 { + instrumentIds = append(instrumentIds, share.GetUid()) } } logger.Infof("got %v instruments\n", len(instrumentIds)) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 6c879f6..a603e22 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -61,7 +61,7 @@ func NewBot(ctx context.Context, c *investgo.Client, dd time.Time, config OrderB // Run - Запуск бота func (b *Bot) Run() error { wg := &sync.WaitGroup{} - + // по конфигу стратегии заполняем map для executor instrumentService := b.Client.NewInstrumentsServiceClient() instruments := make(map[string]Instrument, len(b.StrategyConfig.Instruments)) @@ -72,11 +72,11 @@ func (b *Bot) Run() error { return err } instruments[instrument] = Instrument{ - quantity: QUANTITY, - inStock: false, - buyPrice: 0, - lot: resp.GetInstrument().GetLot(), - currency: resp.GetInstrument().GetCurrency(), + quantity: QUANTITY, + inStock: false, + entryPrice: 0, + lot: resp.GetInstrument().GetLot(), + currency: resp.GetInstrument().GetCurrency(), } } lastPrices := make(map[string]float64, len(b.StrategyConfig.Instruments)) @@ -141,10 +141,11 @@ func (b *Bot) Run() error { wg.Add(1) go func(ctx context.Context) { defer wg.Done() - err := b.HandleOrderBooks(ctx, orderBooks) + profit, err := b.HandleOrderBooks(ctx, orderBooks) if err != nil { b.Client.Logger.Errorf(err.Error()) } + b.Client.Logger.Infof("profit by strategy = %v", profit) }(b.ctx) // Завершение работы бота по его контексту: вызов Stop() или отмена по дедлайну @@ -166,33 +167,32 @@ func (b *Bot) Run() error { return nil } -// Stop - Принудительное завершение работы бота, если = true, то бот выходит из всех активных позиций по счету +// Stop - Принудительное завершение работы бота, если SellOut = true, то бот выходит из всех активных позиций, которые он открыл func (b *Bot) Stop() { b.cancelBot() } // HandleOrderBooks - нужно вызвать асинхронно, будет писать в канал id инструментов, которые нужно купить или продать -func (b *Bot) HandleOrderBooks(ctx context.Context, orderBooks chan OrderBook) error { +func (b *Bot) HandleOrderBooks(ctx context.Context, orderBooks chan OrderBook) (float64, error) { var totalProfit float64 - defer b.Client.Logger.Infof("total profit = %.9f", totalProfit) for { select { case <-ctx.Done(): - return nil + return totalProfit, nil case ob, ok := <-orderBooks: if !ok { - return nil + return totalProfit, nil } ratio := b.checkRatio(ob) if ratio > b.StrategyConfig.BuyRatio { err := b.executor.Buy(ob.InstrumentUid) if err != nil { - return err + return totalProfit, err } } else if 1/ratio > b.StrategyConfig.SellRatio { profit, err := b.executor.Sell(ob.InstrumentUid) if err != nil { - return err + return totalProfit, err } if profit > 0 { b.Client.Logger.Infof("profit = %.9f", profit) @@ -201,7 +201,6 @@ func (b *Bot) HandleOrderBooks(ctx context.Context, orderBooks chan OrderBook) e } } } - } // checkRate - возвращает значения коэффициента count(ask) / count(bid) @@ -219,7 +218,7 @@ func ordersCount(o []Order) int64 { return count } -// Преобразование стакана в нужный формат +// transformOrderBook - Преобразование стакана в нужный формат func transformOrderBook(input *pb.OrderBook) OrderBook { depth := input.GetDepth() bids := make([]Order, 0, depth) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index cb92918..403a59e 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -6,19 +6,26 @@ import ( ) type Instrument struct { + // quantity - Количество лотов, которое покупает/продает исполнитель за 1 поручение quantity int64 - lot int32 + // lot - Лотность инструмента + lot int32 + // currency - Код валюты инструмента currency string - - inStock bool - buyPrice float64 + // inStock - Флаг открытой позиции по инструменту, если true - позиция открыта + inStock bool + // entryPrice - После открытия позиции, сохраняется цена этой сделки + entryPrice float64 } +// Executor - Вызывается ботом и исполняет торговые поручения type Executor struct { + // instruments - Инструменты, которыми торгует исполнитель instruments map[string]Instrument - minProfit float64 + // minProfit - Процент минимального профита, после которого выставляются рыночные заявки + minProfit float64 - // lastPrices - read only for executor + // lastPrices - Мапа последних цен по инструментам, бот в нее пишет, исполнитель читает lastPrices map[string]float64 client *investgo.Client @@ -26,6 +33,7 @@ type Executor struct { operationsService *investgo.OperationsServiceClient } +// NewExecutor - Создание экземпляра исполнителя func NewExecutor(c *investgo.Client, ids map[string]Instrument, lp map[string]float64, minProfit float64) *Executor { return &Executor{ instruments: ids, @@ -37,6 +45,7 @@ func NewExecutor(c *investgo.Client, ids map[string]Instrument, lp map[string]fl } } +// Buy - Метод покупки инструмента с идентификатором id func (e *Executor) Buy(id string) error { currentInstrument := e.instruments[id] // если этот инструмент уже куплен ботом @@ -60,13 +69,14 @@ func (e *Executor) Buy(id string) error { } if resp.GetExecutionReportStatus() == pb.OrderExecutionReportStatus_EXECUTION_REPORT_STATUS_FILL { currentInstrument.inStock = true - currentInstrument.buyPrice = resp.GetExecutedOrderPrice().ToFloat() + currentInstrument.entryPrice = resp.GetExecutedOrderPrice().ToFloat() } e.instruments[id] = currentInstrument e.client.Logger.Infof("Buy with %v, price %v", resp.GetFigi(), resp.GetExecutedOrderPrice().ToFloat()) return nil } +// Sell - Метод покупки инструмента с идентификатором id func (e *Executor) Sell(id string) (float64, error) { currentInstrument := e.instruments[id] if !currentInstrument.inStock { @@ -90,7 +100,7 @@ func (e *Executor) Sell(id string) (float64, error) { var profit float64 if resp.GetExecutionReportStatus() == pb.OrderExecutionReportStatus_EXECUTION_REPORT_STATUS_FILL { currentInstrument.inStock = false - profit = resp.GetExecutedOrderPrice().ToFloat() - currentInstrument.buyPrice + profit = resp.GetExecutedOrderPrice().ToFloat() - currentInstrument.entryPrice } e.client.Logger.Infof("Sell with %v, price %v", resp.GetFigi(), resp.GetExecutedOrderPrice().ToFloat()) e.instruments[id] = currentInstrument @@ -98,7 +108,7 @@ func (e *Executor) Sell(id string) (float64, error) { } func (e *Executor) isProfitable(id string) bool { - return ((e.lastPrices[id]-e.instruments[id].buyPrice)/e.instruments[id].buyPrice)*100 > e.minProfit + return ((e.lastPrices[id]-e.instruments[id].entryPrice)/e.instruments[id].entryPrice)*100 > e.minProfit } func (e *Executor) possibleToBuy(id string) bool { @@ -124,7 +134,7 @@ func (e *Executor) possibleToSell() { } -// SellOut - продать все текущие позиции +// SellOut - Метод выхода из всех текущих позиций func (e *Executor) SellOut() error { resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) if err != nil { From 94d48f765ce05c2f08ac26fb1424cfa889f99923 Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 16:31:29 +0300 Subject: [PATCH 14/57] lint main.go --- examples/ob_bot/cmd/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index 5628f38..fc48366 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -29,8 +29,8 @@ func main() { log.Fatalf("config loading error %v", err.Error()) } - sigs := make(chan os.Signal) - signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM, syscall.SIGKILL) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) ctx, cancel := context.WithCancel(context.Background()) defer cancel() From e63a9fd5ac0a0072da40191a9fb084e368c42e1b Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 17:05:59 +0300 Subject: [PATCH 15/57] change ratio in strategy config --- examples/ob_bot/cmd/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index fc48366..9e8663d 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -90,8 +90,8 @@ func main() { orderBookConfig := bot.OrderBookStrategyConfig{ Instruments: instruments, Depth: 20, - BuyRatio: 0.5, - SellRatio: 0.5, + BuyRatio: 2, + SellRatio: 2, MinProfit: 0.5, SellOut: true, SellOutAhead: 10 * time.Minute, From 64882d9791d1a43290d9a565267e1dad9060f9dd Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 17:59:06 +0300 Subject: [PATCH 16/57] replace strings.Compare() by == operator --- examples/ob_bot/cmd/main.go | 5 ++--- examples/sandbox.go | 3 +-- investgo/client.go | 5 ++--- investgo/marketdata.go | 3 +-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index 9e8663d..23ceb74 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -10,7 +10,6 @@ import ( "log" "os" "os/signal" - "strings" "syscall" "time" ) @@ -77,7 +76,7 @@ func main() { if len(instrumentIds) > SHARES_NUM-1 { break } - if strings.Compare(share.GetExchange(), EXCHANGE) == 0 { + if share.GetExchange() == EXCHANGE { instrumentIds = append(instrumentIds, share.GetUid()) } } @@ -136,7 +135,7 @@ func tradingDeadLine(client *investgo.Client, exchange string) (time.Time, error exchanges := resp.GetExchanges() for _, exch := range exchanges { // если нужная биржа - if strings.Compare(exch.GetExchange(), exchange) == 0 { + if exch.GetExchange() == exchange { for _, day := range exch.GetDays() { // если день совпадает с днем запроса if from.Day() == day.GetDate().AsTime().Day() { diff --git a/examples/sandbox.go b/examples/sandbox.go index 66d2789..7302f2f 100644 --- a/examples/sandbox.go +++ b/examples/sandbox.go @@ -8,7 +8,6 @@ import ( "go.uber.org/zap" "log" "os/signal" - "strings" "syscall" ) @@ -98,7 +97,7 @@ func main() { } else { instruments := instrumentResp.GetInstruments() for _, instrument := range instruments { - if strings.Compare(instrument.GetTicker(), "TCSG") == 0 { + if instrument.GetTicker() == "TCSG" { id = instrument.GetUid() } } diff --git a/investgo/client.go b/investgo/client.go index a988cef..f458c98 100644 --- a/investgo/client.go +++ b/investgo/client.go @@ -12,7 +12,6 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/oauth" "google.golang.org/grpc/metadata" - "strings" "time" ) @@ -92,10 +91,10 @@ func NewClient(ctx context.Context, conf Config, l Logger) (*Client, error) { } func setDefaultConfig(conf *Config) { - if strings.Compare(conf.AppName, "") == 0 { + if conf.AppName == "" { conf.AppName = "invest-api-go-sdk" } - if strings.Compare(conf.EndPoint, "") == 0 { + if conf.EndPoint == "" { conf.EndPoint = "sandbox-invest-public-api.tinkoff.ru:443" } if conf.DisableAllRetry { diff --git a/investgo/marketdata.go b/investgo/marketdata.go index d65d9c8..1e2cf40 100644 --- a/investgo/marketdata.go +++ b/investgo/marketdata.go @@ -7,7 +7,6 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/metadata" "os" - "strings" "time" ) @@ -251,7 +250,7 @@ func selectDuration(interval pb.CandleInterval) time.Duration { // Метод записи в .csv файл исторических свечей в формате instrumentId;time;open;close;high;low;volume func (md *MarketDataServiceClient) writeCandlesToFile(candles []*pb.HistoricCandle, id string, filename string) error { h, m, s := time.Now().Clock() - if strings.Compare(filename, "") == 0 { + if filename == "" { filename = fmt.Sprintf("candles %v:%v:%v", h, m, s) } From 9018e0ccda7240a287a1ea81ffcbcd3536f48e6e Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 17:05:59 +0300 Subject: [PATCH 17/57] change ratio in strategy config --- examples/ob_bot/cmd/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index fc48366..9e8663d 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -90,8 +90,8 @@ func main() { orderBookConfig := bot.OrderBookStrategyConfig{ Instruments: instruments, Depth: 20, - BuyRatio: 0.5, - SellRatio: 0.5, + BuyRatio: 2, + SellRatio: 2, MinProfit: 0.5, SellOut: true, SellOutAhead: 10 * time.Minute, From 6977f0c95a225f9ae73db240eb7b0ecb8eaa64e2 Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 17:59:06 +0300 Subject: [PATCH 18/57] replace strings.Compare() by == operator --- examples/ob_bot/cmd/main.go | 5 ++--- examples/sandbox.go | 3 +-- investgo/client.go | 5 ++--- investgo/marketdata.go | 3 +-- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index 9e8663d..23ceb74 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -10,7 +10,6 @@ import ( "log" "os" "os/signal" - "strings" "syscall" "time" ) @@ -77,7 +76,7 @@ func main() { if len(instrumentIds) > SHARES_NUM-1 { break } - if strings.Compare(share.GetExchange(), EXCHANGE) == 0 { + if share.GetExchange() == EXCHANGE { instrumentIds = append(instrumentIds, share.GetUid()) } } @@ -136,7 +135,7 @@ func tradingDeadLine(client *investgo.Client, exchange string) (time.Time, error exchanges := resp.GetExchanges() for _, exch := range exchanges { // если нужная биржа - if strings.Compare(exch.GetExchange(), exchange) == 0 { + if exch.GetExchange() == exchange { for _, day := range exch.GetDays() { // если день совпадает с днем запроса if from.Day() == day.GetDate().AsTime().Day() { diff --git a/examples/sandbox.go b/examples/sandbox.go index 66d2789..7302f2f 100644 --- a/examples/sandbox.go +++ b/examples/sandbox.go @@ -8,7 +8,6 @@ import ( "go.uber.org/zap" "log" "os/signal" - "strings" "syscall" ) @@ -98,7 +97,7 @@ func main() { } else { instruments := instrumentResp.GetInstruments() for _, instrument := range instruments { - if strings.Compare(instrument.GetTicker(), "TCSG") == 0 { + if instrument.GetTicker() == "TCSG" { id = instrument.GetUid() } } diff --git a/investgo/client.go b/investgo/client.go index a988cef..f458c98 100644 --- a/investgo/client.go +++ b/investgo/client.go @@ -12,7 +12,6 @@ import ( "google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials/oauth" "google.golang.org/grpc/metadata" - "strings" "time" ) @@ -92,10 +91,10 @@ func NewClient(ctx context.Context, conf Config, l Logger) (*Client, error) { } func setDefaultConfig(conf *Config) { - if strings.Compare(conf.AppName, "") == 0 { + if conf.AppName == "" { conf.AppName = "invest-api-go-sdk" } - if strings.Compare(conf.EndPoint, "") == 0 { + if conf.EndPoint == "" { conf.EndPoint = "sandbox-invest-public-api.tinkoff.ru:443" } if conf.DisableAllRetry { diff --git a/investgo/marketdata.go b/investgo/marketdata.go index d65d9c8..1e2cf40 100644 --- a/investgo/marketdata.go +++ b/investgo/marketdata.go @@ -7,7 +7,6 @@ import ( "google.golang.org/grpc" "google.golang.org/grpc/metadata" "os" - "strings" "time" ) @@ -251,7 +250,7 @@ func selectDuration(interval pb.CandleInterval) time.Duration { // Метод записи в .csv файл исторических свечей в формате instrumentId;time;open;close;high;low;volume func (md *MarketDataServiceClient) writeCandlesToFile(candles []*pb.HistoricCandle, id string, filename string) error { h, m, s := time.Now().Clock() - if strings.Compare(filename, "") == 0 { + if filename == "" { filename = fmt.Sprintf("candles %v:%v:%v", h, m, s) } From 8496ff83ab31128388c3735ed8800f1f622514ee Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 18:25:24 +0300 Subject: [PATCH 19/57] Options is deprecated --- investgo/instruments.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/investgo/instruments.go b/investgo/instruments.go index f6bc7ae..f061c7c 100644 --- a/investgo/instruments.go +++ b/investgo/instruments.go @@ -286,6 +286,8 @@ func (is *InstrumentsServiceClient) optionBy(id string, idType pb.InstrumentIdTy } // Options - Метод получения списка опционов +// +// Deprecated: Do not use func (is *InstrumentsServiceClient) Options(status pb.InstrumentStatus) (*OptionsResponse, error) { var header, trailer metadata.MD resp, err := is.pbClient.Options(is.ctx, &pb.InstrumentsRequest{ From 689513b72d1f5e8d654628bd5194c3695f64cb00 Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 18:52:48 +0300 Subject: [PATCH 20/57] update README.md --- examples/ob_bot/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/ob_bot/README.md b/examples/ob_bot/README.md index b3d0a97..20f179d 100644 --- a/examples/ob_bot/README.md +++ b/examples/ob_bot/README.md @@ -1,9 +1,9 @@ ## Робот на стакане ### Стратегия -Робот отслеживает "стакан". Если лотов в заявках на покупку больше, чем в лотах на продажу в BuyRatio раз, +Робот отслеживает "стакан". Если лотов в заявках на покупку больше, чем в лотах на продажу в `BuyRatio` раз, то поступает сигнал на покупку, в противном случае, если лотов в заявках на продажу больше, чем в лотах на покупку -в SellRatio раз - поступает сигнал на продажу +в `SellRatio` раз - поступает сигнал на продажу #### Конфигурация ```go @@ -40,4 +40,9 @@ type OrderBookStrategyConfig struct { Заявка на продажу *не* выставляется если: * Позиция не открыта -* Цена открытия позиции меньше цены последней сделки по этому инструменту \ No newline at end of file +* Цена открытия позиции меньше цены последней сделки по этому инструменту + +### Режим работы +Данный пример ориентирован на торговлю внутри одного дня. При запуске бота функция `tradingDeadLine()` возвращает +дедлайн торгов на сегодня, если выставлен флаг `SellOut` и время `SellOutAhead`, то бот завершит работу и закроет все +позиции за `SellOutAhead`до дедлайна. \ No newline at end of file From 21dcf3e5abecd433451be975758c9321378836ed Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 18:58:26 +0300 Subject: [PATCH 21/57] fix SellRatio description --- examples/ob_bot/README.md | 2 +- examples/ob_bot/internal/bot/bot.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ob_bot/README.md b/examples/ob_bot/README.md index 20f179d..e2f0962 100644 --- a/examples/ob_bot/README.md +++ b/examples/ob_bot/README.md @@ -14,7 +14,7 @@ type OrderBookStrategyConfig struct { Depth int32 // Если кол-во бид/аск больше чем BuyRatio - покупаем BuyRatio float64 - // Если кол-во бид/аск меньше чем SellRatio - продаем + // Если кол-во аск/бид больше чем SellRatio - продаем SellRatio float64 // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки MinProfit float64 diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index a603e22..5ad1d82 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -19,7 +19,7 @@ type OrderBookStrategyConfig struct { Depth int32 // Если кол-во бид/аск больше чем BuyRatio - покупаем BuyRatio float64 - // Если кол-во бид/аск меньше чем SellRatio - продаем + // Если кол-во аск/бид больше чем SellRatio - продаем SellRatio float64 // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки MinProfit float64 From 6d2adb5e445014bab069405cbd2bd08353f01779 Mon Sep 17 00:00:00 2001 From: jstalex Date: Thu, 22 Jun 2023 18:58:26 +0300 Subject: [PATCH 22/57] fix SellRatio description --- examples/ob_bot/README.md | 2 +- examples/ob_bot/internal/bot/bot.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ob_bot/README.md b/examples/ob_bot/README.md index 20f179d..e2f0962 100644 --- a/examples/ob_bot/README.md +++ b/examples/ob_bot/README.md @@ -14,7 +14,7 @@ type OrderBookStrategyConfig struct { Depth int32 // Если кол-во бид/аск больше чем BuyRatio - покупаем BuyRatio float64 - // Если кол-во бид/аск меньше чем SellRatio - продаем + // Если кол-во аск/бид больше чем SellRatio - продаем SellRatio float64 // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки MinProfit float64 diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index a603e22..5ad1d82 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -19,7 +19,7 @@ type OrderBookStrategyConfig struct { Depth int32 // Если кол-во бид/аск больше чем BuyRatio - покупаем BuyRatio float64 - // Если кол-во бид/аск меньше чем SellRatio - продаем + // Если кол-во аск/бид больше чем SellRatio - продаем SellRatio float64 // MinProfit - Минимальный процент выгоды, с которым можно совершать сделки MinProfit float64 From 3915a655d384584ae9c4df45130844238562a40f Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 09:57:25 +0300 Subject: [PATCH 23/57] zap NewExample --- examples/ob_bot/cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index 23ceb74..c4745d7 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -35,7 +35,7 @@ func main() { defer cancel() // сдк использует для внутреннего логирования investgo.Logger // для примера передадим uber.zap - prod, _ := zap.NewProduction() + prod := zap.NewExample() defer func() { err := prod.Sync() if err != nil { From 614f50eb1a9639290590348cdb1c38a7370e9d6f Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 09:58:41 +0300 Subject: [PATCH 24/57] update README.md --- examples/ob_bot/README.md | 2 +- examples/ob_bot/internal/bot/bot.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ob_bot/README.md b/examples/ob_bot/README.md index e2f0962..d5a1811 100644 --- a/examples/ob_bot/README.md +++ b/examples/ob_bot/README.md @@ -20,7 +20,7 @@ type OrderBookStrategyConfig struct { MinProfit float64 // SellOut - Если true, то по достижению дедлайна бот выходит из всех активных позиций SellOut bool - // (Дедлайн интрадей торговли - SellOutAheadMin) - это момент времени, когда бот начнет продавать + // (Дедлайн интрадей торговли - SellOutAhead) - это момент времени, когда бот начнет продавать // все активные позиции SellOutAhead time.Duration } diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 5ad1d82..52a69ef 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -25,7 +25,7 @@ type OrderBookStrategyConfig struct { MinProfit float64 // SellOut - Если true, то по достижению дедлайна бот выходит из всех активных позиций SellOut bool - // (Дедлайн интрадей торговли - SellOutAheadMin) - это момент времени, когда бот начнет продавать + // (Дедлайн интрадей торговли - SellOutAhead) - это момент времени, когда бот начнет продавать // все активные позиции SellOutAhead time.Duration } From a38b692b7531a6ebaf8d544ee488a28fcd5d1e06 Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 10:40:29 +0300 Subject: [PATCH 25/57] fix profit value --- examples/ob_bot/internal/bot/executor.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 403a59e..41d0ada 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -100,7 +100,8 @@ func (e *Executor) Sell(id string) (float64, error) { var profit float64 if resp.GetExecutionReportStatus() == pb.OrderExecutionReportStatus_EXECUTION_REPORT_STATUS_FILL { currentInstrument.inStock = false - profit = resp.GetExecutedOrderPrice().ToFloat() - currentInstrument.entryPrice + // разница в цене инструмента * лотность * кол-во лотов + profit = (resp.GetExecutedOrderPrice().ToFloat() - currentInstrument.entryPrice) * float64(currentInstrument.lot) * float64(currentInstrument.quantity) } e.client.Logger.Infof("Sell with %v, price %v", resp.GetFigi(), resp.GetExecutedOrderPrice().ToFloat()) e.instruments[id] = currentInstrument From 7bceea61ce0c41c85d90d22ebcd2199f9f2c8ffa Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 11:46:26 +0300 Subject: [PATCH 26/57] update README.md --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6b340dd..8f01fe8 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,38 @@ SDK предназначен для упрощения работы с API Ти * `sandbox.go` - пример работы с песочницей * `order_book_download/order_book.go` - пример сохранения стаканов из стрима маркетдаты в sqlite или json -#### Конфигурация SDK +### Запуск примеров + +#### 1. Клонирование репозитория + + $ git clone https://github.com/tinkoff/invest-api-go-sdk + +#### 2. Конфигурация SDK +Перейдите в директорию с примерами + + $ cd invest-api-go-sdk/examples + +Создайте файл `config.yaml` + + $ touch "config.yaml" + +И заполните его по примеру `example.yaml` + +```yaml +AccountId: "" +APIToken: +EndPoint: sandbox-invest-public-api.tinkoff.ru:443 +AppName: invest-api-go-sdk +DisableResourceExhaustedRetry: false +DisableAllRetry: false +MaxRetries: 3 +``` + +*Для быстрого старта на песочнице достаточно указать только токен, остальное заполнится по умолчанию.* + +Так же вы можете не использовать `.yaml` файлы, а в main функции вместо `investgo.LoadConfig()` +явно создать `investgo.Config`, и заполнить его по описанию: + ```go type Config struct { // EndPoint - Для работы с реальным контуром и контуром песочницы нужны разные эндпоинты. @@ -59,7 +90,14 @@ DisableAllRetry bool `yaml:"DisableAllRetry"` MaxRetries uint `yaml:"MaxRetries"` } ``` -Для проверки достаточно указать токен и запустить пример `sandbox.go` + +#### 3. Запуск +Пример использования `MarketDataStreamService`: + + $ go run md_stream.go +Загрузка стаканов из стрима: + + $ go run order_book_download/order_book.go ### Дополнительные возможности * **Загрузка исторических данных.** В рамках сервиса `Marketdata`, метод `GetHistoricCandles` возвращает список From 1eb4d632aa4fad739822e34d26383a402cdfd336 Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 11:50:30 +0300 Subject: [PATCH 27/57] fix dp path in order_book.go --- examples/order_book_download/order_book.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/examples/order_book_download/order_book.go b/examples/order_book_download/order_book.go index 8d51b4d..bd29d09 100644 --- a/examples/order_book_download/order_book.go +++ b/examples/order_book_download/order_book.go @@ -68,7 +68,7 @@ create table if not exists asks ( func main() { // создаем базу данных sqlite - db, err := initDB("examples/order_book_download/order_books.db") + db, err := initDB("order_book_download/order_books.db") if err != nil { log.Fatalf(err.Error()) } @@ -296,7 +296,7 @@ func main() { wg.Wait() } -// преобразование стакана в нужный формат +// transformOrderBook - Преобразование стакана в нужный формат func transformOrderBook(input *pb.OrderBook) *OrderBook { depth := input.GetDepth() bids := make([]Order, 0, depth) @@ -326,7 +326,7 @@ func transformOrderBook(input *pb.OrderBook) *OrderBook { } } -// сохранение стаканов в json +// storeOrderBooksInFile - Сохранение стаканов в json func storeOrderBooksInFile(orderBooks []*OrderBook) error { file, err := os.Create("order_books.json") if err != nil { @@ -346,7 +346,7 @@ func storeOrderBooksInFile(orderBooks []*OrderBook) error { return err } -// инициализация бд +// initDB - Инициализация бд func initDB(path string) (*sqlx.DB, error) { db, err := sqlx.Open("sqlite3", path) if err != nil { @@ -363,7 +363,7 @@ func initDB(path string) (*sqlx.DB, error) { return db, nil } -// сохранение партии стаканов в бд +// storeOrderBooksInDB - Сохранение партии стаканов в бд func storeOrderBooksInDB(db *sqlx.DB, obooks []*OrderBook) error { tx, err := db.Begin() if err != nil { From 816a5a50a81103eb168bf5c3dc9b075c1b6d483c Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 15:50:47 +0300 Subject: [PATCH 28/57] add money balance check --- examples/ob_bot/internal/bot/bot.go | 56 ++++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 4 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 52a69ef..0fbf1c9 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -2,8 +2,10 @@ package bot import ( "context" + "errors" "github.com/tinkoff/invest-api-go-sdk/investgo" pb "github.com/tinkoff/invest-api-go-sdk/proto" + "strings" "sync" "time" ) @@ -50,17 +52,27 @@ func NewBot(ctx context.Context, c *investgo.Client, dd time.Time, config OrderB botCtx, cancelBot = context.WithDeadline(botCtx, dd.Add(-config.SellOutAhead)) } + instruments := make(map[string]Instrument, len(config.Instruments)) + executor := NewExecutor(ctx, c, instruments, config.MinProfit) + return &Bot{ Client: c, StrategyConfig: config, ctx: botCtx, cancelBot: cancelBot, + executor: executor, }, nil } // Run - Запуск бота func (b *Bot) Run() error { wg := &sync.WaitGroup{} + + err := b.checkMoneyBalance("RUB", 200000) + if err != nil { + b.Client.Logger.Fatalf(err.Error()) + } + // по конфигу стратегии заполняем map для executor instrumentService := b.Client.NewInstrumentsServiceClient() instruments := make(map[string]Instrument, len(b.StrategyConfig.Instruments)) @@ -79,10 +91,8 @@ func (b *Bot) Run() error { currency: resp.GetInstrument().GetCurrency(), } } - lastPrices := make(map[string]float64, len(b.StrategyConfig.Instruments)) - executor := NewExecutor(b.Client, instruments, lastPrices, b.StrategyConfig.MinProfit) - b.executor = executor + b.executor.instruments = instruments // инфраструктура для работы стратегии: запрос, получение, преобразование рыночных данных MarketDataStreamService := b.Client.NewMarketDataStreamClient() @@ -132,7 +142,7 @@ func (b *Bot) Run() error { return } // обновление данных в мапе последних цен - lastPrices[lp.GetInstrumentUid()] = lp.GetPrice().ToFloat() + b.executor.lastPrices[lp.GetInstrumentUid()] = lp.GetPrice().ToFloat() } } }(b.ctx) @@ -218,6 +228,44 @@ func ordersCount(o []Order) int64 { return count } +// checkMoneyBalance - проверка доступного баланса денежных средств +func (b *Bot) checkMoneyBalance(currency string, required float64) error { + operationsService := b.Client.NewOperationsServiceClient() + + resp, err := operationsService.GetPositions(b.Client.Config.AccountId) + if err != nil { + return err + } + var balance float64 + money := resp.GetMoney() + for _, m := range money { + b.Client.Logger.Infof("money balance = %v %v", m.ToFloat(), m.GetCurrency()) + if m.GetCurrency() == currency { + balance = m.ToFloat() + } + } + + if diff := balance - required; diff < 0 { + if strings.HasPrefix(b.Client.Config.EndPoint, "sandbox") { + sandbox := b.Client.NewSandboxServiceClient() + resp, err := sandbox.SandboxPayIn(&investgo.SandboxPayInRequest{ + AccountId: b.Client.Config.AccountId, + Currency: currency, + Unit: int64(-diff), + Nano: 0, + }) + if err != nil { + return err + } + b.Client.Logger.Infof("sandbox auto pay in, balance = %v", resp.GetBalance().ToFloat()) + } else { + return errors.New("not enough money on balance") + } + } + + return nil +} + // transformOrderBook - Преобразование стакана в нужный формат func transformOrderBook(input *pb.OrderBook) OrderBook { depth := input.GetDepth() From e92e1449b4db0e2941ced386a7c5d852c6390f2c Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 15:51:35 +0300 Subject: [PATCH 29/57] add positions stream --- examples/ob_bot/internal/bot/executor.go | 125 +++++++++++++++++++---- 1 file changed, 103 insertions(+), 22 deletions(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 41d0ada..49cc8ad 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -1,8 +1,10 @@ package bot import ( + "context" "github.com/tinkoff/invest-api-go-sdk/investgo" pb "github.com/tinkoff/invest-api-go-sdk/proto" + "sync" ) type Instrument struct { @@ -18,6 +20,27 @@ type Instrument struct { entryPrice float64 } +type Positions struct { + mx sync.Mutex + pd *pb.PositionData +} + +func NewPositions() *Positions { + return &Positions{pd: &pb.PositionData{}} +} + +func (p *Positions) Update(data *pb.PositionData) { + p.mx.Lock() + p.pd = data + p.mx.Unlock() +} + +func (p *Positions) Get() *pb.PositionData { + p.mx.Lock() + defer p.mx.Unlock() + return p.pd +} + // Executor - Вызывается ботом и исполняет торговые поручения type Executor struct { // instruments - Инструменты, которыми торгует исполнитель @@ -27,22 +50,84 @@ type Executor struct { // lastPrices - Мапа последних цен по инструментам, бот в нее пишет, исполнитель читает lastPrices map[string]float64 + positions *Positions - client *investgo.Client - ordersService *investgo.OrdersServiceClient - operationsService *investgo.OperationsServiceClient + client *investgo.Client + ordersService *investgo.OrdersServiceClient } // NewExecutor - Создание экземпляра исполнителя -func NewExecutor(c *investgo.Client, ids map[string]Instrument, lp map[string]float64, minProfit float64) *Executor { - return &Executor{ - instruments: ids, - lastPrices: lp, - client: c, - ordersService: c.NewOrdersServiceClient(), - operationsService: c.NewOperationsServiceClient(), - minProfit: minProfit, +func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrument, minProfit float64) *Executor { + e := &Executor{ + instruments: ids, + lastPrices: make(map[string]float64, len(ids)), + client: c, + ordersService: c.NewOrdersServiceClient(), + minProfit: minProfit, + positions: NewPositions(), } + + go func(ctx context.Context) { + err := e.updatePositions(ctx) + if err != nil { + e.client.Logger.Errorf(err.Error()) + } + }(ctx) + + return e +} + +func (e *Executor) updatePositions(ctx context.Context) error { + operationsStreamService := e.client.NewOperationsStreamClient() + operationsService := e.client.NewOperationsServiceClient() + // в начале получаем баланс денежных средств на счете + resp, err := operationsService.GetPositions(e.client.Config.AccountId) + if err != nil { + return err + } + + money := resp.GetMoney() + positionMoney := make([]*pb.PositionsMoney, 0) + for _, m := range money { + positionMoney = append(positionMoney, &pb.PositionsMoney{ + AvailableValue: m, + BlockedValue: nil, + }) + } + // обновляем баланс для исполнителя + e.positions.Update(&pb.PositionData{Money: positionMoney}) + + stream, err := operationsStreamService.PositionsStream([]string{e.client.Config.AccountId}) + if err != nil { + return err + } + positionsChan := stream.Positions() + + go func() { + err := stream.Listen() + if err != nil { + e.client.Logger.Errorf(err.Error()) + } + }() + + go func(ctx context.Context) { + for { + select { + case <-ctx.Done(): + return + case p, ok := <-positionsChan: + if !ok { + return + } + e.positions.Update(p) + } + } + }(ctx) + + <-ctx.Done() + e.client.Logger.Infof("stop updating positions in executor") + stream.Stop() + return nil } // Buy - Метод покупки инструмента с идентификатором id @@ -116,18 +201,18 @@ func (e *Executor) possibleToBuy(id string) bool { // требуемая сумма для покупки // кол-во лотов * лотность * стоимость 1 инструмента required := float64(e.instruments[id].quantity) * float64(e.instruments[id].lot) * e.lastPrices[id] - resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) - if err != nil { - e.client.Logger.Errorf(err.Error()) - } - money := resp.GetMoney() + positionMoney := e.positions.Get().GetMoney() var moneyInFloat float64 - for _, m := range money { + for _, pm := range positionMoney { + m := pm.GetAvailableValue() if m.GetCurrency() == e.instruments[id].currency { moneyInFloat = m.ToFloat() } } // TODO сравнение дробных чисел + if moneyInFloat < required { + e.client.Logger.Infof("executor: not enough money to buy order") + } return moneyInFloat > required } @@ -137,12 +222,8 @@ func (e *Executor) possibleToSell() { // SellOut - Метод выхода из всех текущих позиций func (e *Executor) SellOut() error { - resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) - if err != nil { - return err - } // TODO for futures and options - securities := resp.GetSecurities() + securities := e.positions.Get().GetSecurities() for _, security := range securities { var lot int64 instrument, ok := e.instruments[security.GetInstrumentUid()] From eba8547d69a9f70eb329c193a308967c868795c1 Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 15:52:13 +0300 Subject: [PATCH 30/57] change shares num --- examples/ob_bot/cmd/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index c4745d7..dab0551 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -16,7 +16,7 @@ import ( const ( // SHARES_NUM - Количество акций для торгов - SHARES_NUM = 50 + SHARES_NUM = 30 // EXCHANGE - Биржа на которой будет работать бот EXCHANGE = "MOEX" ) From af38c03d38555730d56432409666b547e4259225 Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 15:52:31 +0300 Subject: [PATCH 31/57] update README.md --- examples/ob_bot/README.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/examples/ob_bot/README.md b/examples/ob_bot/README.md index d5a1811..06d4d56 100644 --- a/examples/ob_bot/README.md +++ b/examples/ob_bot/README.md @@ -45,4 +45,31 @@ type OrderBookStrategyConfig struct { ### Режим работы Данный пример ориентирован на торговлю внутри одного дня. При запуске бота функция `tradingDeadLine()` возвращает дедлайн торгов на сегодня, если выставлен флаг `SellOut` и время `SellOutAhead`, то бот завершит работу и закроет все -позиции за `SellOutAhead`до дедлайна. \ No newline at end of file +позиции за `SellOutAhead`до дедлайна. + +### Запуск + + $ cd examples/ob_bot + +Создайте файл `config.yaml` + + $ touch "config.yaml" + +И заполните его по примеру `example.yaml` + +```yaml +AccountId: "" +APIToken: +EndPoint: sandbox-invest-public-api.tinkoff.ru:443 +AppName: invest-api-go-sdk +DisableResourceExhaustedRetry: false +DisableAllRetry: false +MaxRetries: 3 +``` + +*Для быстрого старта на песочнице достаточно указать только токен, остальное заполнится по умолчанию.* + + $ go run cmd/main.go + +Обратите внимание, что в одной функции main есть возможность создать несколько клиентов для investAPI c разными +токенами и счетами, а с разными клиентами можно создавать разных ботов и запускать их одновременно. \ No newline at end of file From ee7953064ab52a3198c0e963e34a174c7309e4a7 Mon Sep 17 00:00:00 2001 From: jstalex Date: Fri, 23 Jun 2023 18:59:40 +0300 Subject: [PATCH 32/57] add UpdatePositions by unary call --- examples/ob_bot/internal/bot/bot.go | 4 ++++ examples/ob_bot/internal/bot/executor.go | 15 +++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 0fbf1c9..4acec5f 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -258,6 +258,10 @@ func (b *Bot) checkMoneyBalance(currency string, required float64) error { return err } b.Client.Logger.Infof("sandbox auto pay in, balance = %v", resp.GetBalance().ToFloat()) + err = b.executor.UpdatePositions() + if err != nil { + return err + } } else { return errors.New("not enough money on balance") } diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 49cc8ad..72b8015 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -68,7 +68,7 @@ func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrum } go func(ctx context.Context) { - err := e.updatePositions(ctx) + err := e.checkPositions(ctx) if err != nil { e.client.Logger.Errorf(err.Error()) } @@ -77,10 +77,9 @@ func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrum return e } -func (e *Executor) updatePositions(ctx context.Context) error { - operationsStreamService := e.client.NewOperationsStreamClient() +func (e *Executor) UpdatePositions() error { operationsService := e.client.NewOperationsServiceClient() - // в начале получаем баланс денежных средств на счете + resp, err := operationsService.GetPositions(e.client.Config.AccountId) if err != nil { return err @@ -96,7 +95,15 @@ func (e *Executor) updatePositions(ctx context.Context) error { } // обновляем баланс для исполнителя e.positions.Update(&pb.PositionData{Money: positionMoney}) + return nil +} +func (e *Executor) checkPositions(ctx context.Context) error { + err := e.UpdatePositions() + if err != nil { + return err + } + operationsStreamService := e.client.NewOperationsStreamClient() stream, err := operationsStreamService.PositionsStream([]string{e.client.Config.AccountId}) if err != nil { return err From d6ca725bc3e9ae9d5f439588084157ea307fc0b4 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 08:58:25 +0300 Subject: [PATCH 33/57] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8f01fe8..591d46a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ SDK предназначен для упрощения работы с API Ти * Перейдите в [настройки](https://www.tinkoff.ru/invest/settings/) * Проверьте, что функция “Подтверждение сделок кодом” отключена * Выпустите токен (если не хотите через API выдавать торговые поручения, то надо выпустить токен "только для чтения") -* Скопируйте токен и сохраните, токен отображается только один раз, просмотреть его позже не получится, тем не менее вы можете выпускать неограниченное количество токенов. Токен передается в библиотеку в методе инициализации SDKInit() +* Скопируйте токен и сохраните, токен отображается только один раз, просмотреть его позже не получится, тем не менее вы можете выпускать неограниченное количество токенов. ## Документация From a21b42e0c673fd8683f62005924458e3d2426494 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 10:40:07 +0300 Subject: [PATCH 34/57] rename methods --- examples/ob_bot/internal/bot/bot.go | 2 +- examples/ob_bot/internal/bot/executor.go | 32 +++++++++++++----------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 4acec5f..9c4523d 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -258,7 +258,7 @@ func (b *Bot) checkMoneyBalance(currency string, required float64) error { return err } b.Client.Logger.Infof("sandbox auto pay in, balance = %v", resp.GetBalance().ToFloat()) - err = b.executor.UpdatePositions() + err = b.executor.UpdateBalance() if err != nil { return err } diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 72b8015..712b55b 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -2,6 +2,7 @@ package bot import ( "context" + "fmt" "github.com/tinkoff/invest-api-go-sdk/investgo" pb "github.com/tinkoff/invest-api-go-sdk/proto" "sync" @@ -52,23 +53,25 @@ type Executor struct { lastPrices map[string]float64 positions *Positions - client *investgo.Client - ordersService *investgo.OrdersServiceClient + client *investgo.Client + ordersService *investgo.OrdersServiceClient + operationsService *investgo.OperationsServiceClient } // NewExecutor - Создание экземпляра исполнителя func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrument, minProfit float64) *Executor { e := &Executor{ - instruments: ids, - lastPrices: make(map[string]float64, len(ids)), - client: c, - ordersService: c.NewOrdersServiceClient(), - minProfit: minProfit, - positions: NewPositions(), + instruments: ids, + lastPrices: make(map[string]float64, len(ids)), + client: c, + ordersService: c.NewOrdersServiceClient(), + operationsService: c.NewOperationsServiceClient(), + minProfit: minProfit, + positions: NewPositions(), } go func(ctx context.Context) { - err := e.checkPositions(ctx) + err := e.updatePositions(ctx) if err != nil { e.client.Logger.Errorf(err.Error()) } @@ -77,10 +80,8 @@ func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrum return e } -func (e *Executor) UpdatePositions() error { - operationsService := e.client.NewOperationsServiceClient() - - resp, err := operationsService.GetPositions(e.client.Config.AccountId) +func (e *Executor) UpdateBalance() error { + resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) if err != nil { return err } @@ -98,8 +99,8 @@ func (e *Executor) UpdatePositions() error { return nil } -func (e *Executor) checkPositions(ctx context.Context) error { - err := e.UpdatePositions() +func (e *Executor) updatePositions(ctx context.Context) error { + err := e.UpdateBalance() if err != nil { return err } @@ -126,6 +127,7 @@ func (e *Executor) checkPositions(ctx context.Context) error { if !ok { return } + fmt.Printf("from stream %v\n", p.GetMoney()) e.positions.Update(p) } } From 38780bd308fde871334b38753fbb7cd59d1e3926 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 11:10:10 +0300 Subject: [PATCH 35/57] get securities in sellOut by unary method --- examples/ob_bot/internal/bot/executor.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 712b55b..ed4c6b4 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -232,7 +232,12 @@ func (e *Executor) possibleToSell() { // SellOut - Метод выхода из всех текущих позиций func (e *Executor) SellOut() error { // TODO for futures and options - securities := e.positions.Get().GetSecurities() + resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) + if err != nil { + return err + } + + securities := resp.GetSecurities() for _, security := range securities { var lot int64 instrument, ok := e.instruments[security.GetInstrumentUid()] From eeb5bc888c64697f11b70e4561a3cbff1d03c94e Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 12:19:04 +0300 Subject: [PATCH 36/57] change update balance to unary update all positions --- examples/ob_bot/internal/bot/bot.go | 4 +-- examples/ob_bot/internal/bot/executor.go | 46 +++++++++++++++++++----- 2 files changed, 39 insertions(+), 11 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 9c4523d..fd880d2 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -240,7 +240,7 @@ func (b *Bot) checkMoneyBalance(currency string, required float64) error { money := resp.GetMoney() for _, m := range money { b.Client.Logger.Infof("money balance = %v %v", m.ToFloat(), m.GetCurrency()) - if m.GetCurrency() == currency { + if strings.ToLower(m.GetCurrency()) == strings.ToLower(currency) { balance = m.ToFloat() } } @@ -258,7 +258,7 @@ func (b *Bot) checkMoneyBalance(currency string, required float64) error { return err } b.Client.Logger.Infof("sandbox auto pay in, balance = %v", resp.GetBalance().ToFloat()) - err = b.executor.UpdateBalance() + err = b.executor.updatePositionsUnary() if err != nil { return err } diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index ed4c6b4..41584ed 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -6,6 +6,7 @@ import ( "github.com/tinkoff/invest-api-go-sdk/investgo" pb "github.com/tinkoff/invest-api-go-sdk/proto" "sync" + "time" ) type Instrument struct { @@ -80,27 +81,54 @@ func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrum return e } -func (e *Executor) UpdateBalance() error { +func (e *Executor) updatePositionsUnary() error { resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) if err != nil { return err } + // два слайса *MoneyValue + available := resp.GetMoney() + blocked := resp.GetBlocked() - money := resp.GetMoney() + // слайс *PositionMoney positionMoney := make([]*pb.PositionsMoney, 0) - for _, m := range money { - positionMoney = append(positionMoney, &pb.PositionsMoney{ - AvailableValue: m, + // ключ - код валюты, значение - *PositionMoney + moneyByCurrency := make(map[string]*pb.PositionsMoney, 0) + + for _, avail := range available { + moneyByCurrency[avail.GetCurrency()] = &pb.PositionsMoney{ + AvailableValue: avail, BlockedValue: nil, - }) + } + } + + for _, block := range blocked { + m := moneyByCurrency[block.GetCurrency()] + moneyByCurrency[block.GetCurrency()] = &pb.PositionsMoney{ + AvailableValue: m.GetAvailableValue(), + BlockedValue: block, + } } - // обновляем баланс для исполнителя - e.positions.Update(&pb.PositionData{Money: positionMoney}) + + for _, money := range moneyByCurrency { + positionMoney = append(positionMoney, money) + } + + // обновляем позиции для исполнителя + e.positions.Update(&pb.PositionData{ + AccountId: e.client.Config.AccountId, + Money: positionMoney, + Securities: resp.GetSecurities(), + Futures: resp.GetFutures(), + Options: resp.GetOptions(), + Date: investgo.TimeToTimestamp(time.Now()), + }) + return nil } func (e *Executor) updatePositions(ctx context.Context) error { - err := e.UpdateBalance() + err := e.updatePositionsUnary() if err != nil { return err } From ac52454c4614f41289c6c7c98a0023590162e324 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 12:37:44 +0300 Subject: [PATCH 37/57] add unary update positions in possibleToBuy --- examples/ob_bot/internal/bot/executor.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 41584ed..2d7eec3 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -2,7 +2,6 @@ package bot import ( "context" - "fmt" "github.com/tinkoff/invest-api-go-sdk/investgo" pb "github.com/tinkoff/invest-api-go-sdk/proto" "sync" @@ -155,7 +154,7 @@ func (e *Executor) updatePositions(ctx context.Context) error { if !ok { return } - fmt.Printf("from stream %v\n", p.GetMoney()) + e.client.Logger.Infof("update from positions stream %v\n", p.GetMoney()) e.positions.Update(p) } } @@ -246,6 +245,17 @@ func (e *Executor) possibleToBuy(id string) bool { moneyInFloat = m.ToFloat() } } + + // TODO убрать, когда починят стрим + if moneyInFloat < 0 { + e.client.Logger.Infof("balance < 0, update positions by unary call") + err := e.updatePositionsUnary() + if err != nil { + e.client.Logger.Errorf(err.Error()) + } + return e.possibleToBuy(id) + } + // TODO сравнение дробных чисел if moneyInFloat < required { e.client.Logger.Infof("executor: not enough money to buy order") From fc5e9a484dd1d1a200fe051712bada1e34cd36d5 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 15:15:01 +0300 Subject: [PATCH 38/57] add start/stop for executor & lp listen in executor too --- examples/ob_bot/internal/bot/executor.go | 176 +++++++++++++++++------ 1 file changed, 132 insertions(+), 44 deletions(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 2d7eec3..8e5f678 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -21,6 +21,7 @@ type Instrument struct { entryPrice float64 } +// Positions - Данные о позициях счета type Positions struct { mx sync.Mutex pd *pb.PositionData @@ -30,12 +31,14 @@ func NewPositions() *Positions { return &Positions{pd: &pb.PositionData{}} } +// Update - Обновление позиций func (p *Positions) Update(data *pb.PositionData) { p.mx.Lock() p.pd = data p.mx.Unlock() } +// Get - получение позиций func (p *Positions) Get() *pb.PositionData { p.mx.Lock() defer p.mx.Unlock() @@ -53,6 +56,9 @@ type Executor struct { lastPrices map[string]float64 positions *Positions + wg *sync.WaitGroup + cancel context.CancelFunc + client *investgo.Client ordersService *investgo.OrdersServiceClient operationsService *investgo.OperationsServiceClient @@ -60,26 +66,146 @@ type Executor struct { // NewExecutor - Создание экземпляра исполнителя func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrument, minProfit float64) *Executor { + ctxExecutor, cancel := context.WithCancel(ctx) + wg := &sync.WaitGroup{} e := &Executor{ instruments: ids, + minProfit: minProfit, lastPrices: make(map[string]float64, len(ids)), + positions: NewPositions(), + wg: wg, + cancel: cancel, client: c, ordersService: c.NewOrdersServiceClient(), operationsService: c.NewOperationsServiceClient(), - minProfit: minProfit, - positions: NewPositions(), } + e.start(ctxExecutor) + return e +} + +// Stop - Завершение работы +func (e *Executor) Stop() { + e.cancel() + e.wg.Wait() + e.client.Logger.Infof("executor stopped") +} +func (e *Executor) start(ctx context.Context) { + e.wg.Add(1) go func(ctx context.Context) { - err := e.updatePositions(ctx) + defer e.wg.Done() + err := e.listenPositions(ctx) if err != nil { e.client.Logger.Errorf(err.Error()) } }(ctx) - return e + e.wg.Add(1) + go func(ctx context.Context) { + defer e.wg.Done() + err := e.listenLastPrices(ctx) + if err != nil { + e.client.Logger.Errorf(err.Error()) + } + }(ctx) +} + +// listenPositions - Метод слушает стрим позиций и обновляет их +func (e *Executor) listenPositions(ctx context.Context) error { + err := e.updatePositionsUnary() + if err != nil { + return err + } + operationsStreamService := e.client.NewOperationsStreamClient() + stream, err := operationsStreamService.PositionsStream([]string{e.client.Config.AccountId}) + if err != nil { + return err + } + positionsChan := stream.Positions() + + e.wg.Add(1) + go func() { + defer e.wg.Done() + err := stream.Listen() + if err != nil { + e.client.Logger.Errorf(err.Error()) + } + }() + + e.wg.Add(1) + go func(ctx context.Context) { + defer e.wg.Done() + for { + select { + case <-ctx.Done(): + return + case p, ok := <-positionsChan: + if !ok { + return + } + e.client.Logger.Infof("update from positions stream %v\n", p.GetMoney()) + e.positions.Update(p) + } + } + }(ctx) + + <-ctx.Done() + e.client.Logger.Infof("stop updating positions in executor") + stream.Stop() + return nil +} + +func (e *Executor) listenLastPrices(ctx context.Context) error { + MarketDataStreamService := e.client.NewMarketDataStreamClient() + stream, err := MarketDataStreamService.MarketDataStream() + if err != nil { + return err + } + + ids := make([]string, 0, len(e.instruments)) + for id := range e.instruments { + ids = append(ids, id) + } + lastPricesChan, err := stream.SubscribeLastPrice(ids) + if err != nil { + return err + } + + e.wg.Add(1) + go func() { + defer e.wg.Done() + err := stream.Listen() + if err != nil { + e.client.Logger.Errorf(err.Error()) + } + }() + + // чтение из стрима + e.wg.Add(1) + go func(ctx context.Context) { + defer e.wg.Done() + for { + select { + case <-ctx.Done(): + return + case _, ok := <-lastPricesChan: + if !ok { + return + } + // обновление данных в мапе последних цен + // b.executor.lastPrices[lp.GetInstrumentUid()] = lp.GetPrice().ToFloat() + // update map + } + } + }(ctx) + + <-ctx.Done() + e.client.Logger.Infof("stop updating last prices in executor") + stream.Stop() + return nil } +// updatePositionsUnary - Unary метод обновления позиций func (e *Executor) updatePositionsUnary() error { resp, err := e.operationsService.GetPositions(e.client.Config.AccountId) if err != nil { @@ -126,46 +252,6 @@ func (e *Executor) updatePositionsUnary() error { return nil } -func (e *Executor) updatePositions(ctx context.Context) error { - err := e.updatePositionsUnary() - if err != nil { - return err - } - operationsStreamService := e.client.NewOperationsStreamClient() - stream, err := operationsStreamService.PositionsStream([]string{e.client.Config.AccountId}) - if err != nil { - return err - } - positionsChan := stream.Positions() - - go func() { - err := stream.Listen() - if err != nil { - e.client.Logger.Errorf(err.Error()) - } - }() - - go func(ctx context.Context) { - for { - select { - case <-ctx.Done(): - return - case p, ok := <-positionsChan: - if !ok { - return - } - e.client.Logger.Infof("update from positions stream %v\n", p.GetMoney()) - e.positions.Update(p) - } - } - }(ctx) - - <-ctx.Done() - e.client.Logger.Infof("stop updating positions in executor") - stream.Stop() - return nil -} - // Buy - Метод покупки инструмента с идентификатором id func (e *Executor) Buy(id string) error { currentInstrument := e.instruments[id] @@ -229,10 +315,12 @@ func (e *Executor) Sell(id string) (float64, error) { return profit, nil } +// isProfitable - Верно если процент выгоды возможной сделки, рассчитанный по цене последней сделки, больше чем minProfit func (e *Executor) isProfitable(id string) bool { return ((e.lastPrices[id]-e.instruments[id].entryPrice)/e.instruments[id].entryPrice)*100 > e.minProfit } +// possibleToBuy - Проверка возможности купить инструмент func (e *Executor) possibleToBuy(id string) bool { // требуемая сумма для покупки // кол-во лотов * лотность * стоимость 1 инструмента From 4f0a862dc5af8091d042b88bf0d642a8d3c7a7d2 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 16:20:06 +0300 Subject: [PATCH 39/57] add last price listening in executor --- examples/ob_bot/internal/bot/executor.go | 53 +++++++++++++++++++----- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 8e5f678..e0a9a9e 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -21,6 +21,30 @@ type Instrument struct { entryPrice float64 } +type LastPrices struct { + mx sync.Mutex + lp map[string]float64 +} + +func NewLastPrices() *LastPrices { + return &LastPrices{ + lp: make(map[string]float64, 0), + } +} + +func (l *LastPrices) Update(id string, price float64) { + l.mx.Lock() + l.lp[id] = price + l.mx.Unlock() +} + +func (l *LastPrices) Get(id string) (float64, bool) { + l.mx.Lock() + defer l.mx.Unlock() + p, ok := l.lp[id] + return p, ok +} + // Positions - Данные о позициях счета type Positions struct { mx sync.Mutex @@ -28,7 +52,9 @@ type Positions struct { } func NewPositions() *Positions { - return &Positions{pd: &pb.PositionData{}} + return &Positions{ + pd: &pb.PositionData{}, + } } // Update - Обновление позиций @@ -52,8 +78,7 @@ type Executor struct { // minProfit - Процент минимального профита, после которого выставляются рыночные заявки minProfit float64 - // lastPrices - Мапа последних цен по инструментам, бот в нее пишет, исполнитель читает - lastPrices map[string]float64 + lastPrices *LastPrices positions *Positions wg *sync.WaitGroup @@ -68,10 +93,11 @@ type Executor struct { func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrument, minProfit float64) *Executor { ctxExecutor, cancel := context.WithCancel(ctx) wg := &sync.WaitGroup{} + e := &Executor{ instruments: ids, minProfit: minProfit, - lastPrices: make(map[string]float64, len(ids)), + lastPrices: NewLastPrices(), positions: NewPositions(), wg: wg, cancel: cancel, @@ -188,13 +214,11 @@ func (e *Executor) listenLastPrices(ctx context.Context) error { select { case <-ctx.Done(): return - case _, ok := <-lastPricesChan: + case lp, ok := <-lastPricesChan: if !ok { return } - // обновление данных в мапе последних цен - // b.executor.lastPrices[lp.GetInstrumentUid()] = lp.GetPrice().ToFloat() - // update map + e.lastPrices.Update(lp.GetInstrumentUid(), lp.GetPrice().ToFloat()) } } }(ctx) @@ -317,14 +341,23 @@ func (e *Executor) Sell(id string) (float64, error) { // isProfitable - Верно если процент выгоды возможной сделки, рассчитанный по цене последней сделки, больше чем minProfit func (e *Executor) isProfitable(id string) bool { - return ((e.lastPrices[id]-e.instruments[id].entryPrice)/e.instruments[id].entryPrice)*100 > e.minProfit + lp, ok := e.lastPrices.Get(id) + if !ok { + return false + } + return ((lp-e.instruments[id].entryPrice)/e.instruments[id].entryPrice)*100 > e.minProfit } // possibleToBuy - Проверка возможности купить инструмент func (e *Executor) possibleToBuy(id string) bool { // требуемая сумма для покупки // кол-во лотов * лотность * стоимость 1 инструмента - required := float64(e.instruments[id].quantity) * float64(e.instruments[id].lot) * e.lastPrices[id] + //return true + lp, ok := e.lastPrices.Get(id) + if !ok { + return false + } + required := float64(e.instruments[id].quantity) * float64(e.instruments[id].lot) * lp positionMoney := e.positions.Get().GetMoney() var moneyInFloat float64 for _, pm := range positionMoney { From 7fc63d029dff375e201799f77ac91952cd6d0378 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 16:21:55 +0300 Subject: [PATCH 40/57] delete last price listening from bot & move instruments construct in NewBot --- examples/ob_bot/internal/bot/bot.go | 52 +++++++++++------------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index fd880d2..1449fc0 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -52,7 +52,25 @@ func NewBot(ctx context.Context, c *investgo.Client, dd time.Time, config OrderB botCtx, cancelBot = context.WithDeadline(botCtx, dd.Add(-config.SellOutAhead)) } + // по конфигу стратегии заполняем map для executor + instrumentService := c.NewInstrumentsServiceClient() instruments := make(map[string]Instrument, len(config.Instruments)) + + for _, instrument := range config.Instruments { + // в данном случае ключ это uid, поэтому используем LotByUid() + resp, err := instrumentService.InstrumentByUid(instrument) + if err != nil { + cancelBot() + return nil, err + } + instruments[instrument] = Instrument{ + quantity: QUANTITY, + inStock: false, + entryPrice: 0, + lot: resp.GetInstrument().GetLot(), + currency: resp.GetInstrument().GetCurrency(), + } + } executor := NewExecutor(ctx, c, instruments, config.MinProfit) return &Bot{ @@ -73,27 +91,6 @@ func (b *Bot) Run() error { b.Client.Logger.Fatalf(err.Error()) } - // по конфигу стратегии заполняем map для executor - instrumentService := b.Client.NewInstrumentsServiceClient() - instruments := make(map[string]Instrument, len(b.StrategyConfig.Instruments)) - - for _, instrument := range b.StrategyConfig.Instruments { - // в данном случае ключ это uid, поэтому используем LotByUid() - resp, err := instrumentService.InstrumentByUid(instrument) - if err != nil { - return err - } - instruments[instrument] = Instrument{ - quantity: QUANTITY, - inStock: false, - entryPrice: 0, - lot: resp.GetInstrument().GetLot(), - currency: resp.GetInstrument().GetCurrency(), - } - } - - b.executor.instruments = instruments - // инфраструктура для работы стратегии: запрос, получение, преобразование рыночных данных MarketDataStreamService := b.Client.NewMarketDataStreamClient() stream, err := MarketDataStreamService.MarketDataStream() @@ -105,11 +102,6 @@ func (b *Bot) Run() error { return err } - lastPricesChan, err := stream.SubscribeLastPrice(b.StrategyConfig.Instruments) - if err != nil { - return err - } - wg.Add(1) go func() { defer wg.Done() @@ -137,12 +129,6 @@ func (b *Bot) Run() error { return } orderBooks <- transformOrderBook(ob) - case lp, ok := <-lastPricesChan: - if !ok { - return - } - // обновление данных в мапе последних цен - b.executor.lastPrices[lp.GetInstrumentUid()] = lp.GetPrice().ToFloat() } } }(b.ctx) @@ -173,6 +159,8 @@ func (b *Bot) Run() error { } } + b.executor.Stop() + wg.Wait() return nil } From e1e611dd1d678b949142d67ea7d1be545951c607 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 16:59:15 +0300 Subject: [PATCH 41/57] add comments --- examples/ob_bot/internal/bot/bot.go | 12 +++++------- examples/ob_bot/internal/bot/executor.go | 10 +++++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 1449fc0..dc31bf2 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -44,7 +44,6 @@ type Bot struct { // NewBot - Создание экземпляра бота на стакане // dd - дедлайн работы бота для интрадей торговли -// каждый бот создает своего клиента для работы с investAPI func NewBot(ctx context.Context, c *investgo.Client, dd time.Time, config OrderBookStrategyConfig) (*Bot, error) { botCtx, cancelBot := context.WithDeadline(ctx, dd) // если нужно выходить из позиций, то бот будет завершать свою работу раньше чем дедлайн @@ -71,14 +70,12 @@ func NewBot(ctx context.Context, c *investgo.Client, dd time.Time, config OrderB currency: resp.GetInstrument().GetCurrency(), } } - executor := NewExecutor(ctx, c, instruments, config.MinProfit) - return &Bot{ Client: c, StrategyConfig: config, ctx: botCtx, cancelBot: cancelBot, - executor: executor, + executor: NewExecutor(ctx, c, instruments, config.MinProfit), }, nil } @@ -117,9 +114,7 @@ func (b *Bot) Run() error { // чтение из стрима wg.Add(1) go func(ctx context.Context) { - defer func() { - wg.Done() - }() + defer wg.Done() for { select { case <-ctx.Done(): @@ -151,6 +146,7 @@ func (b *Bot) Run() error { // стримы работают на контексте клиента, завершать их нужно явно stream.Stop() + // если нужно, то в конце торговой сессии выходим из всех, открытых ботом, позиций if b.StrategyConfig.SellOut { b.Client.Logger.Infof("start positions sell out...") err := b.executor.SellOut() @@ -159,6 +155,7 @@ func (b *Bot) Run() error { } } + // так как исполнитель тоже слушает стримы, его нужно явно остановить b.executor.Stop() wg.Wait() @@ -208,6 +205,7 @@ func (b *Bot) checkRatio(ob OrderBook) float64 { return float64(buy) / float64(sell) } +// ordersCount - возвращает кол-во заявок из слайса ордеров func ordersCount(o []Order) int64 { var count int64 for _, order := range o { diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index e0a9a9e..8de3003 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -21,6 +21,7 @@ type Instrument struct { entryPrice float64 } +// LastPrices - Последние цены инструментов type LastPrices struct { mx sync.Mutex lp map[string]float64 @@ -32,12 +33,14 @@ func NewLastPrices() *LastPrices { } } +// Update - обновление последних цен func (l *LastPrices) Update(id string, price float64) { l.mx.Lock() l.lp[id] = price l.mx.Unlock() } +// Get - получение последней цены func (l *LastPrices) Get(id string) (float64, bool) { l.mx.Lock() defer l.mx.Unlock() @@ -78,8 +81,10 @@ type Executor struct { // minProfit - Процент минимального профита, после которого выставляются рыночные заявки minProfit float64 + // lastPrices - Последние цены по инструментам, обновляются через стрим маркетдаты lastPrices *LastPrices - positions *Positions + // lastPrices - Текущие позиции на счете, обновляются через стрим сервиса операций + positions *Positions wg *sync.WaitGroup cancel context.CancelFunc @@ -105,6 +110,7 @@ func NewExecutor(ctx context.Context, c *investgo.Client, ids map[string]Instrum ordersService: c.NewOrdersServiceClient(), operationsService: c.NewOperationsServiceClient(), } + // Сразу запускаем исполнителя из его же конструктора e.start(ctxExecutor) return e } @@ -116,6 +122,7 @@ func (e *Executor) Stop() { e.client.Logger.Infof("executor stopped") } +// start - Запуск чтения стримов позиций и последних цен func (e *Executor) start(ctx context.Context) { e.wg.Add(1) go func(ctx context.Context) { @@ -181,6 +188,7 @@ func (e *Executor) listenPositions(ctx context.Context) error { return nil } +// listenLastPrices - Метод слушает стрим последних цен и обновляет их func (e *Executor) listenLastPrices(ctx context.Context) error { MarketDataStreamService := e.client.NewMarketDataStreamClient() stream, err := MarketDataStreamService.MarketDataStream() From e99f2eff60d576e81e95185a5aaa5ff84e42c419 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 17:24:53 +0300 Subject: [PATCH 42/57] update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 591d46a..9770ea3 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ SDK предназначен для упрощения работы с API Ти Для непосредственного взаимодействия с INVEST API нужно создать клиента. Примеры использования SDK находятся в директории examples: - * `md_stream.go`, orders_stream.go, operations_stream.go - примеры работы со стримами + * `md_stream.go`, `orders_stream.go`, `operations_stream.go` - примеры работы со стримами * `instruments.go` - примеры работы с сервисом инструментов * `marketdata.go` - примеры работы с сервисом котировок * `operations.go` - примеры работы с сервисом операций From c0938489ab83aaea626c54820023bb980b014524 Mon Sep 17 00:00:00 2001 From: jstalex Date: Mon, 26 Jun 2023 17:29:14 +0300 Subject: [PATCH 43/57] minor refactor --- examples/ob_bot/internal/bot/bot.go | 2 +- examples/ob_bot/internal/bot/executor.go | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index dc31bf2..4f3315e 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -226,7 +226,7 @@ func (b *Bot) checkMoneyBalance(currency string, required float64) error { money := resp.GetMoney() for _, m := range money { b.Client.Logger.Infof("money balance = %v %v", m.ToFloat(), m.GetCurrency()) - if strings.ToLower(m.GetCurrency()) == strings.ToLower(currency) { + if strings.EqualFold(m.GetCurrency(), currency) { balance = m.ToFloat() } } diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 8de3003..2b8c8c6 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -392,10 +392,6 @@ func (e *Executor) possibleToBuy(id string) bool { return moneyInFloat > required } -func (e *Executor) possibleToSell() { - -} - // SellOut - Метод выхода из всех текущих позиций func (e *Executor) SellOut() error { // TODO for futures and options From 4866e626e6d357f1e864987945e5533dc2677274 Mon Sep 17 00:00:00 2001 From: jstalex Date: Tue, 27 Jun 2023 11:30:53 +0300 Subject: [PATCH 44/57] add docs --- examples/README.md | 14 ++++++++++++++ investgo/doc.go | 13 +++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 examples/README.md create mode 100644 investgo/doc.go diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..de2bbd0 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,14 @@ +## Examples + +В примерах рассмотрены основные сценарии использования investAPI через пакет investgo. + +* `md_stream.go`, `orders_stream.go`, `operations_stream.go` - примеры работы со стримами +* `instruments.go` - примеры работы с сервисом инструментов +* `marketdata.go` - примеры работы с сервисом котировок +* `operations.go` - примеры работы с сервисом операций +* `orders.go` - примеры работы с сервисом торговых поручений +* `stop_orders` - примеры работы с сервисом стоп-заявок +* `users.go` - примеры работы с сервисом счетов +* `sandbox.go` - пример работы с песочницей +* `order_book_download/order_book.go` - пример сохранения стаканов из стрима маркетдаты в sqlite или json +* `ob_bot` - пример простейшего бота на стакане \ No newline at end of file diff --git a/investgo/doc.go b/investgo/doc.go new file mode 100644 index 0000000..9553fa3 --- /dev/null +++ b/investgo/doc.go @@ -0,0 +1,13 @@ +/* +Package investgo предоставляет инструменты для работы с Tinkoff InvestAPI. + +# Client + +Сначала нужно заполнить investgo.Config, затем с помощью функции investgo.NewClient() создать клиента. У каждого клиента +есть свой конфиг, который привязывает его к определенному счету и токену. Если есть потребность использовать разные счета и токены, нужно +создавать разных клиентов. investgo.Client предоставляет функции-конcтрукторы для всех сервисов Tinkoff InvestAPI. + +Подробнее смотрите в директории examples. +*/ + +package investgo From 437f75b6ffe6d926a8d828a5080049e4ed3f2fc1 Mon Sep 17 00:00:00 2001 From: jstalex Date: Tue, 27 Jun 2023 11:53:45 +0300 Subject: [PATCH 45/57] update README.md --- README.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 9770ea3..e925882 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,7 @@ SDK предназначен для упрощения работы с API Ти ### Быстрый старт Для непосредственного взаимодействия с INVEST API нужно создать клиента. -Примеры использования SDK находятся в директории examples: - * `md_stream.go`, `orders_stream.go`, `operations_stream.go` - примеры работы со стримами - * `instruments.go` - примеры работы с сервисом инструментов - * `marketdata.go` - примеры работы с сервисом котировок - * `operations.go` - примеры работы с сервисом операций - * `orders.go` - примеры работы с сервисом торговых поручений - * `stop_orders` - примеры работы с сервисом стоп-заявок - * `users.go` - примеры работы с сервисом счетов - * `sandbox.go` - пример работы с песочницей - * `order_book_download/order_book.go` - пример сохранения стаканов из стрима маркетдаты в sqlite или json +Примеры использования SDK находятся в директории examples. ### Запуск примеров @@ -78,7 +69,7 @@ Token string `yaml:"APIToken"` // AppName - Название вашего приложения, по умолчанию = tinkoff-api-go-sdk AppName string `yaml:"AppName"` // AccountId - Если уже есть аккаунт для апи можно указать напрямую, -// для песочницы создастся и запишется автоматически +// по умолчанию откроется новый счет в песочнице AccountId string `yaml:"AccountId"` // DisableResourceExhaustedRetry - Если true, то сдк не пытается ретраить, после получения ошибки об исчерпывании // лимита запросов, если false, то сдк ждет нужное время и пытается выполнить запрос снова. По умолчанию = false @@ -111,6 +102,10 @@ MaxRetries uint `yaml:"MaxRetries"` ретраер ждет нужное время и продолжает выполнение, *при этом никакого сообщения об ошибке для клиента нет*. #### Пример использования MarketDataStreamService + +
+ Пример использования MarketDataStreamService + ```go package main @@ -210,6 +205,9 @@ func main() { } ``` + +
+ ### У меня есть вопрос [Основной репозиторий с документацией](https://github.com/Tinkoff/investAPI/) — в нем вы можете задать вопрос в Issues и получать информацию о релизах в Releases. From ef7abb724fdc574623ac03feee1f22373f0051c3 Mon Sep 17 00:00:00 2001 From: jstalex Date: Tue, 27 Jun 2023 11:55:22 +0300 Subject: [PATCH 46/57] update README.md --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index e925882..186483c 100644 --- a/README.md +++ b/README.md @@ -101,10 +101,8 @@ MaxRetries uint `yaml:"MaxRetries"` отключить ретраер для ошибки `ResourceExhausted`, по умолчанию он включен и в случае превышения лимитов Unary - запросов, ретраер ждет нужное время и продолжает выполнение, *при этом никакого сообщения об ошибке для клиента нет*. -#### Пример использования MarketDataStreamService -
- Пример использования MarketDataStreamService + Пример использования MarketDataStreamService ```go package main From 6092757fc74aceabbc48644e2c0da474e2e14e0c Mon Sep 17 00:00:00 2001 From: jstalex Date: Tue, 27 Jun 2023 19:18:28 +0300 Subject: [PATCH 47/57] add timer.go --- investgo/timer.go | 153 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 investgo/timer.go diff --git a/investgo/timer.go b/investgo/timer.go new file mode 100644 index 0000000..31e1033 --- /dev/null +++ b/investgo/timer.go @@ -0,0 +1,153 @@ +package investgo + +import ( + "context" + "errors" + pb "github.com/tinkoff/invest-api-go-sdk/proto" + "strings" + "time" +) + +// Event - события, START - сигнал к запуску, STOP - сигнал к остановке +type Event int + +const ( + START Event = iota + STOP +) + +type Timer struct { + client *Client + instrumentsService *InstrumentsServiceClient + exchange string + lastEvent Event + cancel context.CancelFunc + ticker chan struct{} + events chan Event +} + +// NewTimer - Таймер сигнализирует о начале/завершении основной торговой сессии на конкретной бирже +func NewTimer(c *Client, exchange string) *Timer { + return &Timer{ + client: c, + instrumentsService: c.NewInstrumentsServiceClient(), + exchange: exchange, + ticker: make(chan struct{}, 1), + events: make(chan Event, 1), + } +} + +// Events - Канал событий об открытии/закрытии торгов +func (t *Timer) Events() chan Event { + return t.events +} + +// Start - Запуск таймера +func (t *Timer) Start(ctx context.Context) error { + defer t.shutdown() + ctxTimer, cancel := context.WithCancel(ctx) + t.cancel = cancel + // + t.Wait(ctxTimer, 0) + for { + select { + case <-ctxTimer.Done(): + return nil + case <-t.ticker: + // получаем текущее время + from := time.Now() + to := from.Add(time.Hour * 24) + + // получаем ближайшие два торговых дня + resp, err := t.instrumentsService.TradingSchedules(t.exchange, from, to) + if err != nil { + return err + } + + exchanges := resp.GetExchanges() + days := make([]*pb.TradingDay, 0) + for _, ex := range exchanges { + if strings.EqualFold(ex.GetExchange(), t.exchange) { + days = ex.GetDays() + } + } + + var today *pb.TradingDay + if len(days) > 1 { + today = days[0] + } + + // если этот день оказался неторговым, то находим ближайший торговый и ждем до старта торгов в этот день + if !today.GetIsTradingDay() { + today, err = t.findTradingDay(from) + if err != nil { + return err + } + + } + switch { + // если торги еще не начались + case time.Now().Before(today.GetStartTime().AsTime()): + t.Wait(ctxTimer, time.Until(today.GetStartTime().AsTime().Local())) + t.events <- START + t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())) + t.events <- STOP + // если сегодня торги уже идут + case time.Now().After(today.GetStartTime().AsTime()) && time.Now().Before(today.GetEndTime().AsTime().Local()): + t.client.Logger.Infof("now is trading time, sleep for %v", today.GetEndTime().AsTime().Local().String()) + t.events <- START + t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())) + t.events <- STOP + // если на сегодня торги уже окончены + case time.Now().After(today.GetEndTime().AsTime().Local()): + // спать час, пока не дождемся следующего дня + t.client.Logger.Infof("%v closed, wait next day", t.exchange) + t.Wait(ctxTimer, time.Hour) + } + } + } +} + +// Stop - Завершение работы таймера +func (t *Timer) Stop() { + t.cancel() +} + +func (t *Timer) shutdown() { + t.client.Logger.Infof("stop %v timer", t.exchange) + close(t.events) + close(t.ticker) +} + +func (t *Timer) Wait(ctx context.Context, dur time.Duration) { + tim := time.NewTimer(dur) + for { + select { + case <-ctx.Done(): + return + case <-tim.C: + // t.ticker <- struct{}{} + return + } + } +} + +// findTradingDay - Поиск ближайшего торгового дня +func (t *Timer) findTradingDay(start time.Time) (*pb.TradingDay, error) { + resp, err := t.instrumentsService.TradingSchedules(t.exchange, start, start.Add(time.Hour*24*7)) + if err != nil { + return nil, err + } + for _, ex := range resp.GetExchanges() { + if strings.EqualFold(ex.GetExchange(), t.exchange) { + for _, day := range ex.GetDays() { + if day.GetIsTradingDay() { + return day, nil + } + } + // если не нашлось дня, запросим еще на неделю расписание + return t.findTradingDay(start.Add(time.Hour * 24 * 7)) + } + } + return nil, errors.New("trading day not found") +} From 53da7ae16f329844933115c8c5d5f10c2dad4352 Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 10:18:22 +0300 Subject: [PATCH 48/57] fix position stream --- examples/ob_bot/internal/bot/bot.go | 2 +- examples/ob_bot/internal/bot/executor.go | 16 +++------------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 4f3315e..a1a638e 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -136,7 +136,7 @@ func (b *Bot) Run() error { if err != nil { b.Client.Logger.Errorf(err.Error()) } - b.Client.Logger.Infof("profit by strategy = %v", profit) + b.Client.Logger.Infof("profit by strategy = %.9f", profit) }(b.ctx) // Завершение работы бота по его контексту: вызов Stop() или отмена по дедлайну diff --git a/examples/ob_bot/internal/bot/executor.go b/examples/ob_bot/internal/bot/executor.go index 2b8c8c6..7cf62c0 100644 --- a/examples/ob_bot/internal/bot/executor.go +++ b/examples/ob_bot/internal/bot/executor.go @@ -176,7 +176,7 @@ func (e *Executor) listenPositions(ctx context.Context) error { if !ok { return } - e.client.Logger.Infof("update from positions stream %v\n", p.GetMoney()) + // e.client.Logger.Infof("update from positions stream %v\n", p.GetMoney()) e.positions.Update(p) } } @@ -375,19 +375,9 @@ func (e *Executor) possibleToBuy(id string) bool { } } - // TODO убрать, когда починят стрим - if moneyInFloat < 0 { - e.client.Logger.Infof("balance < 0, update positions by unary call") - err := e.updatePositionsUnary() - if err != nil { - e.client.Logger.Errorf(err.Error()) - } - return e.possibleToBuy(id) - } - - // TODO сравнение дробных чисел + // TODO сравнение дробных чисел, реакция на недостаток баланса if moneyInFloat < required { - e.client.Logger.Infof("executor: not enough money to buy order") + e.client.Logger.Infof("executor: not enough money to buy order with id = %v", id) } return moneyInFloat > required } From 7c18e403e6308e6a9d8b49efbbf5384fb0d61cac Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 10:59:15 +0300 Subject: [PATCH 49/57] add logs and delete ticker --- investgo/timer.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/investgo/timer.go b/investgo/timer.go index 31e1033..681e82f 100644 --- a/investgo/timer.go +++ b/investgo/timer.go @@ -22,7 +22,6 @@ type Timer struct { exchange string lastEvent Event cancel context.CancelFunc - ticker chan struct{} events chan Event } @@ -32,7 +31,6 @@ func NewTimer(c *Client, exchange string) *Timer { client: c, instrumentsService: c.NewInstrumentsServiceClient(), exchange: exchange, - ticker: make(chan struct{}, 1), events: make(chan Event, 1), } } @@ -53,7 +51,7 @@ func (t *Timer) Start(ctx context.Context) error { select { case <-ctxTimer.Done(): return nil - case <-t.ticker: + default: // получаем текущее время from := time.Now() to := from.Add(time.Hour * 24) @@ -88,20 +86,22 @@ func (t *Timer) Start(ctx context.Context) error { switch { // если торги еще не начались case time.Now().Before(today.GetStartTime().AsTime()): + t.client.Logger.Infof("%v is closed yet, wait for start %v", t.exchange, time.Until(today.GetStartTime().AsTime().Local())) t.Wait(ctxTimer, time.Until(today.GetStartTime().AsTime().Local())) t.events <- START + t.client.Logger.Infof("start trading session, remaining time = %v", time.Until(today.GetEndTime().AsTime().Local())) t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())) t.events <- STOP // если сегодня торги уже идут case time.Now().After(today.GetStartTime().AsTime()) && time.Now().Before(today.GetEndTime().AsTime().Local()): - t.client.Logger.Infof("now is trading time, sleep for %v", today.GetEndTime().AsTime().Local().String()) + t.client.Logger.Infof("start trading session, remaining time = %v", time.Until(today.GetEndTime().AsTime().Local())) t.events <- START t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())) t.events <- STOP // если на сегодня торги уже окончены case time.Now().After(today.GetEndTime().AsTime().Local()): // спать час, пока не дождемся следующего дня - t.client.Logger.Infof("%v closed, wait next day", t.exchange) + t.client.Logger.Infof("%v is already closed, wait next day for 1 hour", t.exchange) t.Wait(ctxTimer, time.Hour) } } @@ -116,7 +116,6 @@ func (t *Timer) Stop() { func (t *Timer) shutdown() { t.client.Logger.Infof("stop %v timer", t.exchange) close(t.events) - close(t.ticker) } func (t *Timer) Wait(ctx context.Context, dur time.Duration) { @@ -126,7 +125,6 @@ func (t *Timer) Wait(ctx context.Context, dur time.Duration) { case <-ctx.Done(): return case <-tim.C: - // t.ticker <- struct{}{} return } } From af968a679516a31f0a0e4b2243e1cb636d94a8a3 Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 11:09:05 +0300 Subject: [PATCH 50/57] add stop flag --- investgo/timer.go | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/investgo/timer.go b/investgo/timer.go index 681e82f..eb904e8 100644 --- a/investgo/timer.go +++ b/investgo/timer.go @@ -83,26 +83,35 @@ func (t *Timer) Start(ctx context.Context) error { } } + switch { // если торги еще не начались case time.Now().Before(today.GetStartTime().AsTime()): t.client.Logger.Infof("%v is closed yet, wait for start %v", t.exchange, time.Until(today.GetStartTime().AsTime().Local())) - t.Wait(ctxTimer, time.Until(today.GetStartTime().AsTime().Local())) + if stop := t.Wait(ctxTimer, time.Until(today.GetStartTime().AsTime().Local())); stop { + return nil + } t.events <- START t.client.Logger.Infof("start trading session, remaining time = %v", time.Until(today.GetEndTime().AsTime().Local())) - t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())) + if stop := t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())); stop { + return nil + } t.events <- STOP // если сегодня торги уже идут case time.Now().After(today.GetStartTime().AsTime()) && time.Now().Before(today.GetEndTime().AsTime().Local()): t.client.Logger.Infof("start trading session, remaining time = %v", time.Until(today.GetEndTime().AsTime().Local())) t.events <- START - t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())) + if stop := t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())); stop { + return nil + } t.events <- STOP // если на сегодня торги уже окончены case time.Now().After(today.GetEndTime().AsTime().Local()): // спать час, пока не дождемся следующего дня t.client.Logger.Infof("%v is already closed, wait next day for 1 hour", t.exchange) - t.Wait(ctxTimer, time.Hour) + if stop := t.Wait(ctxTimer, time.Hour); stop { + return nil + } } } } @@ -118,14 +127,14 @@ func (t *Timer) shutdown() { close(t.events) } -func (t *Timer) Wait(ctx context.Context, dur time.Duration) { +func (t *Timer) Wait(ctx context.Context, dur time.Duration) bool { tim := time.NewTimer(dur) for { select { case <-ctx.Done(): - return + return true case <-tim.C: - return + return false } } } From 90d1e6d5cead406f3bdda1015aa8de6459b1c97f Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 12:48:14 +0300 Subject: [PATCH 51/57] add cancel ahead --- investgo/timer.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/investgo/timer.go b/investgo/timer.go index eb904e8..6ab07a0 100644 --- a/investgo/timer.go +++ b/investgo/timer.go @@ -20,17 +20,19 @@ type Timer struct { client *Client instrumentsService *InstrumentsServiceClient exchange string - lastEvent Event - cancel context.CancelFunc - events chan Event + // cancelAhead - Событие STOP будет отправлено в канал за cancelAhead до конца торгов + cancelAhead time.Duration + cancel context.CancelFunc + events chan Event } // NewTimer - Таймер сигнализирует о начале/завершении основной торговой сессии на конкретной бирже -func NewTimer(c *Client, exchange string) *Timer { +func NewTimer(c *Client, exchange string, cancelAhead time.Duration) *Timer { return &Timer{ client: c, instrumentsService: c.NewInstrumentsServiceClient(), exchange: exchange, + cancelAhead: cancelAhead, events: make(chan Event, 1), } } @@ -45,8 +47,6 @@ func (t *Timer) Start(ctx context.Context) error { defer t.shutdown() ctxTimer, cancel := context.WithCancel(ctx) t.cancel = cancel - // - t.Wait(ctxTimer, 0) for { select { case <-ctxTimer.Done(): @@ -88,12 +88,12 @@ func (t *Timer) Start(ctx context.Context) error { // если торги еще не начались case time.Now().Before(today.GetStartTime().AsTime()): t.client.Logger.Infof("%v is closed yet, wait for start %v", t.exchange, time.Until(today.GetStartTime().AsTime().Local())) - if stop := t.Wait(ctxTimer, time.Until(today.GetStartTime().AsTime().Local())); stop { + if stop := t.wait(ctxTimer, time.Until(today.GetStartTime().AsTime().Local())); stop { return nil } t.events <- START t.client.Logger.Infof("start trading session, remaining time = %v", time.Until(today.GetEndTime().AsTime().Local())) - if stop := t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())); stop { + if stop := t.wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())-t.cancelAhead); stop { return nil } t.events <- STOP @@ -101,7 +101,7 @@ func (t *Timer) Start(ctx context.Context) error { case time.Now().After(today.GetStartTime().AsTime()) && time.Now().Before(today.GetEndTime().AsTime().Local()): t.client.Logger.Infof("start trading session, remaining time = %v", time.Until(today.GetEndTime().AsTime().Local())) t.events <- START - if stop := t.Wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())); stop { + if stop := t.wait(ctxTimer, time.Until(today.GetEndTime().AsTime().Local())-t.cancelAhead); stop { return nil } t.events <- STOP @@ -109,7 +109,7 @@ func (t *Timer) Start(ctx context.Context) error { case time.Now().After(today.GetEndTime().AsTime().Local()): // спать час, пока не дождемся следующего дня t.client.Logger.Infof("%v is already closed, wait next day for 1 hour", t.exchange) - if stop := t.Wait(ctxTimer, time.Hour); stop { + if stop := t.wait(ctxTimer, time.Hour); stop { return nil } } @@ -119,6 +119,7 @@ func (t *Timer) Start(ctx context.Context) error { // Stop - Завершение работы таймера func (t *Timer) Stop() { + t.events <- STOP t.cancel() } @@ -127,7 +128,8 @@ func (t *Timer) shutdown() { close(t.events) } -func (t *Timer) Wait(ctx context.Context, dur time.Duration) bool { +// wait - Ожидание, с возможностью отмены по контексту +func (t *Timer) wait(ctx context.Context, dur time.Duration) bool { tim := time.NewTimer(dur) for { select { From 1e8f37e4d5031a85e1d81e50902c940f47a242e7 Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 12:49:20 +0300 Subject: [PATCH 52/57] delete sell out ahead from bot --- examples/ob_bot/internal/bot/bot.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 4f3315e..d5598ac 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -7,7 +7,6 @@ import ( pb "github.com/tinkoff/invest-api-go-sdk/proto" "strings" "sync" - "time" ) // QUANTITY - Кол-во лотов инструментов, которыми торгует бот @@ -27,9 +26,9 @@ type OrderBookStrategyConfig struct { MinProfit float64 // SellOut - Если true, то по достижению дедлайна бот выходит из всех активных позиций SellOut bool - // (Дедлайн интрадей торговли - SellOutAhead) - это момент времени, когда бот начнет продавать - // все активные позиции - SellOutAhead time.Duration + //// (Дедлайн интрадей торговли - SellOutAhead) - это момент времени, когда бот начнет продавать + //// все активные позиции + //SellOutAhead time.Duration } type Bot struct { @@ -44,12 +43,8 @@ type Bot struct { // NewBot - Создание экземпляра бота на стакане // dd - дедлайн работы бота для интрадей торговли -func NewBot(ctx context.Context, c *investgo.Client, dd time.Time, config OrderBookStrategyConfig) (*Bot, error) { - botCtx, cancelBot := context.WithDeadline(ctx, dd) - // если нужно выходить из позиций, то бот будет завершать свою работу раньше чем дедлайн - if config.SellOut { - botCtx, cancelBot = context.WithDeadline(botCtx, dd.Add(-config.SellOutAhead)) - } +func NewBot(ctx context.Context, c *investgo.Client, config OrderBookStrategyConfig) (*Bot, error) { + botCtx, cancelBot := context.WithCancel(ctx) // по конфигу стратегии заполняем map для executor instrumentService := c.NewInstrumentsServiceClient() From e0e52363858ee553859ce63e252bcd3a124391b7 Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 12:49:40 +0300 Subject: [PATCH 53/57] add run by timer --- examples/ob_bot/cmd/main.go | 109 ++++++++++++++++++------------------ 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index dab0551..2a0e8b3 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "errors" "github.com/tinkoff/invest-api-go-sdk/examples/ob_bot/internal/bot" "github.com/tinkoff/invest-api-go-sdk/investgo" pb "github.com/tinkoff/invest-api-go-sdk/proto" @@ -10,6 +9,7 @@ import ( "log" "os" "os/signal" + "sync" "syscall" "time" ) @@ -87,72 +87,71 @@ func main() { // конфиг стратегии бота на стакане orderBookConfig := bot.OrderBookStrategyConfig{ - Instruments: instruments, - Depth: 20, - BuyRatio: 2, - SellRatio: 2, - MinProfit: 0.5, - SellOut: true, - SellOutAhead: 10 * time.Minute, - } - - // дедлайн для интрадей торговли - dd, err := tradingDeadLine(client, EXCHANGE) - if err != nil { - logger.Fatalf(err.Error()) + Instruments: instruments, + Depth: 20, + BuyRatio: 2, + SellRatio: 2, + MinProfit: 0.5, + SellOut: true, } // создание и запуск бота - botOnOrderBook, err := bot.NewBot(ctx, client, dd, orderBookConfig) + botOnOrderBook, err := bot.NewBot(ctx, client, orderBookConfig) if err != nil { logger.Fatalf("bot on order book creating fail %v", err.Error()) } + wg := &sync.WaitGroup{} + // Таймер для Московской биржи, отслеживает расписание и дает сигналы, на остановку/запуск бота + // cancelAhead - Событие STOP будет отправлено в канал за cancelAhead до конца торгов + cancelAhead := time.Minute * 5 + t := investgo.NewTimer(client, "MOEX", cancelAhead) + + // запуск таймера + wg.Add(1) + go func(ctx context.Context) { + defer wg.Done() + err := t.Start(ctx) + if err != nil { + logger.Errorf(err.Error()) + } + }(ctx) + go func() { <-sigs - botOnOrderBook.Stop() + t.Stop() }() - err = botOnOrderBook.Run() - if err != nil { - logger.Errorf(err.Error()) - } -} - -// tradingDeadLine - возвращает дедлайн торгов на сегодня, или ошибку если торговля на бирже сейчас недоступна -func tradingDeadLine(client *investgo.Client, exchange string) (time.Time, error) { - from := time.Now() - // так как основная сессия на бирже не больше 9 часов - to := from.Add(time.Hour * 9) - - instrumentsService := client.NewInstrumentsServiceClient() - resp, err := instrumentsService.TradingSchedules(exchange, from, to) - if err != nil { - return time.Time{}, err - } - - var deadLine time.Time - exchanges := resp.GetExchanges() - for _, exch := range exchanges { - // если нужная биржа - if exch.GetExchange() == exchange { - for _, day := range exch.GetDays() { - // если день совпадает с днем запроса - if from.Day() == day.GetDate().AsTime().Day() { - switch { - // выходной - case !day.GetIsTradingDay(): - return time.Time{}, errors.New("trading isn't available today") - // основная сессия либо еще не началась, либо уже закончилась - case from.Before(day.GetStartTime().AsTime().Local()) || from.After(day.GetEndTime().AsTime().Local()): - return time.Time{}, errors.New("from don't belong trading time") - // положительный случай, возвращаем остаток до конца основной сессии - case from.After(day.GetStartTime().AsTime().Local()) && from.Before(day.GetEndTime().AsTime().Local()): - deadLine = day.GetEndTime().AsTime().Local() - } + // чтение событий от таймера + events := t.Events() + wg.Add(1) + go func(ctx context.Context) { + defer wg.Done() + for { + select { + case <-ctx.Done(): + return + case ev, ok := <-events: + if !ok { + return + } + logger.Infof("got event = %v", ev) + switch ev { + case investgo.START: + wg.Add(1) + go func(ctx context.Context) { + defer wg.Done() + err = botOnOrderBook.Run() + if err != nil { + logger.Errorf(err.Error()) + } + }(ctx) + case investgo.STOP: + botOnOrderBook.Stop() } } } - } - return deadLine, nil + }(ctx) + + wg.Wait() } From 1263f23ee0938f72e672ee15159362b84331f98b Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 13:22:24 +0300 Subject: [PATCH 54/57] update README.md --- examples/ob_bot/README.md | 11 +++++------ examples/ob_bot/internal/bot/bot.go | 4 ---- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/examples/ob_bot/README.md b/examples/ob_bot/README.md index 06d4d56..a3f772f 100644 --- a/examples/ob_bot/README.md +++ b/examples/ob_bot/README.md @@ -20,9 +20,6 @@ type OrderBookStrategyConfig struct { MinProfit float64 // SellOut - Если true, то по достижению дедлайна бот выходит из всех активных позиций SellOut bool - // (Дедлайн интрадей торговли - SellOutAhead) - это момент времени, когда бот начнет продавать - // все активные позиции - SellOutAhead time.Duration } ``` @@ -43,9 +40,11 @@ type OrderBookStrategyConfig struct { * Цена открытия позиции меньше цены последней сделки по этому инструменту ### Режим работы -Данный пример ориентирован на торговлю внутри одного дня. При запуске бота функция `tradingDeadLine()` возвращает -дедлайн торгов на сегодня, если выставлен флаг `SellOut` и время `SellOutAhead`, то бот завершит работу и закроет все -позиции за `SellOutAhead`до дедлайна. +Данный пример ориентирован на торговлю внутри одного дня. За расписанием торгов следит `investgo.Timer`, +он сигнализирует о начале и завершении основной торговй сессии на сегодня. +При запуске main `investgo.Timer` возвращает канал с событиями, START/STOP - сигналы к запуску и остановке бота, +если выставлен флаг `SellOut` в конфигурации стратеги и время `cancelAhead` при создании таймера, то бот завершит работу и закроет все +позиции за `cancelAhead` до конца торгов текущего дня. ### Запуск diff --git a/examples/ob_bot/internal/bot/bot.go b/examples/ob_bot/internal/bot/bot.go index 2ff43da..040c8ba 100644 --- a/examples/ob_bot/internal/bot/bot.go +++ b/examples/ob_bot/internal/bot/bot.go @@ -26,9 +26,6 @@ type OrderBookStrategyConfig struct { MinProfit float64 // SellOut - Если true, то по достижению дедлайна бот выходит из всех активных позиций SellOut bool - //// (Дедлайн интрадей торговли - SellOutAhead) - это момент времени, когда бот начнет продавать - //// все активные позиции - //SellOutAhead time.Duration } type Bot struct { @@ -42,7 +39,6 @@ type Bot struct { } // NewBot - Создание экземпляра бота на стакане -// dd - дедлайн работы бота для интрадей торговли func NewBot(ctx context.Context, c *investgo.Client, config OrderBookStrategyConfig) (*Bot, error) { botCtx, cancelBot := context.WithCancel(ctx) From 507b098cc01d36f27273f5b6c76187c05a00584b Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 13:32:03 +0300 Subject: [PATCH 55/57] add comments --- examples/ob_bot/cmd/main.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/examples/ob_bot/cmd/main.go b/examples/ob_bot/cmd/main.go index 2a0e8b3..f29138f 100644 --- a/examples/ob_bot/cmd/main.go +++ b/examples/ob_bot/cmd/main.go @@ -95,7 +95,7 @@ func main() { SellOut: true, } - // создание и запуск бота + // создание бота на стакане botOnOrderBook, err := bot.NewBot(ctx, client, orderBookConfig) if err != nil { logger.Fatalf("bot on order book creating fail %v", err.Error()) @@ -117,12 +117,13 @@ func main() { } }(ctx) + // по сигналам останавливаем таймер go func() { <-sigs t.Stop() }() - // чтение событий от таймера + // чтение событий от таймера и управление ботом events := t.Events() wg.Add(1) go func(ctx context.Context) { @@ -138,15 +139,17 @@ func main() { logger.Infof("got event = %v", ev) switch ev { case investgo.START: + // запуск бота wg.Add(1) - go func(ctx context.Context) { + go func() { defer wg.Done() err = botOnOrderBook.Run() if err != nil { logger.Errorf(err.Error()) } - }(ctx) + }() case investgo.STOP: + // остановка бота botOnOrderBook.Stop() } } From ffdd0bf98208175d305828653ada5ccc8e9755fd Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 13:54:22 +0300 Subject: [PATCH 56/57] change log message for stop md stream --- investgo/md_stream.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/investgo/md_stream.go b/investgo/md_stream.go index 849e6a3..aead667 100644 --- a/investgo/md_stream.go +++ b/investgo/md_stream.go @@ -254,7 +254,7 @@ func (mds *MarketDataStream) Listen() error { for { select { case <-mds.ctx.Done(): - mds.mdsClient.logger.Infof("stop listening") + mds.mdsClient.logger.Infof("stop listening market data stream") return nil default: resp, err := mds.stream.Recv() @@ -262,7 +262,7 @@ func (mds *MarketDataStream) Listen() error { // если ошибка связана с завершением контекста, обрабатываем ее switch { case status.Code(err) == codes.Canceled: - mds.mdsClient.logger.Infof("stop listening") + mds.mdsClient.logger.Infof("stop listening market data stream") return nil default: return err From 17b3fffd7e3e58cf29629bb8ae7f7675f6ed8f4f Mon Sep 17 00:00:00 2001 From: jstalex Date: Wed, 28 Jun 2023 13:54:36 +0300 Subject: [PATCH 57/57] update README.md --- examples/ob_bot/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/ob_bot/README.md b/examples/ob_bot/README.md index a3f772f..f96e7dc 100644 --- a/examples/ob_bot/README.md +++ b/examples/ob_bot/README.md @@ -48,7 +48,9 @@ type OrderBookStrategyConfig struct { ### Запуск - $ cd examples/ob_bot + $ git clone https://github.com/tinkoff/invest-api-go-sdk + + $ cd invest-api-go-sdk/examples/ob_bot Создайте файл `config.yaml`