# Máquinas de estado , el mindset que necesitábamos
Luego de mucho tiempo especular, respirar y exhalar humo con respecto a _las cryptos_, decidimos meternos a este mundo descentralizado y aprovechar nuestros conocimientos en programación para hacer cosas más dinámicas que sólo _holdear_ o tomar un curso de trading y aprovechar la UI del exchange de turno.
La idea original fue usar las API que los exchange exponen y -con un poco de suerte- poner a correr un algoritmo que pudiera _mover_ nuestras cryptos. Comprar y vender básicamente. Pusimos manos a la obra y luego de muchas horas delante del computador y varias tazas de café, se logró poner en producción una versión demasiado "alfa" de lo que era nuestro algoritmo.
### El problema
Ya que estábamos entrando en un mercado nuevo, teníamos que comprar el activo primero para luego poder venderlo. Esto significó que nuestra primera consigna fue tener una forma automatizada para poder hacer la compra de nuestra _crypto_. El problema fue que al estar en un mercado de oferta/demanda constante, el precio del activo que queremos comprar puede cambiar de un segundo a otro y todo lo que nuestro algoritmo había calculado, ya no sirve.
Lo anterior llevó poco a poco a que nuestro código fuera un caracol interminable, y el hacer _testing_ era prácticamente imposible. El cambio constante de precio a mitad del proceso hacía que mucho código se repitiera y se saltara de un lado al otro. Ese código era imposible de seguir.
Fuera de todo lo complicado que fue implementar la primera versión, lo más doloroso era _debuggear_ y entender dónde se moría el programa y por qué lo hacía. Cada vez que el código tenía algún error, nos decíamos a nosotros mismos: **deberíamos tener una forma de visualizar por qué parte del algoritmo estamos pasando en X momento**
> No sólo **_goto is evil_**, también lo puede ser un código sin este _keyword_. Nuestra primera versión del algoritmo era básicamente un `for` loop con un montón de `continue` y `break` dentro de otros varios `if/else`.
Un problema doloros fue que la única forma de probar nuestra primera versión, era corriendolo contra un _exchange_ real, con plata real. Cada vez que se corría una prueba era una angustia tremenda porque, por muy poco que fuera, estábamos perdiendo plata de verdad y eso no fue divertido.
### Turing al rescate
Luego de muchos cabezazo, varios ceviches... Unos cuántos Gin y muchos dibujos diferentes de nuestro algoritmo, logramos llegar a una idea que nos convenció a todos: **llevar nuestro algoritmo a una máquina de estados**.
Una máquina de estados nos permitió abstraer nuestro algoritmo enorme en pequeños subprocesos con una combinación de _input/output_ , que pareció encajar perfecto en nuestro problema. Desde el punto de vista _del dibujo_, sabíamos perfectamente qué teníamos que hacer en cada subproceso, además ahora con este _approach_ de la máquina de estados, teníamos una forma clara de cómo hacer tests sobre nuestro código, algo que hasta ese punto era imposible.
Luego de buscar en las bondades del _open-source_ y la comunidad de **Go**, encontramos un par de implementaciones interesantes que probamos en ejemplos simples pero, por un motivo u otro, no era lo que estábamos buscando. Además de esto, ninguna de las opciones que evaluamos nos daba la opción de visualizar qué trozo de código se estaba ejecutando.
Como somos motivados, decidimos implementar nuestra propia versión de una máquina de estados con los siguientes puntos en mente:
1. Fácil de usar (al menos más que las opciones anteriores).
2. Soporte para visualización.
3. Validación en las transiciones de estado.
4. Soporte al manejo de errores custom. Esto es super importante para hacer cosas cada vez más _custom_ .
El diseño y la implementación se mantuvieron lo más liviano posible, es decir, se definieron sólo las estructura y tipos que eran estrictamente necesarios para el funcionamiento de la máquina.
### Talk is cheap, show me the code
Para mostrar y probar nuestra máquina de estados tomaremos como referencia el ejemplo pequeño que está disponible en este [repositorio de github](https://github.com/blue-factory/statemachine-buda-example). Este repo tiene el código de un algorítmo de compra básico que funciona sobre nuestra **statemachine** y usa la API de [buda](https://buda.com) para operar. De esta manera, nustras únicas dependencias son:
- [statemachine go package](https://github.com/blue-factory/statemachine)
- [Buda client go package](https://github.com/mtavano/buda-go)
#### El algoritmo
El diagrama que tenemos a continuación describe gráficamente nuestro algoritmo. Disponible con el método `RenderMermaid` de nuestra **statemachine**
<center>

</center>
Tenemos 4 estados que según su transición al siguiente, determinan como se va armando nuestra máquina de estados.
Para que nuestra maquinita funcione, necesitamos configurar las siguiente variables de entorno:
```
BUDA_API_KEY=buda-api-key
BUDA_API_SECRET=buda-api-secret
TARGET_VOLUME=100000 // amount to spend
RATE_TO_ACTION=0.1 // minimum expected price fall
CURRENCY=LTC //crypto currency
SHOULD_RUN=1
```
En palabaras un poco más simple, podemos resumir el diagrama y la configuración de la siguiente manera:
> Consultaremos constantemente el mercado (ticker) para poder calcular si el precio de la _crypto_ `CURRENCY` ha bajado **al menos** un porcentaje igual a `RATE_TO_ACTION`. Si esto sucede, compraremos el equivalente a `TARGET_VOLUME` en nuestra moneda local (CLP en nuestro caso) de la _crypto_ `CURRENCY` al precio de mercado.
Los estados y su explicación son los siguientes:
- `event_get_ticker`
- Este estado tiene como finalidad gatillar la obtención del _ticker_ según la moneda base que se le configure. En el caso de la API de buda sólo tenemos disponibles `BTC`; `LTC`; `BCH`; `ETH`.
- Cuando se tenga el ticker, se despachará el evento `event_calculate`, pero si hay algún error, será nuevamente `event_get_ticker`.
```go
const (
eventGetTicker = "event_get_ticker"
)
func (b *Bot) GetTickerHandler(e *statemachine.Event) (*statemachine.Event, error) {
nextRequestStartAt := b.lastRequestMadeAt.Add(time.Second * time.Duration(b.config.TimeoutInSeconds))
now := b.now()
if now.Before(nextRequestStartAt) { // to avoid blocks by too many requests
return &statemachine.Event{Name: eventGetTicker}, nil
}
ticker, err := b.buda.GetTicker(strings.ToUpper(b.config.Currency))
if err != nil {
log.Printf("bot: Bot GetTickerHandler b.buda.GetTicker error %s", err.Error())
return &statemachine.Event{Name: eventGetTicker}, nil
}
b.lastRequestMadeAt = now
return &statemachine.Event{
Name: eventCalculate,
Data: map[string]interface{}{
"payload": &eventPayload{ticker: ticker},
},
}, nil
}
```
- `event_calculate`
- Este estado tiene como finalidad calcular si el precio de nuestro _ticker_ cumple con nuestra porcentaje configurado anteriormente con `RATE_TO_ACTION`. En caso de cumplir será despachado el evento `event_create_order`, de lo contrario se despachará `event_get_ticker`.
- Para calcular un porcentaje de bajada necesitamos un precio de referencia, este será el valor del primer ticker que obtengamos.
```go
const (
eventCalculate = "event_calculate"
)
var bitSize = 64
func (b *Bot) CalculateHandler(e *statemachine.Event) (*statemachine.Event, error) {
payload := b.parseData(e.Data)
ask, err := strconv.ParseFloat(payload.ticker.MinAsk[0], bitSize)
if err != nil {
// if this happens we will assume that is something wrong with the API response/connection
return nil, errors.Wrap(err, "bot: Bot CalculateHandler strconv.ParseFloat error")
}
// if is the first execution
if b.referencePrice == 0 {
b.referencePrice = ask
}
if ask <= (b.referencePrice*1 + (b.config.RateToAction / 100)) {
return &statemachine.Event{Name: eventGetTicker}, nil
}
return &statemachine.Event{
Name: eventCreateOrder,
Data: map[string]interface{}{
"payload": &eventPayload{
buyPrice: ask,
},
},
}, nil
}
```
- `event_create_order`
- Este evento es el encargado de interactuar con la api de buda y poner una order de compra. En este caso en particular, nuestra orden de compra será de tipo market por lo que se espera que sea una _compra instantánea_. Una vez que se compró, se despachará `event_finish`, si la llamada al API de buda falla retornaremos un error y la ejecución de nuestro flujo será terminada por la **statemachine**.
```go
const (
eventCreateOrder = "event_create_order"
typeOrderBid = "Bid"
typePriceLimit = "market"
)
func (b *Bot) BuyCryptoHandler(e *statemachine.Event) (*statemachine.Event, error) {
payload := b.parseData(e.Data)
order, err := b.buda.CreateOrder(b.config.Currency, &buda.CreateOrderRequest{
Type: typeOrderBid,
PriceType: typePriceLimit,
Amount: b.config.TargetVolume / payload.buyPrice,
})
if err != nil {
return nil, errors.Wrap(err, "but: Bot.BuyCryptoHandler b.buda.CreateOrder error")
}
log.Printf("Bot.BuyCryptoHandler order created %+v", order)
return &statemachine.Event{Name: eventFinish}, nil
}
```
- `event_finish`
- Este evento despacha un `event_abort` luego que hayamos terminado la ejecución por completo
```go
const (
eventFinish = "event_finish"
)
func (b *Bot) FinishHandler(e *statemachine.Event) (*statemachine.Event, error) {
return &statemachine.Event{Name: statemachine.EventAbort}, nil
}
```
### Conclusión
Pasamos de un approach bastante simplista a un approach más complejo. Esto no es porque sí. Si bien es cierto, un `for` _loop_ más `break` y `continue` suelen ser la solución en muchos de los casos, esta vez el problema no era la solución sino como probar nuestra solución.
Mientras que la máquina de estados agrega una cuota de complejidad que antes no teníamos, por otra parte facilita enormemente el proceso de _testing_ ya que en vez de tener un ciclo enorme y con todo el código acoplado, ahora tenemos todas las partes de nuestro algoritmo separadas por responsabilidad y bien definidas.