Copiar .env.example a .env y modificar con los datos de conexión a la db.
Si se está usando docker, los datos pueden quedar tal cual están.
pnpm installIncluí una db postgres local en docker. Esta base de datos se inicializa usando
el archivo database.sql. Es el mismo que fue proveído en el challenge.
Para levantarla correr:
docker compose up -dDe no usarla, se tiene que actualizar el
.envcon los datos de la db de neon. Sino, sale andando tal como está.
Usando drizzle-kit, hacer push para actualizar la db:
pnpm drizzle-kit pushSi por alguna razón,
pnpm drizzle-kit pushpregunta de truncar tablas, responder que no (debería ser la opcion seleccionada por defecto). De última, siempre está la posibilidad de usar las queries dedatabase.sqlcon Drizzle Studio para restaurar la db fácil.
Luego reconstruir las positions para el único usuario que inicialmente tiene ordenes:
pnpm rebuild-positions 10001
# Modo dry-run para ver cual sería el resultado:
pnpm rebuild-positions 10001 --dry
rebuild-positionssolo es necesario la primera vez, para compensar el estado original de la db, que no tiene positions precargadas. Si se corre más de una vez no pasa nada, es idempotente (a excepcion de la fecha delast_updated).
Para levantar el dev server:
pnpm dev # O usar la task de vscode - Detalles más abajoPor defecto levanta en el puerto 3000, pero se puede cambiar seteando la
variable de entorno PORT en el .env.
Para facilitar un poco el desarrollo, incluí 2 tasks de vscode:
pnpm devlevanta el dev serverdrizzle-kit studiolevanta drizzle-kit studio
Hay tests unitarios y de integración. Para correrlos:
# Correr todos los tests:
pnpm test
# O por separado:
pnpm test:unit
pnpm test:integrationLos tests de integración corren en una db in-memory usando PGlite.
Incluí requests para probar el API usando la extension de vscode REST Client.
Están en la carpeta rest-client. Hay uno por contexto, account, market, y
orders.
Importante: De cambiar el puerto del dev-server, actualizarlo en
.vscode/settings.json.
Opté por dividir el sistema en 3 bounded contexts, y un módulo compartido:
- Portfolio
- Trading
- Market
- Shared
La desición fue porque ninguna de las 3 operaciones parecían estar fuertemente relacionadas. Portfolio solo se encarga de mostrar datos de la cuenta, Market es un dominio soporte que solo provee datos, y Trading es donde las operaciones se llevan a cabo.
Intenté evitar comunicación cross-modulo por medio de Shared.
Para lograr esto, todas las cosas que se necesitan en común y la comunicación cross-module están en Shared. Como la conexión a la db, utilidades de arquitectura (bootstrapping, UoW, domain events, etc), contratos comunes, DTOs y types compartidos, etc.
Ya que tanto Trading como Portfolio necesitan datos de Market, el contrato de
MarketDataProvider reside en Shared, pero la implementación está en Market.
En el caso de Trading, necesita tambíen accedar a datos de Portfolio (datos de
la cuenta), de manera que el contrato de AccountDataProvider lo define Trading,
pero la implementación está en Portfolio.
La idea es tener un "snapshot" acumulado de cada instrumento para evitar consultar el historial de ordenes constantemente.
Inicialmente no sería un gran problema no tenerlo, pero a medida que el historial de ordenes crece, se iría haciendo más pesado—O(n). Al tener Position, se actualiza (o crea) la correspondiente position del instrumento, teniendo así una consulta O(1) en vez O(n) por instrumento para generar la respuesta del portfolio.
La función de este campo sería análoga a la de la tabla positions, cumpliendo
el mismo objetivo de evitar recorrer todas las ordenes para calcular el dinero
disponible.
Opté por esta solución porque, según los requerimientos, "Los precios de los
activos tienen que estar en pesos". Si se necesitara soportar operaciones
multi-moneda, entonces habría que tratar a los instrumentos de tipo MONEDA
como el resto. Pero esto implicaría tambíen la necesidad de aclarar con qué
moneda se hace cada operación y repensar el snapshot de positions, porque un
mismo instrumento podría ser operado en distintas monedas, entre otras cosas.
Con el objetivo de optimizar la base de datos y asegurar la consistencia de los datos, agregué los siguientes índices nuevos:
usersemail:uniquenot nullaccountnumber:uniquenot null
ordersnot nulla todos los campos
instrumentstickeruniquenot nullnot nulla todos los campos
marketdatadate:not null
-
En el endpoint para enviar una order, idealmente se debería validar que que
userIdoaccountNumberconcuerde con el usuario que está haciendo el request, ya sea por medio de JWT o cualquier otro medio de autenticación.Esta validación ya se está aplicando a nivel repo, para evitar que usuarios puedan generar ordenes a otros usuarios, pero al no haber autenticación es solo un proof-of-concept.
-
Actualmente, si bien no está especificado en los requerimientos, en la funcionalidad de Venta en Corto se permite sólo si el usuario tiene el 100% de pesos disponibles para cubrir la venta. Según lo que investigué, lo ideal sería implementar una limitación usando un "factor de garantía" para que el monto a cubrir sea mayor al total de la venta, ya que la pérdida es potencialmente ilimitada. Pero esto implica otros conceptos que también están fuera de los requisitos originales.
-
Validar venta si se pasa de posición larga a corta. De momento se permite este tipo de venta (por ej.: tenía 10 acciones, quiero vender 20). Se podría incluir una validación para no permitir pasar a una posición corta y solo permitir ventas en corto si se tiene 0. Pero no estoy seguro de las reglas de negocio en este caso.