@@ -12,7 +12,9 @@ import (
1212 "maps"
1313 "os"
1414 "slices"
15+ "strings"
1516 "sync"
17+ "sync/atomic"
1618 "time"
1719
1820 "decred.org/dcrdex/client/asset"
@@ -119,8 +121,9 @@ type bot interface {
119121
120122type runningBot struct {
121123 bot
122- cm * dex.ConnectionMaster
123- cexCfg * CEXConfig
124+ cm * dex.ConnectionMaster
125+ cexCfg * CEXConfig
126+ stopping atomic.Bool
124127}
125128
126129func (rb * runningBot ) assets () map [uint32 ]any {
@@ -343,8 +346,9 @@ func newCEXProblems() *CEXProblems {
343346
344347// BotStatus is state information about a configured bot.
345348type BotStatus struct {
346- Config * BotConfig `json:"config"`
347- Running bool `json:"running"`
349+ Config * BotConfig `json:"config"`
350+ Running bool `json:"running"`
351+ Stopping bool `json:"stopping"`
348352 // RunStats being non-nil means the bot is running.
349353 RunStats * RunStats `json:"runStats"`
350354 LatestEpoch * EpochReport `json:"latestEpoch"`
@@ -366,14 +370,17 @@ func (m *MarketMaker) Status() *Status {
366370 var stats * RunStats
367371 var epochReport * EpochReport
368372 var cexProblems * CEXProblems
373+ var stopping bool
369374 if rb != nil {
370375 stats = rb .stats ()
371376 epochReport = rb .latestEpoch ()
372377 cexProblems = rb .latestCEXProblems ()
378+ stopping = rb .stopping .Load ()
373379 }
374380 status .Bots = append (status .Bots , & BotStatus {
375381 Config : botCfg ,
376382 Running : rb != nil ,
383+ Stopping : stopping ,
377384 RunStats : stats ,
378385 LatestEpoch : epochReport ,
379386 CEXProblems : cexProblems ,
@@ -409,6 +416,7 @@ func (m *MarketMaker) RunningBotsStatus() *Status {
409416 status .Bots = append (status .Bots , & BotStatus {
410417 Config : rb .botCfg (),
411418 Running : true ,
419+ Stopping : rb .stopping .Load (),
412420 RunStats : rb .stats (),
413421 LatestEpoch : rb .latestEpoch (),
414422 CEXProblems : rb .latestCEXProblems (),
@@ -718,6 +726,10 @@ func (m *MarketMaker) Connect(ctx context.Context) (*sync.WaitGroup, error) {
718726}
719727
720728func (m * MarketMaker ) balancesSufficient (balances * BotBalanceAllocation , mkt * MarketWithHost , botCfg * BotConfig , cexCfg * CEXConfig ) error {
729+ if balances == nil {
730+ return fmt .Errorf ("no balance allocation provided" )
731+ }
732+
721733 availableDEXBalances , availableCEXBalances , err := m .availableBalances (mkt , botCfg .CEXBaseID , botCfg .CEXQuoteID , cexCfg )
722734 if err != nil {
723735 return fmt .Errorf ("error getting available balances: %v" , err )
@@ -863,28 +875,19 @@ func (m *MarketMaker) newBot(cfg *BotConfig, adaptorCfg *exchangeAdaptorCfg) (bo
863875 }
864876}
865877
866- // StartConfig contains the data that must be submitted with a call to StartBot.
867- type StartConfig struct {
868- MarketWithHost
869- AutoRebalance * AutoRebalanceConfig `json:"autoRebalance"`
870- Alloc * BotBalanceAllocation `json:"alloc"`
871- }
872-
873878// StartBot starts a market making bot.
874- func (m * MarketMaker ) StartBot (startCfg * StartConfig , alternateConfigPath * string , appPW []byte , overrideLotSizeChange bool ) (err error ) {
875- mkt := startCfg .MarketWithHost
876-
879+ func (m * MarketMaker ) StartBot (mkt * MarketWithHost , alternateConfigPath * string , appPW []byte , overrideLotSizeChange bool ) (err error ) {
877880 m .startUpdateMtx .Lock ()
878881 defer m .startUpdateMtx .Unlock ()
879882
880883 m .runningBotsMtx .RLock ()
881- _ , found := m .runningBots [startCfg . MarketWithHost ]
884+ _ , found := m .runningBots [* mkt ]
882885 m .runningBotsMtx .RUnlock ()
883886 if found {
884887 return fmt .Errorf ("bot for %s already running" , mkt )
885888 }
886889
887- coreMkt , err := m .core .ExchangeMarket (startCfg .Host , startCfg .BaseID , startCfg .QuoteID )
890+ coreMkt , err := m .core .ExchangeMarket (mkt .Host , mkt .BaseID , mkt .QuoteID )
888891 if err != nil {
889892 return fmt .Errorf ("error getting market: %v" , err )
890893 }
@@ -898,16 +901,11 @@ func (m *MarketMaker) StartBot(startCfg *StartConfig, alternateConfigPath *strin
898901 }
899902 }
900903
901- botCfg , cexCfg , err := m .configsForMarket (& startCfg . MarketWithHost , alternateConfigPath )
904+ botCfg , cexCfg , err := m .configsForMarket (mkt , alternateConfigPath )
902905 if err != nil {
903906 return err
904907 }
905908
906- if botCfg .RPCConfig != nil {
907- startCfg .Alloc = botCfg .RPCConfig .Alloc
908- startCfg .AutoRebalance = botCfg .RPCConfig .AutoRebalance
909- }
910-
911909 // Lot size may be zero if started from RPC. If the lot size in the config
912910 // is set, then we check if the lot size has changed since the configuration
913911 // was saved. If so, and overrideLotSizeChange is false, we return an error.
@@ -933,12 +931,11 @@ func (m *MarketMaker) StartBot(startCfg *StartConfig, alternateConfigPath *strin
933931 botCfg .LotSize = mktInfo .LotSize
934932 }
935933
936- return m .startBot (startCfg , botCfg , cexCfg , appPW )
934+ return m .startBot (mkt , botCfg , cexCfg , appPW )
937935}
938936
939- func (m * MarketMaker ) startBot (startCfg * StartConfig , botCfg * BotConfig , cexCfg * CEXConfig , appPW []byte ) (err error ) {
940- mwh := & startCfg .MarketWithHost
941- if err := m .balancesSufficient (startCfg .Alloc , mwh , botCfg , cexCfg ); err != nil {
937+ func (m * MarketMaker ) startBot (mkt * MarketWithHost , botCfg * BotConfig , cexCfg * CEXConfig , appPW []byte ) (err error ) {
938+ if err := m .balancesSufficient (botCfg .Alloc , mkt , botCfg , cexCfg ); err != nil {
942939 return err
943940 }
944941
@@ -971,10 +968,10 @@ func (m *MarketMaker) startBot(startCfg *StartConfig, botCfg *BotConfig, cexCfg
971968
972969 adaptorCfg := & exchangeAdaptorCfg {
973970 botID : dexMarketID (botCfg .Host , botCfg .BaseID , botCfg .QuoteID ),
974- mwh : mwh ,
975- baseDexBalances : startCfg .Alloc .DEX ,
976- baseCexBalances : startCfg .Alloc .CEX ,
977- autoRebalanceConfig : startCfg .AutoRebalance ,
971+ mwh : mkt ,
972+ baseDexBalances : botCfg .Alloc .DEX ,
973+ baseCexBalances : botCfg .Alloc .CEX ,
974+ autoRebalanceConfig : botCfg .AutoRebalance ,
978975 core : m .core ,
979976 cex : cex ,
980977 log : m .botSubLogger (botCfg ),
@@ -997,14 +994,14 @@ func (m *MarketMaker) startBot(startCfg *StartConfig, botCfg *BotConfig, cexCfg
997994 go func () {
998995 cm .Wait ()
999996 m .runningBotsMtx .Lock ()
1000- if bot , found := m .runningBots [* mwh ]; found {
997+ if bot , found := m .runningBots [* mkt ]; found {
1001998 if bot .botCfg ().requiresPriceOracle () {
1002- m .oracle .stopAutoSyncingMarket (mwh .BaseID , mwh .QuoteID )
999+ m .oracle .stopAutoSyncingMarket (mkt .BaseID , mkt .QuoteID )
10031000 }
1004- delete (m .runningBots , * mwh )
1001+ delete (m .runningBots , * mkt )
10051002 }
10061003 m .runningBotsMtx .Unlock ()
1007- m .core .Broadcast (newRunStatsNote (mwh .Host , mwh .BaseID , mwh .QuoteID , nil ))
1004+ m .core .Broadcast (newRunStatsNote (mkt .Host , mkt .BaseID , mkt .QuoteID , nil ))
10081005 }()
10091006
10101007 startedBot = true
@@ -1016,24 +1013,66 @@ func (m *MarketMaker) startBot(startCfg *StartConfig, botCfg *BotConfig, cexCfg
10161013 }
10171014
10181015 m .runningBotsMtx .Lock ()
1019- m .runningBots [* mwh ] = rb
1016+ m .runningBots [* mkt ] = rb
10201017 m .runningBotsMtx .Unlock ()
10211018
10221019 return nil
10231020}
10241021
1025- // StopBot stops a running bot.
1022+ // StopBot stops a running bot. The function returns immediately; the bot
1023+ // stops asynchronously. A notification is broadcast when the bot finishes
1024+ // stopping.
10261025func (m * MarketMaker ) StopBot (mkt * MarketWithHost ) error {
10271026 runningBots := m .runningBotsLookup ()
10281027 bot , found := runningBots [* mkt ]
10291028 if ! found {
10301029 return fmt .Errorf ("no bot running on market: %s" , mkt )
10311030 }
1032- bot .cm . Disconnect ( )
1033- m . core . Broadcast ( newRunStatsNote ( mkt . Host , mkt . BaseID , mkt . QuoteID , nil ) )
1031+ bot .stopping . Store ( true )
1032+ go bot . cm . Disconnect ( )
10341033 return nil
10351034}
10361035
1036+ // StartBots starts all bots in the provided config file.
1037+ func (m * MarketMaker ) StartBots (cfgPath * string , appPW []byte ) (started int , err error ) {
1038+ cfg , err := getMarketMakingConfig (* cfgPath )
1039+ if err != nil {
1040+ return 0 , fmt .Errorf ("error loading config: %w" , err )
1041+ }
1042+ if len (cfg .BotConfigs ) == 0 {
1043+ return 0 , fmt .Errorf ("no bots configured in %s" , * cfgPath )
1044+ }
1045+ var errs []string
1046+ for _ , botCfg := range cfg .BotConfigs {
1047+ mkt := & MarketWithHost {botCfg .Host , botCfg .BaseID , botCfg .QuoteID }
1048+ if err := m .StartBot (mkt , cfgPath , appPW , true ); err != nil {
1049+ errs = append (errs , fmt .Sprintf ("%s: %v" , mkt , err ))
1050+ } else {
1051+ started ++
1052+ }
1053+ }
1054+ if len (errs ) > 0 {
1055+ return started , fmt .Errorf ("some bots failed to start: %s" , strings .Join (errs , "; " ))
1056+ }
1057+ return started , nil
1058+ }
1059+
1060+ // StopBots signals all running bots to stop and returns immediately.
1061+ // The bots will finish stopping asynchronously.
1062+ func (m * MarketMaker ) StopBots () (stopped int , err error ) {
1063+ runningBots := m .runningBotsLookup ()
1064+ if len (runningBots ) == 0 {
1065+ return 0 , fmt .Errorf ("no bots running" )
1066+ }
1067+ for mkt := range runningBots {
1068+ if err := m .StopBot (& mkt ); err != nil {
1069+ continue
1070+ }
1071+ stopped ++
1072+ }
1073+ return stopped , nil
1074+ }
1075+
10371076func getMarketMakingConfig (path string ) (* MarketMakingConfig , error ) {
10381077 if path == "" {
10391078 return nil , fmt .Errorf ("no config file provided" )
0 commit comments