From 495c11e47386781ebf491b06345acd8e2111d819 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 10 May 2022 12:49:15 +0430 Subject: [PATCH 1/9] Implement fee calculator --- .../opex/accountant/app/config/AppConfig.kt | 293 +++++------ .../opex/accountant/core/api/FeeCalculator.kt | 12 + .../nilin/opex/accountant/core/model/Order.kt | 67 +-- .../core/service/FeeCalculatorImpl.kt | 145 ++++++ .../core/service/TradeManagerImpl.kt | 468 +++++++----------- .../walletproxy/proxy/WalletProxyImpl.kt | 138 +++--- 6 files changed, 589 insertions(+), 534 deletions(-) create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt index d8a8791c2..1faf9c6cf 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt @@ -1,141 +1,154 @@ -package co.nilin.opex.accountant.app.config - -import co.nilin.opex.accountant.app.listener.AccountantEventListener -import co.nilin.opex.accountant.app.listener.AccountantTempEventListener -import co.nilin.opex.accountant.app.listener.AccountantTradeListener -import co.nilin.opex.accountant.app.listener.OrderListener -import co.nilin.opex.accountant.core.api.FinancialActionJobManager -import co.nilin.opex.accountant.core.api.OrderManager -import co.nilin.opex.accountant.core.api.TradeManager -import co.nilin.opex.accountant.core.service.FinancialActionJobManagerImpl -import co.nilin.opex.accountant.core.service.OrderManagerImpl -import co.nilin.opex.accountant.core.service.TradeManagerImpl -import co.nilin.opex.accountant.core.spi.* -import co.nilin.opex.accountant.ports.kafka.listener.consumer.EventKafkaListener -import co.nilin.opex.accountant.ports.kafka.listener.consumer.OrderKafkaListener -import co.nilin.opex.accountant.ports.kafka.listener.consumer.TempEventKafkaListener -import co.nilin.opex.accountant.ports.kafka.listener.consumer.TradeKafkaListener -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.scheduling.annotation.EnableScheduling - -@Configuration -@EnableScheduling -class AppConfig { - - @Bean - fun getFinancialActionJobManager( - financialActionLoader: FinancialActionLoader, - financialActionPersister: FinancialActionPersister, - walletProxy: WalletProxy - ): FinancialActionJobManager { - return FinancialActionJobManagerImpl( - financialActionLoader, - financialActionPersister, - walletProxy - ) - } - - @Bean - fun orderManager( - pairConfigLoader: PairConfigLoader, - financialActionPersister: FinancialActionPersister, - financeActionLoader: FinancialActionLoader, - orderPersister: OrderPersister, - tempEventPersister: TempEventPersister, - tempEventRepublisher: TempEventRepublisher, - richOrderPublisher: RichOrderPublisher - ): OrderManager { - return OrderManagerImpl( - pairConfigLoader, - financialActionPersister, - financeActionLoader, - orderPersister, - tempEventPersister, - tempEventRepublisher, - richOrderPublisher - ) - } - - @Bean - fun tradeManager( - pairStaticRateLoader: PairStaticRateLoader, - financeActionPersister: FinancialActionPersister, - financeActionLoader: FinancialActionLoader, - orderPersister: OrderPersister, - tempEventPersister: TempEventPersister, - richTradePublisher: RichTradePublisher, - richOrderPublisher: RichOrderPublisher, - walletProxy: WalletProxy, - @Value("\${app.coin}") platformCoin: String, - @Value("\${app.address}") platformAddress: String - ): TradeManager { - return TradeManagerImpl( - pairStaticRateLoader, - financeActionPersister, - financeActionLoader, - orderPersister, - tempEventPersister, - richTradePublisher, - richOrderPublisher, - walletProxy, - platformCoin, - platformAddress - ) - } - - @Bean - fun orderListener(orderManager: OrderManager): OrderListener { - return OrderListener(orderManager) - } - - @Bean - fun accountantTradeListener(tradeManager: TradeManager): AccountantTradeListener { - return AccountantTradeListener(tradeManager) - } - - @Bean - fun accountantEventListener(orderManager: OrderManager): AccountantEventListener { - return AccountantEventListener(orderManager) - } - - @Bean - fun accountantTempEventListener( - orderManager: OrderManager, - tradeManager: TradeManager - ): AccountantTempEventListener { - return AccountantTempEventListener(orderManager, tradeManager) - } - - @Autowired - fun configureOrderListener(orderKafkaListener: OrderKafkaListener, orderListener: OrderListener) { - orderKafkaListener.addOrderListener(orderListener) - } - - @Autowired - fun configureTradeListener( - tradeKafkaListener: TradeKafkaListener, - accountantTradeListener: AccountantTradeListener - ) { - tradeKafkaListener.addTradeListener(accountantTradeListener) - } - - @Autowired - fun configureEventListener( - eventKafkaListener: EventKafkaListener, - accountantEventListener: AccountantEventListener - ) { - eventKafkaListener.addEventListener(accountantEventListener) - } - - @Autowired - fun configureTempEventListener( - tempEventKafkaListener: TempEventKafkaListener, - accountantTempEventListener: AccountantTempEventListener - ) { - tempEventKafkaListener.addEventListener(accountantTempEventListener) - } - +package co.nilin.opex.accountant.app.config + +import co.nilin.opex.accountant.app.listener.AccountantEventListener +import co.nilin.opex.accountant.app.listener.AccountantTempEventListener +import co.nilin.opex.accountant.app.listener.AccountantTradeListener +import co.nilin.opex.accountant.app.listener.OrderListener +import co.nilin.opex.accountant.core.api.FeeCalculator +import co.nilin.opex.accountant.core.api.FinancialActionJobManager +import co.nilin.opex.accountant.core.api.OrderManager +import co.nilin.opex.accountant.core.api.TradeManager +import co.nilin.opex.accountant.core.service.FeeCalculatorImpl +import co.nilin.opex.accountant.core.service.FinancialActionJobManagerImpl +import co.nilin.opex.accountant.core.service.OrderManagerImpl +import co.nilin.opex.accountant.core.service.TradeManagerImpl +import co.nilin.opex.accountant.core.spi.* +import co.nilin.opex.accountant.ports.kafka.listener.consumer.EventKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.OrderKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.TempEventKafkaListener +import co.nilin.opex.accountant.ports.kafka.listener.consumer.TradeKafkaListener +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.scheduling.annotation.EnableScheduling + +@Configuration +@EnableScheduling +class AppConfig { + + @Bean + fun getFinancialActionJobManager( + financialActionLoader: FinancialActionLoader, + financialActionPersister: FinancialActionPersister, + walletProxy: WalletProxy + ): FinancialActionJobManager { + return FinancialActionJobManagerImpl( + financialActionLoader, + financialActionPersister, + walletProxy + ) + } + + @Bean + fun orderManager( + pairConfigLoader: PairConfigLoader, + financialActionPersister: FinancialActionPersister, + financeActionLoader: FinancialActionLoader, + orderPersister: OrderPersister, + tempEventPersister: TempEventPersister, + tempEventRepublisher: TempEventRepublisher, + richOrderPublisher: RichOrderPublisher + ): OrderManager { + return OrderManagerImpl( + pairConfigLoader, + financialActionPersister, + financeActionLoader, + orderPersister, + tempEventPersister, + tempEventRepublisher, + richOrderPublisher + ) + } + + @Bean + fun tradeManager( + financeActionPersister: FinancialActionPersister, + financeActionLoader: FinancialActionLoader, + orderPersister: OrderPersister, + tempEventPersister: TempEventPersister, + richTradePublisher: RichTradePublisher, + richOrderPublisher: RichOrderPublisher + ): TradeManager { + return TradeManagerImpl( + financeActionPersister, + financeActionLoader, + orderPersister, + tempEventPersister, + richTradePublisher, + richOrderPublisher + ) + } + + @Bean + fun feeCalculator( + financeActionPersister: FinancialActionPersister, + financeActionLoader: FinancialActionLoader, + pairStaticRateLoader: PairStaticRateLoader, + walletProxy: WalletProxy, + @Value("\${app.coin}") platformCoin: String, + @Value("\${app.address}") platformAddress: String + ): FeeCalculator { + return FeeCalculatorImpl( + financeActionPersister, + financeActionLoader, + pairStaticRateLoader, + walletProxy, + platformCoin, + platformAddress + ) + } + + @Bean + fun orderListener(orderManager: OrderManager): OrderListener { + return OrderListener(orderManager) + } + + @Bean + fun accountantTradeListener(tradeManager: TradeManager): AccountantTradeListener { + return AccountantTradeListener(tradeManager) + } + + @Bean + fun accountantEventListener(orderManager: OrderManager): AccountantEventListener { + return AccountantEventListener(orderManager) + } + + @Bean + fun accountantTempEventListener( + orderManager: OrderManager, + tradeManager: TradeManager + ): AccountantTempEventListener { + return AccountantTempEventListener(orderManager, tradeManager) + } + + @Autowired + fun configureOrderListener(orderKafkaListener: OrderKafkaListener, orderListener: OrderListener) { + orderKafkaListener.addOrderListener(orderListener) + } + + @Autowired + fun configureTradeListener( + tradeKafkaListener: TradeKafkaListener, + accountantTradeListener: AccountantTradeListener + ) { + tradeKafkaListener.addTradeListener(accountantTradeListener) + } + + @Autowired + fun configureEventListener( + eventKafkaListener: EventKafkaListener, + accountantEventListener: AccountantEventListener + ) { + eventKafkaListener.addEventListener(accountantEventListener) + } + + @Autowired + fun configureTempEventListener( + tempEventKafkaListener: TempEventKafkaListener, + accountantTempEventListener: AccountantTempEventListener + ) { + tempEventKafkaListener.addEventListener(accountantTempEventListener) + } + } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt new file mode 100644 index 000000000..b13a52c3b --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt @@ -0,0 +1,12 @@ +package co.nilin.opex.accountant.core.api + +import co.nilin.opex.accountant.core.model.Order +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent + +interface FeeCalculator { + + suspend fun createMakerFeeAction(trade: TradeEvent, makerOrder: Order, takerOrder: Order) + + suspend fun createTakerFeeAction(trade: TradeEvent, makerOrder: Order, takerOrder: Order) + +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/Order.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/Order.kt index 2298687b1..00498920b 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/Order.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/Order.kt @@ -1,31 +1,36 @@ -package co.nilin.opex.accountant.core.model - -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType -import java.math.BigDecimal - -data class Order( - val pair: String, - val ouid: String, - var matchingEngineId: Long?, - val makerFee: Double, - val takerFee: Double, - val leftSideFraction: Double, - val rightSideFraction: Double, - val uuid: String, - val userLevel: String, - val direction: OrderDirection, - val matchConstraint: MatchConstraint, - val orderType: OrderType, - val price: Long, - val quantity: Long, - val filledQuantity: Long, - val origPrice: BigDecimal, - val origQuantity: BigDecimal, - val filledOrigQuantity: BigDecimal, - val firstTransferAmount: BigDecimal, - var remainedTransferAmount: BigDecimal, - var status: Int, - val id: Long? = null -) \ No newline at end of file +package co.nilin.opex.accountant.core.model + +import co.nilin.opex.matching.engine.core.model.MatchConstraint +import co.nilin.opex.matching.engine.core.model.OrderDirection +import co.nilin.opex.matching.engine.core.model.OrderType +import java.math.BigDecimal + +data class Order( + val pair: String, + val ouid: String, + var matchingEngineId: Long?, + val makerFee: Double, + val takerFee: Double, + val leftSideFraction: Double, + val rightSideFraction: Double, + val uuid: String, + val userLevel: String, + val direction: OrderDirection, + val matchConstraint: MatchConstraint, + val orderType: OrderType, + val price: Long, + val quantity: Long, + val filledQuantity: Long, + val origPrice: BigDecimal, + val origQuantity: BigDecimal, + val filledOrigQuantity: BigDecimal, + val firstTransferAmount: BigDecimal, + var remainedTransferAmount: BigDecimal, + var status: Int, + val id: Long? = null +) { + + fun isAsk() = direction == OrderDirection.ASK + + fun isBid() = direction == OrderDirection.BID +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt new file mode 100644 index 000000000..7f973d64e --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt @@ -0,0 +1,145 @@ +package co.nilin.opex.accountant.core.service + +import co.nilin.opex.accountant.core.api.FeeCalculator +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.Order +import co.nilin.opex.accountant.core.spi.FinancialActionLoader +import co.nilin.opex.accountant.core.spi.FinancialActionPersister +import co.nilin.opex.accountant.core.spi.PairStaticRateLoader +import co.nilin.opex.accountant.core.spi.WalletProxy +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import org.slf4j.LoggerFactory +import java.math.BigDecimal +import java.time.LocalDateTime + +class FeeCalculatorImpl( + private val financeActionPersister: FinancialActionPersister, + private val financeActionLoader: FinancialActionLoader, + private val pairStaticRateLoader: PairStaticRateLoader, + private val walletProxy: WalletProxy, + private val platformCoin: String, + private val platformAddress: String +) : FeeCalculator { + + private val logger = LoggerFactory.getLogger(FeeCalculatorImpl::class.java) + + override suspend fun createMakerFeeAction(trade: TradeEvent, makerOrder: Order, takerOrder: Order) { + val actions = mutableListOf() + logger.info("Start fee calculation for trade ${trade.takerUuid}") + + // Look up parent financial actions + val makerParentFinancialAction = financeActionLoader.findLast(trade.makerUuid, trade.makerOuid) + val takerParentFinancialAction = financeActionLoader.findLast(trade.takerUuid, trade.takerOuid) + logger.info("Parent financial actions loaded") + + // TODO cache this + val leftSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.leftSideName) ?: 0.0 + val rightSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.rightSideName) ?: 0.0 + + val makerPCFeeCoefficient = if (makerOrder.isAsk()) leftSidePCRate else rightSidePCRate + val takerPCFeeCoefficient = if (takerOrder.isAsk()) leftSidePCRate else rightSidePCRate + + val makerMatchedAmount = if (makerOrder.isAsk()) { + trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) + } else { + trade.matchedQuantity.toBigDecimal() + .multiply(makerOrder.leftSideFraction.toBigDecimal()) + .multiply(trade.makerPrice.toBigDecimal()) + .multiply(makerOrder.rightSideFraction.toBigDecimal()) + } + + val takerMatchedAmount = if (takerOrder.isAsk()) { + trade.matchedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) + } else { + trade.matchedQuantity.toBigDecimal() + .multiply(takerOrder.leftSideFraction.toBigDecimal()) + .multiply(trade.makerPrice.toBigDecimal()) + .multiply(takerOrder.rightSideFraction.toBigDecimal()) + } + + //calculate maker fee + val makerFee = makerOrder.makerFee + val makerTotalFeeWithPlatformCoin = takerMatchedAmount + .multiply(makerFee.toBigDecimal()) + .multiply(makerPCFeeCoefficient.toBigDecimal()) + + //check if maker uuid can pay the fee with platform coin + val makerFeeAction = if (makerTotalFeeWithPlatformCoin > BigDecimal.ZERO && + walletProxy.canFulfil(platformCoin, "main", trade.makerUuid, makerTotalFeeWithPlatformCoin) + ) { + FinancialAction( + makerParentFinancialAction, + TradeEvent::class.simpleName!!, + trade.takerOuid, + platformCoin, + makerTotalFeeWithPlatformCoin, + trade.makerUuid, + "main", + platformAddress, + "exchange", + LocalDateTime.now() + ) + } else { + FinancialAction( + makerParentFinancialAction, + TradeEvent::class.simpleName!!, + trade.takerOuid, + if (takerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + takerMatchedAmount.multiply(makerFee.toBigDecimal()), + trade.makerUuid, + "main", + platformAddress, + "exchange", + LocalDateTime.now() + ) + } + logger.info("trade event makerFeeAction {}") + actions.add(makerFeeAction) + + //calculate taker fee + val takerFee = takerOrder.takerFee + val takerTotalFeeWithPlatformCoin = takerMatchedAmount + .multiply(takerFee.toBigDecimal()) + .multiply(takerPCFeeCoefficient.toBigDecimal()) + + //check if taker uuid can pay the fee with platform coin + val takerFeeAction = if (takerTotalFeeWithPlatformCoin > BigDecimal.ZERO && + walletProxy.canFulfil(platformCoin, "main", trade.takerUuid, takerTotalFeeWithPlatformCoin) + ) { + FinancialAction( + takerParentFinancialAction, + TradeEvent::class.simpleName!!, + trade.makerOuid, + if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + takerTotalFeeWithPlatformCoin, + trade.takerUuid, + "main", + platformAddress, + "", + LocalDateTime.now() + ) + } else { + FinancialAction( + takerParentFinancialAction, + TradeEvent::class.simpleName!!, + trade.makerOuid, + if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + makerMatchedAmount.multiply(takerFee.toBigDecimal()), + trade.takerUuid, + "main", + platformAddress, + "exchange", + LocalDateTime.now() + ) + + } + logger.info("trade event takerFeeAction $takerFeeAction") + actions.add(takerFeeAction) + + financeActionPersister.persist(actions) + } + + override suspend fun createTakerFeeAction(trade: TradeEvent, makerOrder: Order, takerOrder: Order) { + TODO("Not yet implemented") + } +} \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt index 9f237d7ca..fab203fe3 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt @@ -1,294 +1,176 @@ -package co.nilin.opex.accountant.core.service - -import co.nilin.opex.accountant.core.api.TradeManager -import co.nilin.opex.accountant.core.inout.OrderStatus -import co.nilin.opex.accountant.core.inout.RichOrderUpdate -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.Order -import co.nilin.opex.accountant.core.spi.* -import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent -import co.nilin.opex.matching.engine.core.model.OrderDirection -import org.slf4j.LoggerFactory -import org.springframework.transaction.annotation.Transactional -import java.math.BigDecimal -import java.time.LocalDateTime - -open class TradeManagerImpl( - private val pairStaticRateLoader: PairStaticRateLoader, - private val financeActionPersister: FinancialActionPersister, - private val financeActionLoader: FinancialActionLoader, - private val orderPersister: OrderPersister, - private val tempEventPersister: TempEventPersister, - private val richTradePublisher: RichTradePublisher, - private val richOrderPublisher: RichOrderPublisher, - private val walletProxy: WalletProxy, - private val platformCoin: String, - private val platformAddress: String -) : TradeManager { - - private val log = LoggerFactory.getLogger(TradeManagerImpl::class.java) - - @Transactional - override suspend fun handleTrade(trade: TradeEvent): List { - log.info("trade event started {}", trade) - val financialActions = mutableListOf() - //taker order by ouid - val takerOrder = orderPersister.load(trade.takerOuid) - //maker order by ouid - val makerOrder = orderPersister.load(trade.makerOuid) - if (takerOrder == null || makerOrder == null) { - if (takerOrder == null) { - tempEventPersister.saveTempEvent(trade.takerOuid, trade) - } - if (makerOrder == null) { - tempEventPersister.saveTempEvent(trade.makerOuid, trade) - } - return emptyList() - } - //check taker uuid - // - //check maker uuid - // - val leftSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.leftSideName) - val rightSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.rightSideName) - - val takerMatchedAmount = if (takerOrder.direction == OrderDirection.ASK) { - trade.matchedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) - } else { - trade.matchedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) - .multiply(trade.makerPrice.toBigDecimal()).multiply(takerOrder.rightSideFraction.toBigDecimal()) - } - - val takerPCFeeCoefficient: Double = if (takerOrder.direction == OrderDirection.ASK) { - leftSidePCRate ?: 0.0 - } else { - rightSidePCRate ?: 0.0 - } - - val makerMatchedAmount = if (makerOrder.direction == OrderDirection.ASK) { - trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) - } else { - trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) - .multiply(trade.makerPrice.toBigDecimal()).multiply(makerOrder.rightSideFraction.toBigDecimal()) - } - - val makerPCFeeCoefficient: Double = if (makerOrder.direction == OrderDirection.ASK) { - leftSidePCRate ?: 0.0 - } else { - rightSidePCRate ?: 0.0 - } - log.info("trade event configs loaded ") - //lookup for taker parent fa - val takerParentFinancialAction = financeActionLoader.findLast(trade.takerUuid, trade.takerOuid) - log.info("trade event takerParentFinancialAction {} ", takerParentFinancialAction) - //lookup for maker parent fa - val makerParentFinancialAction = financeActionLoader.findLast(trade.makerUuid, trade.makerOuid) - log.info("trade event makerParentFinancialAction {} ", makerParentFinancialAction) - - - //calculate maker fee - val makerFee = makerOrder.makerFee - val makerTotalFeeWithPlatformCoin = takerMatchedAmount - .multiply(makerFee.toBigDecimal()) - .multiply(makerPCFeeCoefficient.toBigDecimal()) - //check if maker uuid can pay the fee with platform coin - //create fa for transfer taker uuid symbol exchange wallet to maker symbol main wallet - /* - amount for sell (ask): match_quantity (if not pay by platform coin then - maker fee) - amount for buy (bid): match_quantity * maker price (if not pay by platform coin then - maker fee) - */ - val takerTransferAction = FinancialAction( - takerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.takerOuid, - if (takerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, - takerMatchedAmount, - trade.takerUuid, - "exchange", - trade.makerUuid, - "main", - LocalDateTime.now() - ) - log.info("trade event takerTransferAction {}", takerTransferAction) - financialActions.add(takerTransferAction) - val makerFeeAction = if (makerTotalFeeWithPlatformCoin > BigDecimal.ZERO && - walletProxy.canFulfil(platformCoin, "main", trade.makerUuid, makerTotalFeeWithPlatformCoin) - ) { - FinancialAction( - makerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.takerOuid, - platformCoin, - makerTotalFeeWithPlatformCoin, - trade.makerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - } else { - FinancialAction( - makerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.takerOuid, - if (takerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, - takerMatchedAmount.multiply(makerFee.toBigDecimal()), - trade.makerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - } - log.info("trade event makerFeeAction {}", makerFeeAction) - financialActions.add(makerFeeAction) - //update taker order status - takerOrder.remainedTransferAmount -= takerMatchedAmount - if (takerOrder.filledQuantity == takerOrder.quantity) { - takerOrder.status = 1 - } - orderPersister.save(takerOrder) - log.info("taker order saved {}", takerOrder) - publishTakerRichOrderUpdate(takerOrder, trade) - - //calculate taker fee - val takerFee = takerOrder.takerFee - val takerTotalFeeWithPlatformCoin = takerMatchedAmount - .multiply(takerFee.toBigDecimal()) - .multiply(takerPCFeeCoefficient.toBigDecimal()) - - //create fa for transfer makeruuid symbol exchange wallet to taker symbol main wallet - /* - amount for sell (ask): match_quantity (if not pay by platform coin then - taker fee) - amount for buy (bid): match_quantity * maker price (if not pay by platform coin then - taker fee) - */ - val makerTransferAction = FinancialAction( - makerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.makerOuid, - if (makerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, - makerMatchedAmount, - trade.makerUuid, - "exchange", - trade.takerUuid, - "main", - LocalDateTime.now() - ) - log.info("trade event makerTransferAction {}", makerTransferAction) - financialActions.add(makerTransferAction) - //check if taker uuid can pay the fee with platform coin - val takerFeeAction = if (takerTotalFeeWithPlatformCoin > BigDecimal.ZERO && - walletProxy.canFulfil(platformCoin, "main", trade.takerUuid, takerTotalFeeWithPlatformCoin) - ) { - FinancialAction( - takerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.makerOuid, - if (makerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, - takerTotalFeeWithPlatformCoin, - trade.takerUuid, - "main", - platformAddress, - "", - LocalDateTime.now() - ) - } else { - FinancialAction( - takerParentFinancialAction, - TradeEvent::class.simpleName!!, - trade.makerOuid, - if (makerOrder.direction == OrderDirection.ASK) { - trade.pair.leftSideName - } else { - trade.pair.rightSideName - }, - makerMatchedAmount.multiply(takerFee.toBigDecimal()), - trade.takerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - - } - log.info("trade event takerFeeAction {}", takerFeeAction) - financialActions.add(takerFeeAction) - //update maker order status - makerOrder.remainedTransferAmount -= makerMatchedAmount - if (makerOrder.filledQuantity == makerOrder.quantity) { - makerOrder.status = 1 - } - orderPersister.save(makerOrder) - log.info("maker order saved {}", makerOrder) - publishMakerRichOrderUpdate(makerOrder, trade) - - richTradePublisher.publish( - RichTrade( - trade.tradeId, - trade.pair.toString(), - trade.takerOuid, - trade.takerUuid, - trade.takerOrderId, - trade.takerDirection, - trade.takerPrice.toBigDecimal().multiply(takerOrder.rightSideFraction.toBigDecimal()), - takerOrder.origQuantity, - takerOrder.origPrice.multiply(takerOrder.origQuantity), - trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()), - takerFeeAction.amount, - takerFeeAction.symbol, - trade.makerOuid, - trade.makerUuid, - trade.makerOrderId, - trade.makerDirection, - trade.makerPrice.toBigDecimal().multiply(makerOrder.rightSideFraction.toBigDecimal()), - makerOrder.origQuantity, - makerOrder.origPrice.multiply(makerOrder.origQuantity), - trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()), - makerFeeAction.amount, - makerFeeAction.symbol, - trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()), - trade.eventDate - ) - - ) - return financeActionPersister.persist(financialActions) - } - - private suspend fun publishTakerRichOrderUpdate(takerOrder: Order, trade: TradeEvent) { - val price = trade.takerPrice.toBigDecimal().multiply(takerOrder.rightSideFraction.toBigDecimal()) - val remained = trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) - publishRichOrderUpdate(takerOrder, price, remained) - } - - private suspend fun publishMakerRichOrderUpdate(makerOrder: Order, trade: TradeEvent) { - val price = trade.makerPrice.toBigDecimal().multiply(makerOrder.rightSideFraction.toBigDecimal()) - val remained = trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) - publishRichOrderUpdate(makerOrder, price, remained) - } - - private suspend fun publishRichOrderUpdate(order: Order, price: BigDecimal, remainedQty: BigDecimal) { - val status = if (remainedQty.compareTo(BigDecimal.ZERO) == 0) - OrderStatus.FILLED - else - OrderStatus.PARTIALLY_FILLED - - richOrderPublisher.publish(RichOrderUpdate(order.ouid, price, order.origQuantity, remainedQty, status)) - } +package co.nilin.opex.accountant.core.service + +import co.nilin.opex.accountant.core.api.TradeManager +import co.nilin.opex.accountant.core.inout.OrderStatus +import co.nilin.opex.accountant.core.inout.RichOrderUpdate +import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.Order +import co.nilin.opex.accountant.core.spi.* +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import org.slf4j.LoggerFactory +import org.springframework.transaction.annotation.Transactional +import java.math.BigDecimal +import java.time.LocalDateTime + +open class TradeManagerImpl( + private val financeActionPersister: FinancialActionPersister, + private val financeActionLoader: FinancialActionLoader, + private val orderPersister: OrderPersister, + private val tempEventPersister: TempEventPersister, + private val richTradePublisher: RichTradePublisher, + private val richOrderPublisher: RichOrderPublisher, +) : TradeManager { + + private val log = LoggerFactory.getLogger(TradeManagerImpl::class.java) + + @Transactional + override suspend fun handleTrade(trade: TradeEvent): List { + log.info("trade event started {}", trade) + val financialActions = mutableListOf() + //taker order by ouid + val takerOrder = orderPersister.load(trade.takerOuid) + //maker order by ouid + val makerOrder = orderPersister.load(trade.makerOuid) + if (takerOrder == null || makerOrder == null) { + if (takerOrder == null) { + tempEventPersister.saveTempEvent(trade.takerOuid, trade) + } + if (makerOrder == null) { + tempEventPersister.saveTempEvent(trade.makerOuid, trade) + } + return emptyList() + } + + val takerMatchedAmount = if (takerOrder.isAsk()) { + trade.matchedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) + } else { + trade.matchedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) + .multiply(trade.makerPrice.toBigDecimal()).multiply(takerOrder.rightSideFraction.toBigDecimal()) + } + + val makerMatchedAmount = if (makerOrder.isAsk()) { + trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) + } else { + trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) + .multiply(trade.makerPrice.toBigDecimal()).multiply(makerOrder.rightSideFraction.toBigDecimal()) + } + log.info("trade event configs loaded") + + //lookup for taker parent fa + val takerParentFinancialAction = financeActionLoader.findLast(trade.takerUuid, trade.takerOuid) + log.info("trade event takerParentFinancialAction {} ", takerParentFinancialAction) + //lookup for maker parent fa + val makerParentFinancialAction = financeActionLoader.findLast(trade.makerUuid, trade.makerOuid) + log.info("trade event makerParentFinancialAction {} ", makerParentFinancialAction) + + //create fa for transfer taker uuid symbol exchange wallet to maker symbol main wallet + /* + amount for sell (ask): match_quantity (if not pay by platform coin then - maker fee) + amount for buy (bid): match_quantity * maker price (if not pay by platform coin then - maker fee) + */ + val takerTransferAction = FinancialAction( + takerParentFinancialAction, + TradeEvent::class.simpleName!!, + trade.takerOuid, + if (takerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + takerMatchedAmount, + trade.takerUuid, + "exchange", + trade.makerUuid, + "main", + LocalDateTime.now() + ) + log.info("trade event takerTransferAction {}", takerTransferAction) + financialActions.add(takerTransferAction) + + //update taker order status + takerOrder.remainedTransferAmount -= takerMatchedAmount + if (takerOrder.filledQuantity == takerOrder.quantity) { + takerOrder.status = 1 + } + orderPersister.save(takerOrder) + log.info("taker order saved {}", takerOrder) + publishTakerRichOrderUpdate(takerOrder, trade) + + //create fa for transfer makerUuid symbol exchange wallet to taker symbol main wallet + /* + amount for sell (ask): match_quantity (if not pay by platform coin then - taker fee) + amount for buy (bid): match_quantity * maker price (if not pay by platform coin then - taker fee) + */ + val makerTransferAction = FinancialAction( + makerParentFinancialAction, + TradeEvent::class.simpleName!!, + trade.makerOuid, + if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + makerMatchedAmount, + trade.makerUuid, + "exchange", + trade.takerUuid, + "main", + LocalDateTime.now() + ) + log.info("trade event makerTransferAction {}", makerTransferAction) + financialActions.add(makerTransferAction) + + //update maker order status + makerOrder.remainedTransferAmount -= makerMatchedAmount + if (makerOrder.filledQuantity == makerOrder.quantity) { + makerOrder.status = 1 + } + orderPersister.save(makerOrder) + log.info("maker order saved {}", makerOrder) + publishMakerRichOrderUpdate(makerOrder, trade) + + richTradePublisher.publish( + RichTrade( + trade.tradeId, + trade.pair.toString(), + trade.takerOuid, + trade.takerUuid, + trade.takerOrderId, + trade.takerDirection, + trade.takerPrice.toBigDecimal().multiply(takerOrder.rightSideFraction.toBigDecimal()), + takerOrder.origQuantity, + takerOrder.origPrice.multiply(takerOrder.origQuantity), + trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()), + takerFeeAction.amount, + takerFeeAction.symbol, + trade.makerOuid, + trade.makerUuid, + trade.makerOrderId, + trade.makerDirection, + trade.makerPrice.toBigDecimal().multiply(makerOrder.rightSideFraction.toBigDecimal()), + makerOrder.origQuantity, + makerOrder.origPrice.multiply(makerOrder.origQuantity), + trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()), + makerFeeAction.amount, + makerFeeAction.symbol, + trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()), + trade.eventDate + ) + ) + return financeActionPersister.persist(financialActions) + } + + private suspend fun publishTakerRichOrderUpdate(takerOrder: Order, trade: TradeEvent) { + val price = trade.takerPrice.toBigDecimal().multiply(takerOrder.rightSideFraction.toBigDecimal()) + val remained = trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()) + publishRichOrderUpdate(takerOrder, price, remained) + } + + private suspend fun publishMakerRichOrderUpdate(makerOrder: Order, trade: TradeEvent) { + val price = trade.makerPrice.toBigDecimal().multiply(makerOrder.rightSideFraction.toBigDecimal()) + val remained = trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) + publishRichOrderUpdate(makerOrder, price, remained) + } + + private suspend fun publishRichOrderUpdate(order: Order, price: BigDecimal, remainedQty: BigDecimal) { + val status = if (remainedQty.compareTo(BigDecimal.ZERO) == 0) + OrderStatus.FILLED + else + OrderStatus.PARTIALLY_FILLED + + richOrderPublisher.publish(RichOrderUpdate(order.ouid, price, order.origQuantity, remainedQty, status)) + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt index 69616db32..6a8ecf17a 100644 --- a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt @@ -1,71 +1,69 @@ -package co.nilin.opex.accountant.ports.walletproxy.proxy - -import co.nilin.opex.accountant.core.spi.WalletProxy -import kotlinx.coroutines.reactive.awaitFirst -import org.springframework.beans.factory.annotation.Value -import org.springframework.core.ParameterizedTypeReference -import org.springframework.stereotype.Component -import org.springframework.web.reactive.function.client.WebClient -import java.math.BigDecimal -import java.net.URI - -inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {} -data class TransferResult( - val date: Long, - val sourceUuid: String, - val sourceWalletType: String, - val sourceBalanceBeforeAction: Amount, - val sourceBalanceAfterAction: Amount, - val amount: Amount, - val destUuid: String, - val destWalletType: String, - val receivedAmount: Amount -) - -data class Amount(val currency: Currency, val amount: BigDecimal) -data class Currency(val name: String, val symbol: String, val precision: Int) - -@Component -class WalletProxyImpl( - @Value("\${app.wallet.url}") val walletBaseUrl: String, val webClient: WebClient -) : WalletProxy { - override suspend fun transfer( - symbol: String, - senderWalletType: String, - senderUuid: String, - receiverWalletType: String, - receiverUuid: String, - amount: BigDecimal, - description: String?, - transferRef: String? - ) { - webClient.post() - .uri(URI.create("$walletBaseUrl/transfer/${amount}_${symbol}/from/${senderUuid}_${senderWalletType}/to/${receiverUuid}_${receiverWalletType}")) - .header("Content-Type", "application/json") - .retrieve() - .onStatus({ t -> t.isError }, { p -> - /* - p.bodyToMono(typeRef>()).map { t -> KycSejamException(p.statusCode().value().toString(), t.error?.errorCode.toString() - + "-" + t.error?.customMessage) } - */ - throw RuntimeException() - }) - .bodyToMono(typeRef()) - .log() - .awaitFirst() - - } - - override suspend fun canFulfil(symbol: String, walletType: String, uuid: String, amount: BigDecimal): Boolean { - data class BooleanResponse(val result: Boolean) - return webClient.get() - .uri(URI.create("$walletBaseUrl/$uuid/wallet_type/${walletType}/can_withdraw/${amount}_${symbol}")) - .header("Content-Type", "application/json") - .retrieve() - .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToMono(typeRef()) - .log() - .awaitFirst() - .result - } +package co.nilin.opex.accountant.ports.walletproxy.proxy + +import co.nilin.opex.accountant.core.spi.WalletProxy +import kotlinx.coroutines.reactive.awaitFirst +import org.springframework.beans.factory.annotation.Value +import org.springframework.core.ParameterizedTypeReference +import org.springframework.stereotype.Component +import org.springframework.web.reactive.function.client.WebClient +import java.math.BigDecimal +import java.net.URI + +inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {} + +data class TransferResult( + val date: Long, + val sourceUuid: String, + val sourceWalletType: String, + val sourceBalanceBeforeAction: Amount, + val sourceBalanceAfterAction: Amount, + val amount: Amount, + val destUuid: String, + val destWalletType: String, + val receivedAmount: Amount +) + +data class Amount(val currency: Currency, val amount: BigDecimal) + +data class Currency(val name: String, val symbol: String, val precision: Int) + +@Component +class WalletProxyImpl( + @Value("\${app.wallet.url}") val walletBaseUrl: String, + val webClient: WebClient +) : WalletProxy { + + override suspend fun transfer( + symbol: String, + senderWalletType: String, + senderUuid: String, + receiverWalletType: String, + receiverUuid: String, + amount: BigDecimal, + description: String?, + transferRef: String? + ) { + webClient.post() + .uri(URI.create("$walletBaseUrl/transfer/${amount}_${symbol}/from/${senderUuid}_${senderWalletType}/to/${receiverUuid}_${receiverWalletType}")) + .header("Content-Type", "application/json") + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToMono(typeRef()) + .log() + .awaitFirst() + + } + + override suspend fun canFulfil(symbol: String, walletType: String, uuid: String, amount: BigDecimal): Boolean { + data class BooleanResponse(val result: Boolean) + return webClient.get() + .uri(URI.create("$walletBaseUrl/$uuid/wallet_type/${walletType}/can_withdraw/${amount}_${symbol}")) + .header("Content-Type", "application/json") + .retrieve() + .onStatus({ t -> t.isError }, { it.createException() }) + .bodyToMono(typeRef()) + .log() + .awaitFirst() + .result + } } \ No newline at end of file From 07ade3d11d7506ba61d9f28943597d0f29dbb624 Mon Sep 17 00:00:00 2001 From: Peyman Date: Tue, 10 May 2022 17:55:05 +0430 Subject: [PATCH 2/9] Adding retry for FA --- .gitignore | 1 + .../opex/accountant/app/config/AppConfig.kt | 27 +- .../app/controller/AccountantController.kt | 99 ++-- .../app/listener/AccountantTradeListener.kt | 37 +- .../app/scheduler/FinancialActionsJob.kt | 57 +- .../opex/accountant/core/api/FeeCalculator.kt | 12 +- .../core/model/FeeFinancialActions.kt | 6 + .../core/service/FeeCalculatorImpl.kt | 56 +- .../service/FinancialActionJobManagerImpl.kt | 23 +- .../core/service/TradeManagerImpl.kt | 21 +- .../core/spi/FinancialActionPersister.kt | 18 +- .../core/service/TradeManagerImplTest.kt | 540 +++++++++--------- 12 files changed, 456 insertions(+), 441 deletions(-) create mode 100644 accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeFinancialActions.kt diff --git a/.gitignore b/.gitignore index 673649625..19d7407a4 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ target/ ### Other files ### .env docker-compose.local.yml +docker-compose.legacy.yml application-local.yml mvnw mvnw.cmd diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt index 1faf9c6cf..05eb0ba44 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/AppConfig.kt @@ -8,7 +8,6 @@ import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.api.FinancialActionJobManager import co.nilin.opex.accountant.core.api.OrderManager import co.nilin.opex.accountant.core.api.TradeManager -import co.nilin.opex.accountant.core.service.FeeCalculatorImpl import co.nilin.opex.accountant.core.service.FinancialActionJobManagerImpl import co.nilin.opex.accountant.core.service.OrderManagerImpl import co.nilin.opex.accountant.core.service.TradeManagerImpl @@ -18,7 +17,6 @@ import co.nilin.opex.accountant.ports.kafka.listener.consumer.OrderKafkaListener import co.nilin.opex.accountant.ports.kafka.listener.consumer.TempEventKafkaListener import co.nilin.opex.accountant.ports.kafka.listener.consumer.TradeKafkaListener import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.scheduling.annotation.EnableScheduling @@ -68,7 +66,8 @@ class AppConfig { orderPersister: OrderPersister, tempEventPersister: TempEventPersister, richTradePublisher: RichTradePublisher, - richOrderPublisher: RichOrderPublisher + richOrderPublisher: RichOrderPublisher, + feeCalculator: FeeCalculator, ): TradeManager { return TradeManagerImpl( financeActionPersister, @@ -76,26 +75,8 @@ class AppConfig { orderPersister, tempEventPersister, richTradePublisher, - richOrderPublisher - ) - } - - @Bean - fun feeCalculator( - financeActionPersister: FinancialActionPersister, - financeActionLoader: FinancialActionLoader, - pairStaticRateLoader: PairStaticRateLoader, - walletProxy: WalletProxy, - @Value("\${app.coin}") platformCoin: String, - @Value("\${app.address}") platformAddress: String - ): FeeCalculator { - return FeeCalculatorImpl( - financeActionPersister, - financeActionLoader, - pairStaticRateLoader, - walletProxy, - platformCoin, - platformAddress + richOrderPublisher, + feeCalculator ) } diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt index 3ce8cc282..dacc694cf 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt @@ -1,51 +1,50 @@ -package co.nilin.opex.accountant.app.controller - -import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig -import co.nilin.opex.accountant.core.spi.FinancialActionLoader -import co.nilin.opex.accountant.core.spi.PairConfigLoader -import co.nilin.opex.accountant.core.spi.WalletProxy -import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent -import co.nilin.opex.matching.engine.core.model.OrderDirection -import org.springframework.web.bind.annotation.GetMapping -import org.springframework.web.bind.annotation.PathVariable -import org.springframework.web.bind.annotation.RestController -import java.math.BigDecimal - -@RestController -class AccountantController( - val walletProxy: WalletProxy, - val financialActionLoader: FinancialActionLoader, - val pairConfigLoader: PairConfigLoader -) { - data class BooleanResponse(val result: Boolean) - - @GetMapping("{uuid}/create_order/{amount}_{currency}/allowed") - suspend fun canCreateOrder( - @PathVariable("uuid") uuid: String, - @PathVariable("currency") currency: String, - @PathVariable("amount") amount: BigDecimal - ): BooleanResponse { - return BooleanResponse( - financialActionLoader.countUnprocessed(uuid, currency, SubmitOrderEvent::class.simpleName!!) <= 0 - && walletProxy.canFulfil(currency, "main", uuid, amount) - ) - } - - @GetMapping( - value = ["/config/{pair}/fee/{direction}-{userLevel}", "/config/{pair}/fee/{direction}"] - ) - suspend fun fetchPairFeeConfig( - @PathVariable("pair") pair: String, - @PathVariable("direction") direction: OrderDirection, - @PathVariable("userLevel") level: String? - ): PairFeeConfig { - return pairConfigLoader.load(pair, direction, level ?: "") - } - - @GetMapping("/config/all") - suspend fun fetchPairConfigs(): List { - return pairConfigLoader.loadPairConfigs() - } - +package co.nilin.opex.accountant.app.controller + +import co.nilin.opex.accountant.core.model.PairConfig +import co.nilin.opex.accountant.core.model.PairFeeConfig +import co.nilin.opex.accountant.core.spi.FinancialActionLoader +import co.nilin.opex.accountant.core.spi.PairConfigLoader +import co.nilin.opex.accountant.core.spi.WalletProxy +import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent +import co.nilin.opex.matching.engine.core.model.OrderDirection +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.RestController +import java.math.BigDecimal + +@RestController +class AccountantController( + val walletProxy: WalletProxy, + val financialActionLoader: FinancialActionLoader, + val pairConfigLoader: PairConfigLoader +) { + + data class BooleanResponse(val result: Boolean) + + @GetMapping("{uuid}/create_order/{amount}_{currency}/allowed") + suspend fun canCreateOrder( + @PathVariable("uuid") uuid: String, + @PathVariable("currency") currency: String, + @PathVariable("amount") amount: BigDecimal + ): BooleanResponse { + return BooleanResponse( + financialActionLoader.countUnprocessed(uuid, currency, SubmitOrderEvent::class.simpleName!!) <= 0 + && walletProxy.canFulfil(currency, "main", uuid, amount) + ) + } + + @GetMapping(value = ["/config/{pair}/fee/{direction}-{userLevel}", "/config/{pair}/fee/{direction}"]) + suspend fun fetchPairFeeConfig( + @PathVariable("pair") pair: String, + @PathVariable("direction") direction: OrderDirection, + @PathVariable("userLevel") level: String? + ): PairFeeConfig { + return pairConfigLoader.load(pair, direction, level ?: "") + } + + @GetMapping("/config/all") + suspend fun fetchPairConfigs(): List { + return pairConfigLoader.loadPairConfigs() + } + } \ No newline at end of file diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/AccountantTradeListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/AccountantTradeListener.kt index 829e6c88d..605798a36 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/AccountantTradeListener.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/AccountantTradeListener.kt @@ -1,19 +1,20 @@ -package co.nilin.opex.accountant.app.listener - -import co.nilin.opex.accountant.core.api.TradeManager -import co.nilin.opex.accountant.ports.kafka.listener.spi.TradeListener -import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent -import kotlinx.coroutines.runBlocking - -class AccountantTradeListener(private val tradeManager: TradeManager) : TradeListener { - - override fun id(): String { - return "TradeListener" - } - - override fun onTrade(tradeEvent: TradeEvent, partition: Int, offset: Long, timestamp: Long) { - runBlocking { - tradeManager.handleTrade(tradeEvent) - } - } +package co.nilin.opex.accountant.app.listener + +import co.nilin.opex.accountant.core.api.TradeManager +import co.nilin.opex.accountant.ports.kafka.listener.spi.TradeListener +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking + +class AccountantTradeListener(private val tradeManager: TradeManager) : TradeListener { + + override fun id(): String { + return "TradeListener" + } + + override fun onTrade(tradeEvent: TradeEvent, partition: Int, offset: Long, timestamp: Long) { + runBlocking { + tradeManager.handleTrade(tradeEvent) + } + } } \ No newline at end of file diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/FinancialActionsJob.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/FinancialActionsJob.kt index b8b1a73db..dc7699b13 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/FinancialActionsJob.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/scheduler/FinancialActionsJob.kt @@ -1,30 +1,29 @@ -package co.nilin.opex.accountant.app.scheduler - -import co.nilin.opex.accountant.core.api.FinancialActionJobManager -import kotlinx.coroutines.runBlocking -import org.slf4j.LoggerFactory -import org.springframework.context.annotation.Profile -import org.springframework.scheduling.annotation.Scheduled -import org.springframework.stereotype.Service - -@Service -@Profile("scheduled") -class FinancialActionsJob( - val financialActionJobManager: FinancialActionJobManager -) { - - private val log = LoggerFactory.getLogger(FinancialActionsJob::class.java) - - @Scheduled(fixedDelay = 10000) - fun processFinancialActions() { - runBlocking { - try { - //read unprocessed fa records and call transfer - financialActionJobManager.processFinancialActions(0, 100) - } catch (e: Exception) { - log.error("Job error!", e) - } - } - } - +package co.nilin.opex.accountant.app.scheduler + +import co.nilin.opex.accountant.core.api.FinancialActionJobManager +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Profile +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Service + +@Service +@Profile("scheduled") +class FinancialActionsJob(val financialActionJobManager: FinancialActionJobManager) { + + private val log = LoggerFactory.getLogger(FinancialActionsJob::class.java) + + @Scheduled(fixedDelay = 10000) + fun processFinancialActions() { + runBlocking(Dispatchers.IO) { + try { + //read unprocessed fa records and call transfer + financialActionJobManager.processFinancialActions(0, 100) + } catch (e: Exception) { + log.error("Job error!", e) + } + } + } + } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt index b13a52c3b..b650e29e6 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/api/FeeCalculator.kt @@ -1,12 +1,18 @@ package co.nilin.opex.accountant.core.api +import co.nilin.opex.accountant.core.model.FeeFinancialActions +import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.Order import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent interface FeeCalculator { - suspend fun createMakerFeeAction(trade: TradeEvent, makerOrder: Order, takerOrder: Order) - - suspend fun createTakerFeeAction(trade: TradeEvent, makerOrder: Order, takerOrder: Order) + suspend fun createFeeActions( + trade: TradeEvent, + makerOrder: Order, + takerOrder: Order, + makerParentFA: FinancialAction?, + takerParentFA: FinancialAction? + ): FeeFinancialActions } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeFinancialActions.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeFinancialActions.kt new file mode 100644 index 000000000..078496757 --- /dev/null +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FeeFinancialActions.kt @@ -0,0 +1,6 @@ +package co.nilin.opex.accountant.core.model + +data class FeeFinancialActions( + val makerFeeAction: FinancialAction, + val takerFeeAction: FinancialAction +) \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt index 7f973d64e..e054f2b58 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt @@ -1,37 +1,37 @@ package co.nilin.opex.accountant.core.service import co.nilin.opex.accountant.core.api.FeeCalculator +import co.nilin.opex.accountant.core.model.FeeFinancialActions import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.Order -import co.nilin.opex.accountant.core.spi.FinancialActionLoader -import co.nilin.opex.accountant.core.spi.FinancialActionPersister import co.nilin.opex.accountant.core.spi.PairStaticRateLoader import co.nilin.opex.accountant.core.spi.WalletProxy import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Component import java.math.BigDecimal import java.time.LocalDateTime +@Component class FeeCalculatorImpl( - private val financeActionPersister: FinancialActionPersister, - private val financeActionLoader: FinancialActionLoader, private val pairStaticRateLoader: PairStaticRateLoader, private val walletProxy: WalletProxy, - private val platformCoin: String, - private val platformAddress: String + @Value("\${app.coin}") private val platformCoin: String, + @Value("\${app.address}") private val platformAddress: String ) : FeeCalculator { private val logger = LoggerFactory.getLogger(FeeCalculatorImpl::class.java) - override suspend fun createMakerFeeAction(trade: TradeEvent, makerOrder: Order, takerOrder: Order) { - val actions = mutableListOf() + override suspend fun createFeeActions( + trade: TradeEvent, + makerOrder: Order, + takerOrder: Order, + makerParentFA: FinancialAction?, + takerParentFA: FinancialAction? + ): FeeFinancialActions { logger.info("Start fee calculation for trade ${trade.takerUuid}") - // Look up parent financial actions - val makerParentFinancialAction = financeActionLoader.findLast(trade.makerUuid, trade.makerOuid) - val takerParentFinancialAction = financeActionLoader.findLast(trade.takerUuid, trade.takerOuid) - logger.info("Parent financial actions loaded") - // TODO cache this val leftSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.leftSideName) ?: 0.0 val rightSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.rightSideName) ?: 0.0 @@ -64,11 +64,13 @@ class FeeCalculatorImpl( .multiply(makerPCFeeCoefficient.toBigDecimal()) //check if maker uuid can pay the fee with platform coin - val makerFeeAction = if (makerTotalFeeWithPlatformCoin > BigDecimal.ZERO && + val canMakerFulfil = runCatching { walletProxy.canFulfil(platformCoin, "main", trade.makerUuid, makerTotalFeeWithPlatformCoin) - ) { + }.getOrElse { false } + + val makerFeeAction = if (makerTotalFeeWithPlatformCoin > BigDecimal.ZERO && canMakerFulfil) { FinancialAction( - makerParentFinancialAction, + makerParentFA, TradeEvent::class.simpleName!!, trade.takerOuid, platformCoin, @@ -81,7 +83,7 @@ class FeeCalculatorImpl( ) } else { FinancialAction( - makerParentFinancialAction, + makerParentFA, TradeEvent::class.simpleName!!, trade.takerOuid, if (takerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, @@ -93,8 +95,7 @@ class FeeCalculatorImpl( LocalDateTime.now() ) } - logger.info("trade event makerFeeAction {}") - actions.add(makerFeeAction) + logger.info("trade event makerFeeAction $makerFeeAction") //calculate taker fee val takerFee = takerOrder.takerFee @@ -103,11 +104,13 @@ class FeeCalculatorImpl( .multiply(takerPCFeeCoefficient.toBigDecimal()) //check if taker uuid can pay the fee with platform coin - val takerFeeAction = if (takerTotalFeeWithPlatformCoin > BigDecimal.ZERO && + val canTakerFulfil = runCatching { walletProxy.canFulfil(platformCoin, "main", trade.takerUuid, takerTotalFeeWithPlatformCoin) - ) { + }.getOrElse { false } + + val takerFeeAction = if (takerTotalFeeWithPlatformCoin > BigDecimal.ZERO && canTakerFulfil) { FinancialAction( - takerParentFinancialAction, + takerParentFA, TradeEvent::class.simpleName!!, trade.makerOuid, if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, @@ -120,7 +123,7 @@ class FeeCalculatorImpl( ) } else { FinancialAction( - takerParentFinancialAction, + takerParentFA, TradeEvent::class.simpleName!!, trade.makerOuid, if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, @@ -134,12 +137,7 @@ class FeeCalculatorImpl( } logger.info("trade event takerFeeAction $takerFeeAction") - actions.add(takerFeeAction) - - financeActionPersister.persist(actions) - } - override suspend fun createTakerFeeAction(trade: TradeEvent, makerOrder: Order, takerOrder: Order) { - TODO("Not yet implemented") + return FeeFinancialActions(makerFeeAction, takerFeeAction) } } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt index 7ebcc5ea5..b6ca7a3a7 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt @@ -13,26 +13,27 @@ class FinancialActionJobManagerImpl( private val walletProxy: WalletProxy ) : FinancialActionJobManager { + private val retryLimit = 10 private val log = LoggerFactory.getLogger(FinancialActionJobManagerImpl::class.java) override suspend fun processFinancialActions(offset: Long, size: Long) { val factions = financialActionLoader.loadUnprocessed(offset, size) - factions.forEach { faction -> + factions.forEach { try { walletProxy.transfer( - faction.symbol, - faction.senderWalletType, - faction.sender, - faction.receiverWalletType, - faction.receiver, - faction.amount, - faction.eventType + faction.pointer, - null + it.symbol, + it.senderWalletType, + it.sender, + it.receiverWalletType, + it.receiver, + it.amount, + it.eventType + it.pointer, + "fa${it.id}" ) - financialActionPersister.updateStatus(faction, FinancialActionStatus.PROCESSED) + financialActionPersister.updateStatus(it, FinancialActionStatus.PROCESSED) } catch (e: Exception) { log.error("financial job error", e) - financialActionPersister.updateStatus(faction, FinancialActionStatus.ERROR) + financialActionPersister.updateStatus(it, FinancialActionStatus.ERROR) } } } diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt index fab203fe3..365d64a37 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImpl.kt @@ -1,5 +1,6 @@ package co.nilin.opex.accountant.core.service +import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.api.TradeManager import co.nilin.opex.accountant.core.inout.OrderStatus import co.nilin.opex.accountant.core.inout.RichOrderUpdate @@ -20,6 +21,7 @@ open class TradeManagerImpl( private val tempEventPersister: TempEventPersister, private val richTradePublisher: RichTradePublisher, private val richOrderPublisher: RichOrderPublisher, + private val feeCalculator: FeeCalculator ) : TradeManager { private val log = LoggerFactory.getLogger(TradeManagerImpl::class.java) @@ -122,6 +124,17 @@ open class TradeManagerImpl( log.info("maker order saved {}", makerOrder) publishMakerRichOrderUpdate(makerOrder, trade) + val feeActions = feeCalculator.createFeeActions( + trade, + makerOrder, + takerOrder, + makerParentFinancialAction, + takerParentFinancialAction + ).apply { + financialActions.add(makerFeeAction) + financialActions.add(takerFeeAction) + } + richTradePublisher.publish( RichTrade( trade.tradeId, @@ -134,8 +147,8 @@ open class TradeManagerImpl( takerOrder.origQuantity, takerOrder.origPrice.multiply(takerOrder.origQuantity), trade.takerRemainedQuantity.toBigDecimal().multiply(takerOrder.leftSideFraction.toBigDecimal()), - takerFeeAction.amount, - takerFeeAction.symbol, + feeActions.takerFeeAction.amount, + feeActions.takerFeeAction.symbol, trade.makerOuid, trade.makerUuid, trade.makerOrderId, @@ -144,8 +157,8 @@ open class TradeManagerImpl( makerOrder.origQuantity, makerOrder.origPrice.multiply(makerOrder.origQuantity), trade.makerRemainedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()), - makerFeeAction.amount, - makerFeeAction.symbol, + feeActions.makerFeeAction.amount, + feeActions.makerFeeAction.symbol, trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()), trade.eventDate ) diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt index f51e4eef9..5a3d54c69 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/spi/FinancialActionPersister.kt @@ -1,9 +1,11 @@ -package co.nilin.opex.accountant.core.spi - -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.FinancialActionStatus - -interface FinancialActionPersister { - suspend fun persist(financialActions: List): List - suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) +package co.nilin.opex.accountant.core.spi + +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.FinancialActionStatus + +interface FinancialActionPersister { + + suspend fun persist(financialActions: List): List + + suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) } \ No newline at end of file diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt index e9d638ece..62db326b4 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt @@ -1,267 +1,275 @@ -package co.nilin.opex.accountant.core.service - -import co.nilin.opex.accountant.core.api.OrderManager -import co.nilin.opex.accountant.core.api.TradeManager -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.Order -import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig -import co.nilin.opex.accountant.core.spi.* -import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent -import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType -import co.nilin.opex.matching.engine.core.model.Pair -import kotlinx.coroutines.runBlocking -import org.junit.jupiter.api.Assertions -import org.junit.jupiter.api.Test -import org.mockito.ArgumentMatchers -import org.mockito.Mock -import org.mockito.Mockito -import org.mockito.MockitoAnnotations - -internal class TradeManagerImplTest() { - @Mock - lateinit var financialActionPersister: FinancialActionPersister - - @Mock - lateinit var financeActionLoader: FinancialActionLoader - - @Mock - lateinit var orderPersister: OrderPersister - - @Mock - lateinit var pairConfigLoader: PairConfigLoader - - @Mock - lateinit var pairStaticRateLoader: PairStaticRateLoader - - @Mock - lateinit var walletProxy: WalletProxy - - @Mock - lateinit var tempEventPersister: TempEventPersister - - @Mock - lateinit var tempEventRepublisher: TempEventRepublisher - - @Mock - lateinit var richOrderPublisher: RichOrderPublisher - - @Mock - lateinit var richTradePublisher: RichTradePublisher - - val orderManager: OrderManager - - val tradeManager: TradeManager - - - init { - MockitoAnnotations.openMocks(this) - orderManager = OrderManagerImpl( - pairConfigLoader, - financialActionPersister, - financeActionLoader, - orderPersister, - tempEventPersister, - tempEventRepublisher, - richOrderPublisher - ) - tradeManager = TradeManagerImpl( - pairStaticRateLoader, - financialActionPersister, - financeActionLoader, - orderPersister, - tempEventPersister, - richTradePublisher, - richOrderPublisher, - walletProxy, - "pcoin", - "0x0" - ) - runBlocking { - Mockito.`when`(tempEventPersister.loadTempEvents(ArgumentMatchers.anyString())).thenReturn(emptyList()) - } - } - - @Test - fun givenSellOrder_WhenMatchBuyOrderCome_thenFAMatched() { - runBlocking { - //given - val pair = Pair("eth", "btc") - val pairConfig = PairConfig( - pair.toString(), pair.leftSideName, pair.rightSideName, 1.0, 0.01 - ) - val makerSubmitOrderEvent = SubmitOrderEvent( - "mouid", - "muuid", - null, - pair, - 60000, - 1, - 0, - OrderDirection.ASK, - MatchConstraint.GTC, - OrderType.LIMIT_ORDER - ) - prepareOrder(pair, pairConfig, makerSubmitOrderEvent, 0.1, 0.12) - - val takerSubmitOrderEvent = SubmitOrderEvent( - "touid", - "tuuid", - null, - pair, - 70000, - 1, - 0, - OrderDirection.BID, - MatchConstraint.GTC, - OrderType.LIMIT_ORDER - ) - - prepareOrder(pair, pairConfig, takerSubmitOrderEvent, 0.08, 0.1) - - val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent) - //when - val tradeFinancialActions = tradeManager.handleTrade(tradeEvent) - - Assertions.assertEquals(4, tradeFinancialActions.size) - Assertions.assertEquals( - (makerSubmitOrderEvent.price * pairConfig.rightSideFraction), - tradeFinancialActions[0].amount.toDouble() - ) - } - } - - @Test - fun givenBuyOrder_WhenMatchSellOrderCome_thenFAMatched() { - runBlocking { - //given - val pair = Pair("eth", "btc") - val pairConfig = PairConfig( - pair.toString(), pair.leftSideName, pair.rightSideName, 1.0, 0.001 - ) - val makerSubmitOrderEvent = SubmitOrderEvent( - "mouid", - "muuid", - null, - pair, - 70000, - 1, - 0, - OrderDirection.BID, - MatchConstraint.GTC, - OrderType.LIMIT_ORDER - ) - prepareOrder(pair, pairConfig, makerSubmitOrderEvent, 0.1, 0.12) - - val takerSubmitOrderEvent = SubmitOrderEvent( - "touid", - "tuuid", - null, - pair, - 60000, - 1, - 0, - OrderDirection.ASK, - MatchConstraint.GTC, - OrderType.LIMIT_ORDER - ) - - prepareOrder(pair, pairConfig, takerSubmitOrderEvent, 0.08, 0.1) - - val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent) - //when - val tradeFinancialActions = tradeManager.handleTrade(tradeEvent) - - Assertions.assertEquals(4, tradeFinancialActions.size) - Assertions.assertEquals( - makerSubmitOrderEvent.price * pairConfig.rightSideFraction, - tradeFinancialActions[2].amount.toDouble() - ) - } - } - - private fun makeTradeEvent( - pair: Pair, - takerSubmitOrderEvent: SubmitOrderEvent, - makerSubmitOrderEvent: SubmitOrderEvent - ): TradeEvent { - val tradeEvent = TradeEvent( - 0, - pair, - takerSubmitOrderEvent.ouid, - takerSubmitOrderEvent.uuid, - takerSubmitOrderEvent.orderId ?: -1, - takerSubmitOrderEvent.direction, - takerSubmitOrderEvent.price, - 0, - makerSubmitOrderEvent.ouid, - makerSubmitOrderEvent.uuid, - makerSubmitOrderEvent.orderId ?: 1, - makerSubmitOrderEvent.direction, - makerSubmitOrderEvent.price, - makerSubmitOrderEvent.quantity - takerSubmitOrderEvent.quantity, - takerSubmitOrderEvent.quantity - ) - return tradeEvent - } - - private fun prepareOrder( - pair: Pair, - pairConfig: PairConfig, - submitOrderEvent: SubmitOrderEvent, - makerFee: Double, - takerFee: Double - ) { - runBlocking { - Mockito.`when`(pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, "")) - .thenReturn( - PairFeeConfig( - pairConfig, - submitOrderEvent.direction.toString(), - "", - makerFee, - takerFee - ) - ) - Mockito.`when`(financialActionPersister.persist(MockitoHelper.anyObject())) - .then { - return@then it.getArgument>(0) - } - - val financialActions = orderManager.handleRequestOrder(submitOrderEvent) - - val orderPairFeeConfig = - pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction, "") - val orderMakerFee = orderPairFeeConfig.makerFee * 1 //user level formula - val orderTakerFee = orderPairFeeConfig.takerFee * 1 //user level formula - Mockito.`when`(orderPersister.load(submitOrderEvent.ouid)).thenReturn( - Order( - submitOrderEvent.pair.toString(), - submitOrderEvent.ouid, - null, - orderMakerFee, - orderTakerFee, - orderPairFeeConfig.pairConfig.leftSideFraction, - orderPairFeeConfig.pairConfig.rightSideFraction, - submitOrderEvent.uuid, - "", - submitOrderEvent.direction, - submitOrderEvent.matchConstraint, - submitOrderEvent.orderType, - submitOrderEvent.price, - submitOrderEvent.quantity, - submitOrderEvent.quantity - submitOrderEvent.remainedQuantity, - submitOrderEvent.price.toBigDecimal(), - submitOrderEvent.quantity.toBigDecimal(), - (submitOrderEvent.quantity - submitOrderEvent.remainedQuantity).toBigDecimal(), - financialActions[0].amount, - financialActions[0].amount, - 0 - ) - ) - } - } +package co.nilin.opex.accountant.core.service + +import co.nilin.opex.accountant.core.api.FeeCalculator +import co.nilin.opex.accountant.core.api.OrderManager +import co.nilin.opex.accountant.core.api.TradeManager +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.Order +import co.nilin.opex.accountant.core.model.PairConfig +import co.nilin.opex.accountant.core.model.PairFeeConfig +import co.nilin.opex.accountant.core.spi.* +import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent +import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent +import co.nilin.opex.matching.engine.core.model.MatchConstraint +import co.nilin.opex.matching.engine.core.model.OrderDirection +import co.nilin.opex.matching.engine.core.model.OrderType +import co.nilin.opex.matching.engine.core.model.Pair +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +internal class TradeManagerImplTest { + + @Mock + lateinit var financialActionPersister: FinancialActionPersister + + @Mock + lateinit var financeActionLoader: FinancialActionLoader + + @Mock + lateinit var orderPersister: OrderPersister + + @Mock + lateinit var pairConfigLoader: PairConfigLoader + + @Mock + lateinit var pairStaticRateLoader: PairStaticRateLoader + + @Mock + lateinit var walletProxy: WalletProxy + + @Mock + lateinit var tempEventPersister: TempEventPersister + + @Mock + lateinit var tempEventRepublisher: TempEventRepublisher + + @Mock + lateinit var richOrderPublisher: RichOrderPublisher + + @Mock + lateinit var richTradePublisher: RichTradePublisher + + private val orderManager: OrderManager + private val tradeManager: TradeManager + private val feeCalculator: FeeCalculator + + init { + MockitoAnnotations.openMocks(this) + + orderManager = OrderManagerImpl( + pairConfigLoader, + financialActionPersister, + financeActionLoader, + orderPersister, + tempEventPersister, + tempEventRepublisher, + richOrderPublisher + ) + + feeCalculator = FeeCalculatorImpl( + financeActionLoader, + pairStaticRateLoader, + walletProxy, + "pcoin", + "0x0" + ) + + tradeManager = TradeManagerImpl( + financialActionPersister, + financeActionLoader, + orderPersister, + tempEventPersister, + richTradePublisher, + richOrderPublisher, + feeCalculator + ) + runBlocking { + Mockito.`when`(tempEventPersister.loadTempEvents(ArgumentMatchers.anyString())).thenReturn(emptyList()) + } + } + + @Test + fun givenSellOrder_WhenMatchBuyOrderCome_thenFAMatched() { + runBlocking { + //given + val pair = Pair("eth", "btc") + val pairConfig = PairConfig( + pair.toString(), pair.leftSideName, pair.rightSideName, 1.0, 0.01 + ) + val makerSubmitOrderEvent = SubmitOrderEvent( + "mouid", + "muuid", + null, + pair, + 60000, + 1, + 0, + OrderDirection.ASK, + MatchConstraint.GTC, + OrderType.LIMIT_ORDER + ) + prepareOrder(pair, pairConfig, makerSubmitOrderEvent, 0.1, 0.12) + + val takerSubmitOrderEvent = SubmitOrderEvent( + "touid", + "tuuid", + null, + pair, + 70000, + 1, + 0, + OrderDirection.BID, + MatchConstraint.GTC, + OrderType.LIMIT_ORDER + ) + + prepareOrder(pair, pairConfig, takerSubmitOrderEvent, 0.08, 0.1) + + val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent) + //when + val tradeFinancialActions = tradeManager.handleTrade(tradeEvent) + + Assertions.assertEquals(4, tradeFinancialActions.size) + Assertions.assertEquals( + (makerSubmitOrderEvent.price * pairConfig.rightSideFraction), + tradeFinancialActions[0].amount.toDouble() + ) + } + } + + @Test + fun givenBuyOrder_WhenMatchSellOrderCome_thenFAMatched() { + runBlocking { + //given + val pair = Pair("eth", "btc") + val pairConfig = PairConfig( + pair.toString(), pair.leftSideName, pair.rightSideName, 1.0, 0.001 + ) + val makerSubmitOrderEvent = SubmitOrderEvent( + "mouid", + "muuid", + null, + pair, + 70000, + 1, + 0, + OrderDirection.BID, + MatchConstraint.GTC, + OrderType.LIMIT_ORDER + ) + prepareOrder(pair, pairConfig, makerSubmitOrderEvent, 0.1, 0.12) + + val takerSubmitOrderEvent = SubmitOrderEvent( + "touid", + "tuuid", + null, + pair, + 60000, + 1, + 0, + OrderDirection.ASK, + MatchConstraint.GTC, + OrderType.LIMIT_ORDER + ) + + prepareOrder(pair, pairConfig, takerSubmitOrderEvent, 0.08, 0.1) + + val tradeEvent = makeTradeEvent(pair, takerSubmitOrderEvent, makerSubmitOrderEvent) + //when + val tradeFinancialActions = tradeManager.handleTrade(tradeEvent) + + Assertions.assertEquals(4, tradeFinancialActions.size) + Assertions.assertEquals( + makerSubmitOrderEvent.price * pairConfig.rightSideFraction, + tradeFinancialActions[1].amount.toDouble() + ) + } + } + + private fun makeTradeEvent( + pair: Pair, + takerSubmitOrderEvent: SubmitOrderEvent, + makerSubmitOrderEvent: SubmitOrderEvent + ): TradeEvent { + val tradeEvent = TradeEvent( + 0, + pair, + takerSubmitOrderEvent.ouid, + takerSubmitOrderEvent.uuid, + takerSubmitOrderEvent.orderId ?: -1, + takerSubmitOrderEvent.direction, + takerSubmitOrderEvent.price, + 0, + makerSubmitOrderEvent.ouid, + makerSubmitOrderEvent.uuid, + makerSubmitOrderEvent.orderId ?: 1, + makerSubmitOrderEvent.direction, + makerSubmitOrderEvent.price, + makerSubmitOrderEvent.quantity - takerSubmitOrderEvent.quantity, + takerSubmitOrderEvent.quantity + ) + return tradeEvent + } + + private fun prepareOrder( + pair: Pair, + pairConfig: PairConfig, + submitOrderEvent: SubmitOrderEvent, + makerFee: Double, + takerFee: Double + ) { + runBlocking { + Mockito.`when`(pairConfigLoader.load(pair.toString(), submitOrderEvent.direction, "")) + .thenReturn( + PairFeeConfig( + pairConfig, + submitOrderEvent.direction.toString(), + "", + makerFee, + takerFee + ) + ) + Mockito.`when`(financialActionPersister.persist(MockitoHelper.anyObject())) + .then { + return@then it.getArgument>(0) + } + + val financialActions = orderManager.handleRequestOrder(submitOrderEvent) + + val orderPairFeeConfig = + pairConfigLoader.load(submitOrderEvent.pair.toString(), submitOrderEvent.direction, "") + val orderMakerFee = orderPairFeeConfig.makerFee * 1 //user level formula + val orderTakerFee = orderPairFeeConfig.takerFee * 1 //user level formula + Mockito.`when`(orderPersister.load(submitOrderEvent.ouid)).thenReturn( + Order( + submitOrderEvent.pair.toString(), + submitOrderEvent.ouid, + null, + orderMakerFee, + orderTakerFee, + orderPairFeeConfig.pairConfig.leftSideFraction, + orderPairFeeConfig.pairConfig.rightSideFraction, + submitOrderEvent.uuid, + "", + submitOrderEvent.direction, + submitOrderEvent.matchConstraint, + submitOrderEvent.orderType, + submitOrderEvent.price, + submitOrderEvent.quantity, + submitOrderEvent.quantity - submitOrderEvent.remainedQuantity, + submitOrderEvent.price.toBigDecimal(), + submitOrderEvent.quantity.toBigDecimal(), + (submitOrderEvent.quantity - submitOrderEvent.remainedQuantity).toBigDecimal(), + financialActions[0].amount, + financialActions[0].amount, + 0 + ) + ) + } + } } \ No newline at end of file From 872720cfee994a550b5b284fa5cbcaf0c33006cf Mon Sep 17 00:00:00 2001 From: Peyman Date: Wed, 11 May 2022 18:05:59 +0430 Subject: [PATCH 3/9] Add retry --- .../accountant/core/model/FinancialAction.kt | 79 ++++------- .../core/service/FeeCalculatorImpl.kt | 4 +- .../service/FinancialActionJobManagerImpl.kt | 5 +- .../core/service/TradeManagerImplTest.kt | 1 - .../impl/FinancialActionLoaderImpl.kt | 131 +++++++++--------- .../impl/FinancialActionPersisterImpl.kt | 126 ++++++++--------- .../postgres/model/FinancialActionModel.kt | 58 ++++---- .../ports/walletproxy/data/Amount.kt | 5 + .../ports/walletproxy/data/BooleanResponse.kt | 3 + .../ports/walletproxy/data/Currency.kt | 3 + .../ports/walletproxy/data/TransferResult.kt | 13 ++ .../walletproxy/proxy/WalletProxyImpl.kt | 33 +---- 12 files changed, 222 insertions(+), 239 deletions(-) create mode 100644 accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Amount.kt create mode 100644 accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/BooleanResponse.kt create mode 100644 accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Currency.kt create mode 100644 accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/TransferResult.kt diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FinancialAction.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FinancialAction.kt index 0de5d8a81..5f812e09c 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FinancialAction.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/model/FinancialAction.kt @@ -1,53 +1,28 @@ -package co.nilin.opex.accountant.core.model - -import java.math.BigDecimal -import java.time.LocalDateTime - -class FinancialAction( - val id: Long? = null, - val parent: FinancialAction?, - val eventType: String, - val pointer: String, - val symbol: String, - val amount: BigDecimal, - val sender: String, - val senderWalletType: String, - val receiver: String, - val receiverWalletType: String, - val createDate: LocalDateTime -) { - constructor( - parent: FinancialAction?, - eventType: String, - pointer: String, - symbol: String, - amount: BigDecimal, - sender: String, - senderWalletType: String, - receiver: String, - receiverWalletType: String, - createDate: LocalDateTime - ) : this( - null, - parent, - eventType, - pointer, - symbol, - amount, - sender, - senderWalletType, - receiver, - receiverWalletType, - createDate - ) - - override fun toString(): String { - return "FinancialAction(id=$id, parent=$parent, eventType='$eventType', pointer='$pointer', symbol='$symbol', amount=$amount, sender='$sender', senderWalletType='$senderWalletType', receiver='$receiver', receiverWalletType='$receiverWalletType', createDate=$createDate)" - } - - -} - -enum class FinancialActionStatus { - CREATED, PROCESSED, ERROR +package co.nilin.opex.accountant.core.model + +import java.math.BigDecimal +import java.time.LocalDateTime + +class FinancialAction( + val parent: FinancialAction?, + val eventType: String, + val pointer: String, + val symbol: String, + val amount: BigDecimal, + val sender: String, + val senderWalletType: String, + val receiver: String, + val receiverWalletType: String, + val createDate: LocalDateTime, + val retryCount: Int = 0, + val id: Long? = null +) { + + override fun toString(): String { + return "FinancialAction(id=$id, parent=$parent, eventType='$eventType', pointer='$pointer', symbol='$symbol', amount=$amount, sender='$sender', senderWalletType='$senderWalletType', receiver='$receiver', receiverWalletType='$receiverWalletType', createDate=$createDate)" + } +} + +enum class FinancialActionStatus { + CREATED, PROCESSED, ERROR } \ No newline at end of file diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt index e054f2b58..75c6eba07 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt @@ -66,7 +66,7 @@ class FeeCalculatorImpl( //check if maker uuid can pay the fee with platform coin val canMakerFulfil = runCatching { walletProxy.canFulfil(platformCoin, "main", trade.makerUuid, makerTotalFeeWithPlatformCoin) - }.getOrElse { false } + }.onFailure { logger.error(it.message) }.getOrElse { false } val makerFeeAction = if (makerTotalFeeWithPlatformCoin > BigDecimal.ZERO && canMakerFulfil) { FinancialAction( @@ -106,7 +106,7 @@ class FeeCalculatorImpl( //check if taker uuid can pay the fee with platform coin val canTakerFulfil = runCatching { walletProxy.canFulfil(platformCoin, "main", trade.takerUuid, takerTotalFeeWithPlatformCoin) - }.getOrElse { false } + }.onFailure { logger.error(it.message) }.getOrElse { false } val takerFeeAction = if (takerTotalFeeWithPlatformCoin > BigDecimal.ZERO && canTakerFulfil) { FinancialAction( diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt index b6ca7a3a7..764a7c4ce 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FinancialActionJobManagerImpl.kt @@ -33,7 +33,10 @@ class FinancialActionJobManagerImpl( financialActionPersister.updateStatus(it, FinancialActionStatus.PROCESSED) } catch (e: Exception) { log.error("financial job error", e) - financialActionPersister.updateStatus(it, FinancialActionStatus.ERROR) + financialActionPersister.updateStatus( + it, + if (it.retryCount >= retryLimit) FinancialActionStatus.ERROR else FinancialActionStatus.CREATED + ) } } } diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt index 62db326b4..548f0d73c 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt @@ -72,7 +72,6 @@ internal class TradeManagerImplTest { ) feeCalculator = FeeCalculatorImpl( - financeActionLoader, pairStaticRateLoader, walletProxy, "pcoin", diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionLoaderImpl.kt index e5185f09a..015991e68 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionLoaderImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionLoaderImpl.kt @@ -1,66 +1,67 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.FinancialActionStatus -import co.nilin.opex.accountant.core.spi.FinancialActionLoader -import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository -import kotlinx.coroutines.flow.firstOrNull -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrElse -import org.springframework.data.domain.PageRequest -import org.springframework.data.domain.Sort -import org.springframework.stereotype.Component -import java.math.BigDecimal - -@Component -class FinancialActionLoaderImpl(val financialActionRepository: FinancialActionRepository) : FinancialActionLoader { - - override suspend fun loadUnprocessed(offset: Long, size: Long): List { - return financialActionRepository.findByStatus( - FinancialActionStatus.CREATED.name, - PageRequest.of(offset.toInt(), size.toInt(), Sort.by(Sort.Direction.ASC, "createDate")) - ).map { fim -> - loadFinancialAction(fim.id)!! - }.toList() - } - - override suspend fun findLast(uuid: String, ouid: String): FinancialAction? { - return financialActionRepository.findByOuidAndUuid( - ouid, uuid, PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createDate")) - ).map { fim -> - loadFinancialAction(fim.id) - }.firstOrNull() - } - - private suspend fun loadFinancialAction(id: Long?): FinancialAction? { - if (id != null) { - val fim = financialActionRepository.findById(id).awaitFirst() - return FinancialAction( - fim.id, - loadFinancialAction(fim.parentId), - fim.eventType, - fim.pointer, - fim.symbol, - BigDecimal.valueOf(fim.amount), - fim.sender, - fim.senderWalletType, - fim.receiver, - fim.receiverWalletType, - fim.createDate - ) - } - return null - } - - override suspend fun countUnprocessed(uuid: String, symbol: String, eventType: String): Long { - return financialActionRepository.findByUuidAndSymbolAndEventTypeAndStatus( - uuid, - symbol, - eventType, - FinancialActionStatus.CREATED - ).awaitFirstOrElse { BigDecimal.ZERO } - .toLong() - } +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.FinancialActionStatus +import co.nilin.opex.accountant.core.spi.FinancialActionLoader +import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.toList +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrElse +import org.springframework.data.domain.PageRequest +import org.springframework.data.domain.Sort +import org.springframework.stereotype.Component +import java.math.BigDecimal + +@Component +class FinancialActionLoaderImpl(val financialActionRepository: FinancialActionRepository) : FinancialActionLoader { + + override suspend fun loadUnprocessed(offset: Long, size: Long): List { + return financialActionRepository.findByStatus( + FinancialActionStatus.CREATED.name, + PageRequest.of(offset.toInt(), size.toInt(), Sort.by(Sort.Direction.ASC, "createDate")) + ).map { fim -> + loadFinancialAction(fim.id)!! + }.toList() + } + + override suspend fun findLast(uuid: String, ouid: String): FinancialAction? { + return financialActionRepository.findByOuidAndUuid( + ouid, uuid, PageRequest.of(0, 1, Sort.by(Sort.Direction.DESC, "createDate")) + ).map { fim -> + loadFinancialAction(fim.id) + }.firstOrNull() + } + + private suspend fun loadFinancialAction(id: Long?): FinancialAction? { + if (id != null) { + val fim = financialActionRepository.findById(id).awaitFirst() + return FinancialAction( + loadFinancialAction(fim.parentId), + fim.eventType, + fim.pointer, + fim.symbol, + BigDecimal.valueOf(fim.amount), + fim.sender, + fim.senderWalletType, + fim.receiver, + fim.receiverWalletType, + fim.createDate, + fim.retryCount, + fim.id + ) + } + return null + } + + override suspend fun countUnprocessed(uuid: String, symbol: String, eventType: String): Long { + return financialActionRepository.findByUuidAndSymbolAndEventTypeAndStatus( + uuid, + symbol, + eventType, + FinancialActionStatus.CREATED + ).awaitFirstOrElse { BigDecimal.ZERO } + .toLong() + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt index f5102e7f5..b81bd76da 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/FinancialActionPersisterImpl.kt @@ -1,64 +1,64 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.model.FinancialAction -import co.nilin.opex.accountant.core.model.FinancialActionStatus -import co.nilin.opex.accountant.core.spi.FinancialActionPersister -import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository -import co.nilin.opex.accountant.ports.postgres.model.FinancialActionModel -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrElse -import kotlinx.coroutines.reactive.awaitLast -import org.springframework.stereotype.Component -import java.time.LocalDateTime - -@Component -class FinancialActionPersisterImpl(val financialActionRepository: FinancialActionRepository) : - FinancialActionPersister { - - override suspend fun persist(financialActions: List): List { - financialActionRepository.saveAll(financialActions.map { fa -> - FinancialActionModel( - null, - fa.parent?.id, - fa.eventType, - fa.pointer, - fa.symbol, - fa.amount.toDouble(), - fa.sender, - fa.senderWalletType, - fa.receiver, - fa.receiverWalletType, - "", - "", - fa.createDate - ) - }).awaitLast() - return financialActions - } - - override suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) { - val existing = financialActionRepository.findById(financialAction.id!!).awaitFirstOrElse { - throw IllegalArgumentException() - } - financialActionRepository.save( - FinancialActionModel( - existing.id, - existing.parentId, - existing.eventType, - existing.pointer, - existing.symbol, - existing.amount, - existing.sender, - existing.senderWalletType, - existing.receiver, - existing.receiverWalletType, - existing.agent, - existing.ip, - existing.createDate, - status, - 1 + (existing.retryCount ?: 0), - LocalDateTime.now() - ) - ).awaitFirst() - } +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.model.FinancialAction +import co.nilin.opex.accountant.core.model.FinancialActionStatus +import co.nilin.opex.accountant.core.spi.FinancialActionPersister +import co.nilin.opex.accountant.ports.postgres.dao.FinancialActionRepository +import co.nilin.opex.accountant.ports.postgres.model.FinancialActionModel +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitLast +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class FinancialActionPersisterImpl(val financialActionRepository: FinancialActionRepository) : + FinancialActionPersister { + + override suspend fun persist(financialActions: List): List { + financialActionRepository.saveAll(financialActions.map { + FinancialActionModel( + null, + it.parent?.id, + it.eventType, + it.pointer, + it.symbol, + it.amount.toDouble(), + it.sender, + it.senderWalletType, + it.receiver, + it.receiverWalletType, + "", + "", + it.createDate + ) + }).awaitLast() + return financialActions + } + + override suspend fun updateStatus(financialAction: FinancialAction, status: FinancialActionStatus) { + val existing = financialActionRepository.findById(financialAction.id!!).awaitFirstOrElse { + throw IllegalArgumentException() + } + financialActionRepository.save( + FinancialActionModel( + existing.id, + existing.parentId, + existing.eventType, + existing.pointer, + existing.symbol, + existing.amount, + existing.sender, + existing.senderWalletType, + existing.receiver, + existing.receiverWalletType, + existing.agent, + existing.ip, + existing.createDate, + status, + 1 + existing.retryCount, + LocalDateTime.now() + ) + ).awaitFirst() + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt index ef18db937..631e0c9b5 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/FinancialActionModel.kt @@ -1,29 +1,29 @@ -package co.nilin.opex.accountant.ports.postgres.model - -import co.nilin.opex.accountant.core.model.FinancialActionStatus -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Column -import org.springframework.data.relational.core.mapping.Table -import java.time.LocalDateTime - -@Table("fi_actions") -class FinancialActionModel( - @Id var id: Long?, - @Column("parent_id") var parentId: Long?, - @Column("event_type") val eventType: String, - val pointer: String, - val symbol: String, - @Column("amount") val amount: Double, - val sender: String, - @Column("sender_wallet_type") val senderWalletType: String, - val receiver: String, - @Column("receiver_wallet_type") val receiverWalletType: String, - val agent: String, - val ip: String, - @Column("create_date") val createDate: LocalDateTime, - val status: FinancialActionStatus = FinancialActionStatus.CREATED, - @Column("retry_count") val retryCount: Int? = null, - @Column("last_try_date") val lastTryDate: LocalDateTime? = null -) - - +package co.nilin.opex.accountant.ports.postgres.model + +import co.nilin.opex.accountant.core.model.FinancialActionStatus +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table +import java.time.LocalDateTime + +@Table("fi_actions") +class FinancialActionModel( + @Id var id: Long?, + @Column("parent_id") var parentId: Long?, + @Column("event_type") val eventType: String, + val pointer: String, + val symbol: String, + @Column("amount") val amount: Double, + val sender: String, + @Column("sender_wallet_type") val senderWalletType: String, + val receiver: String, + @Column("receiver_wallet_type") val receiverWalletType: String, + val agent: String, + val ip: String, + @Column("create_date") val createDate: LocalDateTime, + val status: FinancialActionStatus = FinancialActionStatus.CREATED, + @Column("retry_count") val retryCount: Int = 0, + @Column("last_try_date") val lastTryDate: LocalDateTime? = null +) + + diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Amount.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Amount.kt new file mode 100644 index 000000000..1a38bcadf --- /dev/null +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Amount.kt @@ -0,0 +1,5 @@ +package co.nilin.opex.accountant.ports.walletproxy.data + +import java.math.BigDecimal + +data class Amount(val currency: Currency, val amount: BigDecimal) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/BooleanResponse.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/BooleanResponse.kt new file mode 100644 index 000000000..2061a0b91 --- /dev/null +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/BooleanResponse.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.accountant.ports.walletproxy.data + +data class BooleanResponse(val result: Boolean) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Currency.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Currency.kt new file mode 100644 index 000000000..af8fccc29 --- /dev/null +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/Currency.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.accountant.ports.walletproxy.data + +data class Currency(val name: String, val symbol: String, val precision: Int) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/TransferResult.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/TransferResult.kt new file mode 100644 index 000000000..ed426824d --- /dev/null +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/data/TransferResult.kt @@ -0,0 +1,13 @@ +package co.nilin.opex.accountant.ports.walletproxy.data + +data class TransferResult( + val date: Long, + val sourceUuid: String, + val sourceWalletType: String, + val sourceBalanceBeforeAction: Amount, + val sourceBalanceAfterAction: Amount, + val amount: Amount, + val destUuid: String, + val destWalletType: String, + val receivedAmount: Amount +) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt index 6a8ecf17a..5acb55996 100644 --- a/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt +++ b/accountant/accountant-ports/accountant-wallet-proxy/src/main/kotlin/co/nilin/opex/accountant/ports/walletproxy/proxy/WalletProxyImpl.kt @@ -1,31 +1,14 @@ package co.nilin.opex.accountant.ports.walletproxy.proxy import co.nilin.opex.accountant.core.spi.WalletProxy +import co.nilin.opex.accountant.ports.walletproxy.data.BooleanResponse +import co.nilin.opex.accountant.ports.walletproxy.data.TransferResult import kotlinx.coroutines.reactive.awaitFirst import org.springframework.beans.factory.annotation.Value -import org.springframework.core.ParameterizedTypeReference import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.bodyToMono import java.math.BigDecimal -import java.net.URI - -inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {} - -data class TransferResult( - val date: Long, - val sourceUuid: String, - val sourceWalletType: String, - val sourceBalanceBeforeAction: Amount, - val sourceBalanceAfterAction: Amount, - val amount: Amount, - val destUuid: String, - val destWalletType: String, - val receivedAmount: Amount -) - -data class Amount(val currency: Currency, val amount: BigDecimal) - -data class Currency(val name: String, val symbol: String, val precision: Int) @Component class WalletProxyImpl( @@ -44,24 +27,22 @@ class WalletProxyImpl( transferRef: String? ) { webClient.post() - .uri(URI.create("$walletBaseUrl/transfer/${amount}_${symbol}/from/${senderUuid}_${senderWalletType}/to/${receiverUuid}_${receiverWalletType}")) + .uri("$walletBaseUrl/transfer/${amount}_$symbol/from/${senderUuid}_$senderWalletType/to/${receiverUuid}_$receiverWalletType?transferRef=$transferRef&description=$description") .header("Content-Type", "application/json") .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToMono(typeRef()) + .bodyToMono() .log() .awaitFirst() - } override suspend fun canFulfil(symbol: String, walletType: String, uuid: String, amount: BigDecimal): Boolean { - data class BooleanResponse(val result: Boolean) return webClient.get() - .uri(URI.create("$walletBaseUrl/$uuid/wallet_type/${walletType}/can_withdraw/${amount}_${symbol}")) + .uri("$walletBaseUrl/$uuid/wallet_type/$walletType/can_withdraw/${amount}_$symbol") .header("Content-Type", "application/json") .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToMono(typeRef()) + .bodyToMono() .log() .awaitFirst() .result From 0dabb40d0559aadb05f710b84234451c4868ff59 Mon Sep 17 00:00:00 2001 From: Peyman Date: Fri, 13 May 2022 21:56:47 +0430 Subject: [PATCH 4/9] Check health before submitting order --- .../app/controller/AccountantController.kt | 13 +++-- .../accountant/app/listener/OrderListener.kt | 5 ++ .../core/service/OrderManagerImpl.kt | 37 +++++++------- .../listener/inout/OrderSubmitRequest.kt | 45 +++++------------ .../submitter/config/KafkaTopicConfig.kt | 11 ++++- .../submitter/service/RichOrderSubmitter.kt | 4 +- .../submitter/service/RichTradeSubmitter.kt | 2 +- .../matching-engine-submitter-kafka/pom.xml | 4 ++ .../submitter/config/KafkaAdminConfig.kt | 33 ++++++++++--- .../submitter/config/KafkaTopicConfig.kt | 17 +++++-- .../gateway/app/inout/BooleanResponse.kt | 3 ++ .../gateway/app/proxy/AccountantProxyImpl.kt | 30 ++++++------ .../gateway/app/service/OrderService.kt | 48 +++++++++--------- .../submitter/config/KafkaAdminConfig.kt | 27 ++++++++++ .../submitter/inout/OrderSubmitRequest.kt | 49 +++++-------------- .../submitter/service/KafkaHealthIndicator.kt | 28 +++++++++++ .../opex/utility/error/data/OpexError.kt | 1 + 17 files changed, 208 insertions(+), 149 deletions(-) create mode 100644 matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/BooleanResponse.kt create mode 100644 matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/KafkaAdminConfig.kt create mode 100644 matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt index dacc694cf..73791b901 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/controller/AccountantController.kt @@ -5,8 +5,10 @@ import co.nilin.opex.accountant.core.model.PairFeeConfig import co.nilin.opex.accountant.core.spi.FinancialActionLoader import co.nilin.opex.accountant.core.spi.PairConfigLoader import co.nilin.opex.accountant.core.spi.WalletProxy +import co.nilin.opex.accountant.ports.walletproxy.data.BooleanResponse import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent import co.nilin.opex.matching.engine.core.model.OrderDirection +import org.slf4j.LoggerFactory import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RestController @@ -19,7 +21,7 @@ class AccountantController( val pairConfigLoader: PairConfigLoader ) { - data class BooleanResponse(val result: Boolean) + private val logger = LoggerFactory.getLogger(AccountantController::class.java) @GetMapping("{uuid}/create_order/{amount}_{currency}/allowed") suspend fun canCreateOrder( @@ -27,10 +29,11 @@ class AccountantController( @PathVariable("currency") currency: String, @PathVariable("amount") amount: BigDecimal ): BooleanResponse { - return BooleanResponse( - financialActionLoader.countUnprocessed(uuid, currency, SubmitOrderEvent::class.simpleName!!) <= 0 - && walletProxy.canFulfil(currency, "main", uuid, amount) - ) + val canFulfil = runCatching { walletProxy.canFulfil(currency, "main", uuid, amount) } + .onFailure { logger.error(it.message) } + .getOrElse { false } + val unprocessed = financialActionLoader.countUnprocessed(uuid, currency, SubmitOrderEvent::class.simpleName!!) + return BooleanResponse(unprocessed <= 0 && canFulfil) } @GetMapping(value = ["/config/{pair}/fee/{direction}-{userLevel}", "/config/{pair}/fee/{direction}"]) diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt index 16278a928..ac0286ad4 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/listener/OrderListener.kt @@ -5,15 +5,20 @@ import co.nilin.opex.accountant.ports.kafka.listener.inout.OrderSubmitRequest import co.nilin.opex.accountant.ports.kafka.listener.spi.OrderSubmitRequestListener import co.nilin.opex.matching.engine.core.eventh.events.SubmitOrderEvent import kotlinx.coroutines.runBlocking +import org.slf4j.LoggerFactory class OrderListener(private val orderManager: OrderManager) : OrderSubmitRequestListener { + private val logger = LoggerFactory.getLogger(OrderListener::class.java) + override fun id(): String { return "OrderListener" } override fun onOrder(order: OrderSubmitRequest, partition: Int, offset: Long, timestamp: Long) { runBlocking { + logger.info("Order submit event received ${order.ouid}") + orderManager.handleRequestOrder( SubmitOrderEvent( order.ouid, diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt index 3527b2a7a..b8a2ef704 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt @@ -41,25 +41,24 @@ open class OrderManagerImpl( amount for sell (ask): quantity amount for buy (bid): quantity * price */ - val financialAction = - FinancialAction( - null, - SubmitOrderEvent::class.simpleName!!, - submitOrderEvent.ouid, - symbol, - if (submitOrderEvent.direction == OrderDirection.ASK) { - BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) - } else { - BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) - .multiply(submitOrderEvent.price.toBigDecimal()) - .multiply(pairFeeConfig.pairConfig.rightSideFraction.toBigDecimal()) - }, - submitOrderEvent.uuid, - "main", - submitOrderEvent.uuid, - "exchange", - LocalDateTime.now() - ) + val financialAction = FinancialAction( + null, + SubmitOrderEvent::class.simpleName!!, + submitOrderEvent.ouid, + symbol, + if (submitOrderEvent.direction == OrderDirection.ASK) { + BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) + } else { + BigDecimal(submitOrderEvent.quantity).multiply(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) + .multiply(submitOrderEvent.price.toBigDecimal()) + .multiply(pairFeeConfig.pairConfig.rightSideFraction.toBigDecimal()) + }, + submitOrderEvent.uuid, + "main", + submitOrderEvent.uuid, + "exchange", + LocalDateTime.now() + ) //store order (ouid, uuid, fees, userlevel, pair, direction, price, quantity, filledQ, status, transfered) orderPersister.save( Order( diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt index 724850ab4..849158e22 100644 --- a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt +++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/inout/OrderSubmitRequest.kt @@ -5,37 +5,14 @@ import co.nilin.opex.matching.engine.core.model.OrderDirection import co.nilin.opex.matching.engine.core.model.OrderType import co.nilin.opex.matching.engine.core.model.Pair -class OrderSubmitRequest() { - - lateinit var ouid: String - lateinit var uuid: String - var orderId: Long? = null - lateinit var pair: Pair - var price: Long = 0 - var quantity: Long = 0 - var direction: OrderDirection = OrderDirection.BID - var matchConstraint: MatchConstraint = MatchConstraint.GTC - var orderType: OrderType = OrderType.LIMIT_ORDER - - constructor( - ouid: String, - uuid: String, - orderId: Long?, - pair: Pair, - price: Long, - quantity: Long, - direction: OrderDirection, - matchConstraint: MatchConstraint, - orderType: OrderType - ) : this() { - this.ouid = ouid - this.uuid = uuid - this.orderId = orderId - this.pair = pair - this.price = price - this.quantity = quantity - this.direction = direction - this.matchConstraint = matchConstraint - this.orderType = orderType - } -} \ No newline at end of file +data class OrderSubmitRequest( + val ouid: String, + val uuid: String, + val orderId: Long?, + val pair: Pair, + val price: Long = 0, + val quantity: Long = 0, + val direction: OrderDirection, + val matchConstraint: MatchConstraint, + val orderType: OrderType, +) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt index 2ddb2fe1e..97cb316c0 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt @@ -2,8 +2,10 @@ package co.nilin.opex.accountant.ports.kafka.submitter.config import org.apache.kafka.clients.admin.NewTopic import org.apache.kafka.common.config.TopicConfig -import org.springframework.beans.factory.annotation.Autowired +import org.slf4j.LoggerFactory +import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.annotation.Configuration +import org.springframework.context.event.EventListener import org.springframework.context.support.GenericApplicationContext import org.springframework.kafka.config.TopicBuilder import java.util.function.Supplier @@ -11,8 +13,12 @@ import java.util.function.Supplier @Configuration class KafkaTopicConfig { - @Autowired + private val logger = LoggerFactory.getLogger(KafkaTopicConfig::class.java) + + @EventListener(ApplicationReadyEvent::class) fun createTopics(applicationContext: GenericApplicationContext) { + logger.info("Creating kafka topic beans...") + with(applicationContext) { registerBean("topic_richOrder", NewTopic::class.java, Supplier { TopicBuilder.name("richOrder") @@ -32,6 +38,7 @@ class KafkaTopicConfig { registerBean("topic_tempevents", NewTopic::class.java, "tempevents", 1, 1) } + logger.info("Kafka topics created") } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichOrderSubmitter.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichOrderSubmitter.kt index 05c2e7189..0a93b1b2c 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichOrderSubmitter.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichOrderSubmitter.kt @@ -23,8 +23,8 @@ class RichOrderSubmitter(@Qualifier("richOrderKafkaTemplate") val kafkaTemplate: sendFuture.addCallback({ cont.resume(Unit) }, { - logger.info("Error submitting RichOrder", it) - cont.resumeWithException(it) + logger.error("Error submitting RichOrder", it) + cont.resume(Unit) }) } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichTradeSubmitter.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichTradeSubmitter.kt index e35188ada..c5f3641af 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichTradeSubmitter.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/service/RichTradeSubmitter.kt @@ -24,7 +24,7 @@ class RichTradeSubmitter(@Qualifier("richTradeKafkaTemplate") val kafkaTemplate: cont.resume(Unit) }, { logger.error("RichTrade submitter error", it) - cont.resumeWithException(it) + cont.resume(Unit) }) } } \ No newline at end of file diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml index 5ec8e2d4b..b2697b5cb 100644 --- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml +++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/pom.xml @@ -32,6 +32,10 @@ co.nilin.opex.matching.engine.core matching-engine-core + + org.springframework.boot + spring-boot-starter-actuator + org.springframework.kafka spring-kafka diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt index 6184f49aa..7a1923dd9 100644 --- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt +++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt @@ -1,21 +1,40 @@ package co.nilin.opex.matching.engine.ports.kafka.submitter.config -import org.apache.kafka.clients.admin.AdminClientConfig +import org.apache.kafka.clients.admin.AdminClient +import org.apache.kafka.clients.admin.DescribeClusterOptions import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.actuate.health.AbstractHealthIndicator +import org.springframework.boot.actuate.health.Health +import org.springframework.boot.actuate.health.HealthIndicator import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.kafka.core.KafkaAdmin @Configuration -class KafkaAdminConfig { - +class KafkaAdminConfig( @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String + private val bootstrapServers: String +) { @Bean - fun admin(): KafkaAdmin? { - val configs = hashMapOf(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers) - return KafkaAdmin(configs) + fun adminClient(admin: KafkaAdmin): AdminClient { + return AdminClient.create(admin.configurationProperties) + } + + @Bean + fun kafkaHealthIndicator(adminClient: AdminClient): HealthIndicator { + val options = DescribeClusterOptions().timeoutMs(1000) + return object : AbstractHealthIndicator() { + + override fun doHealthCheck(builder: Health.Builder) { + try { + adminClient.describeCluster(options) + builder.up().build() + }catch (e:Exception){ + builder.down(e).build() + } + } + } } } \ No newline at end of file diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt index 5cbc89854..e757f3bbc 100644 --- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt +++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt @@ -2,21 +2,28 @@ package co.nilin.opex.matching.engine.ports.kafka.submitter.config import org.apache.kafka.clients.admin.NewTopic import org.apache.kafka.common.config.TopicConfig +import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.context.event.ApplicationReadyEvent import org.springframework.context.annotation.Configuration +import org.springframework.context.event.EventListener import org.springframework.context.support.GenericApplicationContext import org.springframework.kafka.config.TopicBuilder import java.util.function.Supplier @Configuration -class KafkaTopicConfig { - +class KafkaTopicConfig( @Value("\${spring.app.symbols}") - private lateinit var symbols: String + private val symbols: String +) { + + private val logger = LoggerFactory.getLogger(KafkaTopicConfig::class.java) - @Autowired + @EventListener(ApplicationReadyEvent::class) fun createTopics(applicationContext: GenericApplicationContext) { + logger.info("Creating kafka topics...") + symbols.split(",") .map { s -> "orders_$s" } .forEach { topic -> @@ -52,6 +59,8 @@ class KafkaTopicConfig { .build() }) } + + logger.info("Finished creating topics") } } \ No newline at end of file diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/BooleanResponse.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/BooleanResponse.kt new file mode 100644 index 000000000..ae871751c --- /dev/null +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/inout/BooleanResponse.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.matching.gateway.app.inout + +data class BooleanResponse(val result: Boolean) \ No newline at end of file diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/AccountantProxyImpl.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/AccountantProxyImpl.kt index 1e129e44f..1e76757b1 100644 --- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/AccountantProxyImpl.kt +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/proxy/AccountantProxyImpl.kt @@ -1,30 +1,30 @@ package co.nilin.opex.matching.gateway.app.proxy import co.nilin.opex.matching.engine.core.model.OrderDirection +import co.nilin.opex.matching.gateway.app.inout.BooleanResponse import co.nilin.opex.matching.gateway.app.inout.PairFeeConfig import co.nilin.opex.matching.gateway.app.spi.AccountantApiProxy import kotlinx.coroutines.reactive.awaitFirst import org.springframework.beans.factory.annotation.Value -import org.springframework.core.ParameterizedTypeReference import org.springframework.stereotype.Component import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.reactive.function.client.bodyToMono import java.math.BigDecimal -import java.net.URI - -inline fun typeRef(): ParameterizedTypeReference = object : ParameterizedTypeReference() {} @Component class AccountantProxyImpl( - @Value("\${app.accountant.url}") val accountantBaseUrl: String, val webClient: WebClient + @Value("\${app.accountant.url}") + val accountantBaseUrl: String, + val webClient: WebClient ) : AccountantApiProxy { + override suspend fun canCreateOrder(uuid: String, symbol: String, value: BigDecimal): Boolean { - data class BooleanResponse(val result: Boolean) return webClient.get() - .uri(URI.create("$accountantBaseUrl/$uuid/create_order/${value}_${symbol}/allowed")) + .uri("$accountantBaseUrl/$uuid/create_order/${value}_${symbol}/allowed") .header("Content-Type", "application/json") .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToMono(typeRef()) + .bodyToMono() .log() .awaitFirst() .result @@ -33,18 +33,16 @@ class AccountantProxyImpl( override suspend fun fetchPairFeeConfig(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig { return webClient.get() .uri( - URI.create( - if (userLevel.isBlank()) { - "$accountantBaseUrl/config/${pair}/fee/${direction}" - } else { - "$accountantBaseUrl/config/${pair}/fee/${direction}-${userLevel}" - } - ) + if (userLevel.isBlank()) { + "$accountantBaseUrl/config/${pair}/fee/${direction}" + } else { + "$accountantBaseUrl/config/${pair}/fee/${direction}-${userLevel}" + } ) .header("Content-Type", "application/json") .retrieve() .onStatus({ t -> t.isError }, { it.createException() }) - .bodyToMono(typeRef()) + .bodyToMono() .log() .awaitFirst() } diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt index f78ec858d..3647c7f21 100644 --- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/service/OrderService.kt @@ -10,26 +10,34 @@ import co.nilin.opex.matching.gateway.app.spi.PairConfigLoader import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitRequest import co.nilin.opex.matching.gateway.ports.kafka.submitter.inout.OrderSubmitResult import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.EventSubmitter +import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.KafkaHealthIndicator import co.nilin.opex.matching.gateway.ports.kafka.submitter.service.OrderSubmitter import co.nilin.opex.utility.error.data.OpexError -import co.nilin.opex.utility.error.data.throwError +import co.nilin.opex.utility.error.data.OpexException +import org.slf4j.LoggerFactory import org.springframework.stereotype.Service -import java.util.* @Service class OrderService( val accountantApiProxy: AccountantApiProxy, val orderSubmitter: OrderSubmitter, val eventSubmitter: EventSubmitter, - val pairConfigLoader: PairConfigLoader + val pairConfigLoader: PairConfigLoader, + private val kafkaHealthIndicator: KafkaHealthIndicator, ) { + + private val logger = LoggerFactory.getLogger(OrderService::class.java) + suspend fun submitNewOrder(createOrderRequest: CreateOrderRequest): OrderSubmitResult { val symbolSides = createOrderRequest.pair.split("_") val symbol = if (createOrderRequest.direction == OrderDirection.ASK) symbolSides[0] else symbolSides[1] - if (!accountantApiProxy.canCreateOrder( + val pairFeeConfig = pairConfigLoader.load(createOrderRequest.pair, createOrderRequest.direction, "") + + val canCreateOrder = runCatching { + accountantApiProxy.canCreateOrder( createOrderRequest.uuid!!, symbol, if (createOrderRequest.direction == OrderDirection.ASK) @@ -37,26 +45,22 @@ class OrderService( else createOrderRequest.quantity.multiply(createOrderRequest.price) ) - ) { - throwError(OpexError.SubmitOrderForbiddenByAccountant) - } - val pairFeeConfig = pairConfigLoader.load( - createOrderRequest.pair, createOrderRequest.direction, "" - ) + }.onFailure { logger.error(it.message) }.getOrElse { false } + + if (!canCreateOrder) + throw OpexException(OpexError.SubmitOrderForbiddenByAccountant) + + if (!kafkaHealthIndicator.isHealthy) + throw OpexException(OpexError.ServiceUnavailable) + val orderSubmitRequest = OrderSubmitRequest( - UUID.randomUUID().toString(), - createOrderRequest.uuid!! //get from auth2 - , - null, + createOrderRequest.uuid!!, //get from auth2 Pair(symbolSides[0], symbolSides[1]), - createOrderRequest.price.divide( - pairFeeConfig.pairConfig.rightSideFraction - .toBigDecimal() - ).longValueExact(), - createOrderRequest.quantity.divide( - pairFeeConfig.pairConfig.leftSideFraction - .toBigDecimal() - ) + createOrderRequest.price + .divide(pairFeeConfig.pairConfig.rightSideFraction.toBigDecimal()) + .longValueExact(), + createOrderRequest.quantity + .divide(pairFeeConfig.pairConfig.leftSideFraction.toBigDecimal()) .longValueExact(), createOrderRequest.direction, createOrderRequest.matchConstraint, diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/KafkaAdminConfig.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/KafkaAdminConfig.kt new file mode 100644 index 000000000..daa67266b --- /dev/null +++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/config/KafkaAdminConfig.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.matching.gateway.ports.kafka.submitter.config + +import org.apache.kafka.clients.admin.AdminClient +import org.apache.kafka.clients.admin.AdminClientConfig +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.KafkaAdmin + +@Configuration +class KafkaAdminConfig( + @Value("\${spring.kafka.bootstrap-servers}") + private val bootstrapServers: String +) { + + @Bean + fun admin(): KafkaAdmin { + val configs = hashMapOf(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers) + return KafkaAdmin(configs) + } + + @Bean + fun adminClient(admin: KafkaAdmin): AdminClient { + return AdminClient.create(admin.configurationProperties) + } + +} \ No newline at end of file diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt index 978399f2d..a295e5ba2 100644 --- a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt +++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/inout/OrderSubmitRequest.kt @@ -4,41 +4,16 @@ import co.nilin.opex.matching.engine.core.model.MatchConstraint import co.nilin.opex.matching.engine.core.model.OrderDirection import co.nilin.opex.matching.engine.core.model.OrderType import co.nilin.opex.matching.engine.core.model.Pair +import java.util.* -class OrderSubmitRequest() { - - lateinit var ouid: String - lateinit var uuid: String - var orderId: Long? = null - lateinit var pair: Pair - var price: Long = 0 - var quantity: Long = 0 - var direction: OrderDirection = OrderDirection.BID - var matchConstraint: MatchConstraint = MatchConstraint.GTC - var orderType: OrderType = OrderType.LIMIT_ORDER - - - constructor( - ouid: String, - uuid: String, - orderId: Long?, - pair: Pair, - price: Long, - quantity: Long, - direction: OrderDirection, - matchConstraint: MatchConstraint, - orderType: OrderType - ) : this() { - this.ouid = ouid - this.uuid = uuid - this.orderId = orderId - this.pair = pair - this.price = price - this.quantity = quantity - this.direction = direction - this.matchConstraint = matchConstraint - this.orderType = orderType - } - - -} \ No newline at end of file +data class OrderSubmitRequest( + val uuid: String, + val pair: Pair, + val price: Long, + val quantity: Long, + val direction: OrderDirection, + val matchConstraint: MatchConstraint, + val orderType: OrderType, + val ouid: String = UUID.randomUUID().toString(), + val orderId: Long? = null, +) \ No newline at end of file diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt new file mode 100644 index 000000000..2f194e229 --- /dev/null +++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt @@ -0,0 +1,28 @@ +package co.nilin.opex.matching.gateway.ports.kafka.submitter.service + +import org.apache.kafka.clients.admin.AdminClient +import org.apache.kafka.clients.admin.DescribeClusterOptions +import org.slf4j.LoggerFactory +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component + +@Component +class KafkaHealthIndicator(private val adminClient: AdminClient) { + + private val logger = LoggerFactory.getLogger(KafkaHealthIndicator::class.java) + + var isHealthy = true + private val options = DescribeClusterOptions().timeoutMs(1000) + + @Scheduled(fixedDelay = 5000, initialDelay = 5000) + fun check() { + isHealthy = try { + adminClient.describeCluster(options) + true + } catch (e: Exception) { + logger.warn("Kafka is not healthy!: ${e.message}") + false + } + } + +} \ No newline at end of file diff --git a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt index 634b517b9..bf0e6359d 100644 --- a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt +++ b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt @@ -22,6 +22,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus // code 4000: matching-gateway SubmitOrderForbiddenByAccountant(4001, null, HttpStatus.BAD_REQUEST), + ServiceUnavailable(4002, null, HttpStatus.SERVICE_UNAVAILABLE), // code 5000: user-management EmailAlreadyVerified(5001, "Email is already verified", HttpStatus.BAD_REQUEST), From 64ea9020cf56fa8f0e970e9d3c0a26069283b1ec Mon Sep 17 00:00:00 2001 From: Peyman Date: Sat, 14 May 2022 16:05:28 +0430 Subject: [PATCH 5/9] Removed matching-engine and accountant dependency from api --- .../submitter/config/KafkaTopicConfig.kt | 5 +- .../submitter/config/SubmitterKafkaConfig.kt | 125 ++--- api/api-app/pom.xml | 158 ++++--- .../opex/api/app/listener/ApiListenerImpl.kt | 74 +-- api/api-core/pom.xml | 96 ++-- .../co/nilin/opex/api/core/event/RichOrder.kt | 27 ++ .../opex/api/core/event/RichOrderEvent.kt | 3 + .../opex/api/core/event/RichOrderUpdate.kt | 18 + .../co/nilin/opex/api/core/event/RichTrade.kt | 32 ++ .../nilin/opex/api/core/inout/OrderEnums.kt | 103 +++-- .../opex/api/core/inout/OrderMetaData.kt | 17 + .../nilin/opex/api/core/spi/MEGatewayProxy.kt | 43 +- .../nilin/opex/api/core/spi/OrderPersister.kt | 20 +- .../nilin/opex/api/core/spi/TradePersister.kt | 12 +- api/api-ports/api-binance-rest/pom.xml | 206 +++++---- .../api/ports/binance/util/EnumExtensions.kt | 66 ++- api/api-ports/api-eventlistener-kafka/pom.xml | 122 +++-- .../kafka/listener/config/ApiKafkaConfig.kt | 205 ++++----- .../listener/config/KafkaProducerConfig.kt | 113 +++-- .../listener/consumer/EventKafkaListener.kt | 28 -- .../listener/consumer/OrderKafkaListener.kt | 56 +-- .../listener/consumer/TradeKafkaListener.kt | 54 +-- .../ports/kafka/listener/spi/EventListener.kt | 9 - .../kafka/listener/spi/RichOrderListener.kt | 20 +- .../kafka/listener/spi/RichTradeListener.kt | 14 +- api/api-ports/api-persister-postgres/pom.xml | 142 +++--- .../api/ports/postgres/dao/OrderRepository.kt | 234 +++++----- .../postgres/impl/MarketQueryHandlerImpl.kt | 429 +++++++++--------- .../ports/postgres/impl/OrderPersisterImpl.kt | 146 +++--- .../ports/postgres/impl/TradePersisterImpl.kt | 82 ++-- .../postgres/impl/UserQueryHandlerImpl.kt | 275 ++++++----- .../api/ports/postgres/model/OrderModel.kt | 74 +-- .../api/ports/postgres/util/EnumExtensions.kt | 82 ++-- api/pom.xml | 10 - .../submitter/config/KafkaAdminConfig.kt | 27 +- .../submitter/config/KafkaTopicConfig.kt | 2 +- .../gateway/app/MatchingGatewayApp.kt | 34 +- .../submitter/service/KafkaHealthIndicator.kt | 9 +- 38 files changed, 1577 insertions(+), 1595 deletions(-) create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrder.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderEvent.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderUpdate.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichTrade.kt create mode 100644 api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderMetaData.kt delete mode 100644 api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/EventKafkaListener.kt delete mode 100644 api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/EventListener.kt diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt index 97cb316c0..b8ca2965f 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/KafkaTopicConfig.kt @@ -3,9 +3,8 @@ package co.nilin.opex.accountant.ports.kafka.submitter.config import org.apache.kafka.clients.admin.NewTopic import org.apache.kafka.common.config.TopicConfig import org.slf4j.LoggerFactory -import org.springframework.boot.context.event.ApplicationReadyEvent +import org.springframework.beans.factory.annotation.Autowired import org.springframework.context.annotation.Configuration -import org.springframework.context.event.EventListener import org.springframework.context.support.GenericApplicationContext import org.springframework.kafka.config.TopicBuilder import java.util.function.Supplier @@ -15,7 +14,7 @@ class KafkaTopicConfig { private val logger = LoggerFactory.getLogger(KafkaTopicConfig::class.java) - @EventListener(ApplicationReadyEvent::class) + @Autowired fun createTopics(applicationContext: GenericApplicationContext) { logger.info("Creating kafka topic beans...") diff --git a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/SubmitterKafkaConfig.kt b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/SubmitterKafkaConfig.kt index 0de624e27..8eebc6280 100644 --- a/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/SubmitterKafkaConfig.kt +++ b/accountant/accountant-ports/accountant-submitter-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/submitter/config/SubmitterKafkaConfig.kt @@ -1,63 +1,64 @@ -package co.nilin.opex.accountant.ports.kafka.submitter.config - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import org.apache.kafka.clients.producer.ProducerConfig -import org.apache.kafka.common.serialization.StringSerializer -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.kafka.core.DefaultKafkaProducerFactory -import org.springframework.kafka.core.KafkaTemplate -import org.springframework.kafka.core.ProducerFactory -import org.springframework.kafka.support.serializer.JsonSerializer - -@Configuration -class SubmitterKafkaConfig { - - @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String - - @Bean("producerConfigs") - fun producerConfigs(): Map { - return mapOf( - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, - ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, - ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, - ProducerConfig.ACKS_CONFIG to "all", - //ProducerConfig.CLIENT_ID_CONFIG to "", omitting this option as it produces InstanceAlreadyExistsException - ) - } - - @Bean("accountantEventProducerFactory") - fun producerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("accountantEventKafkaTemplate") - fun kafkaTemplate(@Qualifier("accountantEventProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(producerFactory) - } - - @Bean("richTradeProducerFactory") - fun richTradeProducerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("richTradeKafkaTemplate") - fun richTradeKafkaTemplate(@Qualifier("richTradeProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(producerFactory) - } - - @Bean("richOrderProducerFactory") - fun richOrderProducerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("richOrderKafkaTemplate") - fun richOrderKafkaTemplate(@Qualifier("richOrderProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(producerFactory) - } +package co.nilin.opex.accountant.ports.kafka.submitter.config + +import co.nilin.opex.accountant.core.inout.RichOrderEvent +import co.nilin.opex.accountant.core.inout.RichTrade +import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.core.ProducerFactory +import org.springframework.kafka.support.serializer.JsonSerializer + +@Configuration +class SubmitterKafkaConfig { + + @Value("\${spring.kafka.bootstrap-servers}") + private lateinit var bootstrapServers: String + + @Bean("producerConfigs") + fun producerConfigs(): Map { + return mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, + ProducerConfig.ACKS_CONFIG to "all", + JsonSerializer.TYPE_MAPPINGS to "rich_order_event:co.nilin.opex.accountant.core.inout.RichOrderEvent,rich_order:co.nilin.opex.accountant.core.inout.RichOrder,rich_order_update:co.nilin.opex.accountant.core.inout.RichOrderUpdate, rich_trade:co.nilin.opex.accountant.core.inout.RichTrade" + //ProducerConfig.CLIENT_ID_CONFIG to "", omitting this option as it produces InstanceAlreadyExistsException + ) + } + + @Bean("accountantEventProducerFactory") + fun producerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("accountantEventKafkaTemplate") + fun kafkaTemplate(@Qualifier("accountantEventProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(producerFactory) + } + + @Bean("richTradeProducerFactory") + fun richTradeProducerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("richTradeKafkaTemplate") + fun richTradeKafkaTemplate(@Qualifier("richTradeProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(producerFactory) + } + + @Bean("richOrderProducerFactory") + fun richOrderProducerFactory(@Qualifier("producerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("richOrderKafkaTemplate") + fun richOrderKafkaTemplate(@Qualifier("richOrderProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(producerFactory) + } } \ No newline at end of file diff --git a/api/api-app/pom.xml b/api/api-app/pom.xml index 00529ca01..afa3f85a8 100644 --- a/api/api-app/pom.xml +++ b/api/api-app/pom.xml @@ -1,81 +1,77 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - - - co.nilin.opex.api.app - api-app - api-app - Api app Opex - - - - org.jetbrains.kotlin - kotlin-reflect - - - com.fasterxml.jackson.module - jackson-module-kotlin - - - org.springframework.boot - spring-boot-starter - - - co.nilin.opex.utility.log - logging-handler - - - co.nilin.opex.utility.error - error-handler - - - co.nilin.opex.utility.interceptors - interceptors - - - co.nilin.opex.accountant.core - accountant-core - - - co.nilin.opex.api.core - api-core - - - co.nilin.opex.api.ports.kafka.listener - api-eventlistener-kafka - - - co.nilin.opex.api.ports.binance - api-binance-rest - - - co.nilin.opex.api.ports.postgres - api-persister-postgres - - - io.springfox - springfox-boot-starter - 3.0.0 - - - org.springframework.cloud - spring-cloud-starter-vault-config - - - - - - - org.springframework.boot - spring-boot-maven-plugin - - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + + + co.nilin.opex.api.app + api-app + api-app + Api app Opex + + + + org.jetbrains.kotlin + kotlin-reflect + + + com.fasterxml.jackson.module + jackson-module-kotlin + + + org.springframework.boot + spring-boot-starter + + + co.nilin.opex.utility.log + logging-handler + + + co.nilin.opex.utility.error + error-handler + + + co.nilin.opex.utility.interceptors + interceptors + + + co.nilin.opex.api.core + api-core + + + co.nilin.opex.api.ports.kafka.listener + api-eventlistener-kafka + + + co.nilin.opex.api.ports.binance + api-binance-rest + + + co.nilin.opex.api.ports.postgres + api-persister-postgres + + + io.springfox + springfox-boot-starter + 3.0.0 + + + org.springframework.cloud + spring-cloud-starter-vault-config + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + diff --git a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/listener/ApiListenerImpl.kt b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/listener/ApiListenerImpl.kt index 231209348..8b8d1fda4 100644 --- a/api/api-app/src/main/kotlin/co/nilin/opex/api/app/listener/ApiListenerImpl.kt +++ b/api/api-app/src/main/kotlin/co/nilin/opex/api/app/listener/ApiListenerImpl.kt @@ -1,38 +1,38 @@ -package co.nilin.opex.api.app.listener - -import co.nilin.opex.accountant.core.inout.RichOrder -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichOrderUpdate -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.api.app.config.AppDispatchers -import co.nilin.opex.api.core.spi.OrderPersister -import co.nilin.opex.api.core.spi.TradePersister -import co.nilin.opex.api.ports.kafka.listener.spi.RichOrderListener -import co.nilin.opex.api.ports.kafka.listener.spi.RichTradeListener -import kotlinx.coroutines.runBlocking - -class ApiListenerImpl( - private val richOrderPersister: OrderPersister, - private val richTradePersister: TradePersister -) : RichTradeListener, RichOrderListener { - - override fun id(): String { - return "AppListener" - } - - override fun onTrade(trade: RichTrade, partition: Int, offset: Long, timestamp: Long) { - println("RichTrade received") - runBlocking(AppDispatchers.kafkaExecutor) { - richTradePersister.save(trade) - } - } - - override fun onOrder(order: RichOrderEvent, partition: Int, offset: Long, timestamp: Long) { - runBlocking(AppDispatchers.kafkaExecutor) { - when (order) { - is RichOrder -> richOrderPersister.save(order) - is RichOrderUpdate -> richOrderPersister.update(order) - } - } - } +package co.nilin.opex.api.app.listener + +import co.nilin.opex.api.app.config.AppDispatchers +import co.nilin.opex.api.core.event.RichOrder +import co.nilin.opex.api.core.event.RichOrderEvent +import co.nilin.opex.api.core.event.RichOrderUpdate +import co.nilin.opex.api.core.event.RichTrade +import co.nilin.opex.api.core.spi.OrderPersister +import co.nilin.opex.api.core.spi.TradePersister +import co.nilin.opex.api.ports.kafka.listener.spi.RichOrderListener +import co.nilin.opex.api.ports.kafka.listener.spi.RichTradeListener +import kotlinx.coroutines.runBlocking + +class ApiListenerImpl( + private val richOrderPersister: OrderPersister, + private val richTradePersister: TradePersister +) : RichTradeListener, RichOrderListener { + + override fun id(): String { + return "AppListener" + } + + override fun onTrade(trade: RichTrade, partition: Int, offset: Long, timestamp: Long) { + println("RichTrade received") + runBlocking(AppDispatchers.kafkaExecutor) { + richTradePersister.save(trade) + } + } + + override fun onOrder(order: RichOrderEvent, partition: Int, offset: Long, timestamp: Long) { + runBlocking(AppDispatchers.kafkaExecutor) { + when (order) { + is RichOrder -> richOrderPersister.save(order) + is RichOrderUpdate -> richOrderPersister.update(order) + } + } + } } \ No newline at end of file diff --git a/api/api-core/pom.xml b/api/api-core/pom.xml index 0a8539071..07b42dac9 100644 --- a/api/api-core/pom.xml +++ b/api/api-core/pom.xml @@ -1,52 +1,44 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - - - co.nilin.opex.api.core - api-core - api-core - Api logic of Opex - - - - org.jetbrains.kotlin - kotlin-reflect - - - org.springframework.boot - spring-boot-starter - - - io.projectreactor.kotlin - reactor-kotlin-extensions - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactor - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - co.nilin.opex.matching.engine.core - matching-engine-core - - - co.nilin.opex.accountant.core - accountant-core - - - org.springframework - spring-tx - provided - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + + + co.nilin.opex.api.core + api-core + api-core + Api logic of Opex + + + + org.jetbrains.kotlin + kotlin-reflect + + + org.springframework.boot + spring-boot-starter + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + org.springframework + spring-tx + provided + + + diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrder.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrder.kt new file mode 100644 index 000000000..2bccaf32e --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrder.kt @@ -0,0 +1,27 @@ +package co.nilin.opex.api.core.event + +import co.nilin.opex.api.core.inout.MatchConstraint +import co.nilin.opex.api.core.inout.MatchingOrderType +import co.nilin.opex.api.core.inout.OrderDirection +import java.math.BigDecimal + +data class RichOrder( + val orderId: Long? = 0, + val pair: String, + val ouid: String, + val uuid: String, + val userLevel: String, + val makerFee: BigDecimal, + val takerFee: BigDecimal, + val leftSideFraction: BigDecimal, + val rightSideFraction: BigDecimal, + val direction: OrderDirection, + val constraint: MatchConstraint, + val type: MatchingOrderType, + val price: BigDecimal, + val quantity: BigDecimal, + val quoteQuantity: BigDecimal, + val executedQuantity: BigDecimal, + val accumulativeQuoteQty: BigDecimal, + val status: Int = 0 +) : RichOrderEvent diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderEvent.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderEvent.kt new file mode 100644 index 000000000..a953bf91d --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderEvent.kt @@ -0,0 +1,3 @@ +package co.nilin.opex.api.core.event + +interface RichOrderEvent \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderUpdate.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderUpdate.kt new file mode 100644 index 000000000..2dbff5ab1 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichOrderUpdate.kt @@ -0,0 +1,18 @@ +package co.nilin.opex.api.core.event + +import co.nilin.opex.api.core.inout.OrderStatus +import java.math.BigDecimal + +data class RichOrderUpdate( + val ouid: String, + val price: BigDecimal, + val quantity: BigDecimal, + val remainedQuantity: BigDecimal, + val status: OrderStatus = OrderStatus.NEW +) : RichOrderEvent { + + fun executedQuantity(): BigDecimal = quantity.minus(remainedQuantity) + + fun accumulativeQuoteQuantity(): BigDecimal = price.multiply((quantity.minus(remainedQuantity))) + +} \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichTrade.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichTrade.kt new file mode 100644 index 000000000..8253191bf --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/event/RichTrade.kt @@ -0,0 +1,32 @@ +package co.nilin.opex.api.core.event + +import co.nilin.opex.api.core.inout.OrderDirection +import java.math.BigDecimal +import java.time.LocalDateTime + +class RichTrade( + val id: Long, + val pair: String, + val takerOuid: String, + val takerUuid: String, + val takerOrderId: Long, + val takerDirection: OrderDirection, + val takerPrice: BigDecimal, + val takerQuantity: BigDecimal, + val takerQuoteQuantity: BigDecimal, + val takerRemainedQuantity: BigDecimal, + val takerCommision: BigDecimal, + val takerCommisionAsset: String, + val makerOuid: String, + val makerUuid: String, + val makerOrderId: Long, + val makerDirection: OrderDirection, + val makerPrice: BigDecimal, + val makerQuantity: BigDecimal, + val makerQuoteQuantity: BigDecimal, + val makerRemainedQuantity: BigDecimal, + val makerCommision: BigDecimal, + val makerCommisionAsset: String, + val matchedQuantity: BigDecimal, + val tradeDateTime: LocalDateTime +) \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt index 70c43fec1..fe558f43e 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderEnums.kt @@ -1,41 +1,64 @@ -package co.nilin.opex.api.core.inout - -enum class TimeInForce { - GTC, //Good Til Canceled, An order will be on the book unless the order is canceled. - IOC, //Immediate Or Cancel, An order will try to fill the order as much as it can before the order expires. - FOK, //Fill or Kill, An order will expire if the full order cannot be filled upon execution. -} - -enum class OrderStatus(val code: Int) { - - NEW(1), //The order has been accepted by the engine. - PARTIALLY_FILLED(4), //A part of the order has been filled. - FILLED(5), //The order has been completed. - CANCELED(2), //The order has been canceled by the user. - PENDING_CANCEL(7), //Currently unused - REJECTED(3), //The order was not accepted by the engine and not processed. - EXPIRED(6) //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance) -} - -enum class OrderType { - LIMIT, // timeInForce, quantity, price - MARKET, // quantity or quoteOrderQty - STOP_LOSS, // quantity, stopPrice - STOP_LOSS_LIMIT, // timeInForce, quantity, price, stopPrice - TAKE_PROFIT, // quantity, stopPrice - TAKE_PROFIT_LIMIT, // timeInForce, quantity, price, stopPrice - LIMIT_MAKER; // quantity, price - - companion object { - fun activeTypes() = listOf(LIMIT, MARKET) - } -} - -enum class OrderSide { - BUY, - SELL -} - -enum class OrderResponseType { - ACK, RESULT, FULL +package co.nilin.opex.api.core.inout + +enum class TimeInForce { + GTC, //Good Til Canceled, An order will be on the book unless the order is canceled. + IOC, //Immediate Or Cancel, An order will try to fill the order as much as it can before the order expires. + FOK, //Fill or Kill, An order will expire if the full order cannot be filled upon execution. +} + +enum class OrderStatus(val code: Int, val orderOfAppearance: Int) { + + REQUESTED(0, 0), + NEW(1, 1), //The order has been accepted by the engine. + PARTIALLY_FILLED(4, 2), //A part of the order has been filled. + FILLED(5, 3), //The order has been completed. + CANCELED(2, 3), //The order has been canceled by the user. + REJECTED(3, 3), //The order was not accepted by the engine and not processed. + EXPIRED( + 6, + 3 + ); //The order was canceled according to the order type's rules (e.g. LIMIT FOK orders with no fill, LIMIT IOC or MARKET orders that partially fill) or by the exchange, (e.g. orders canceled during liquidation, orders canceled during maintenance) + + fun comesBefore(status: OrderStatus?): Boolean { + if (status == null) + return false + return orderOfAppearance < status.orderOfAppearance + } + + fun comesAfter(status: OrderStatus?): Boolean { + if (status == null) + return false + return orderOfAppearance > status.orderOfAppearance + } + + companion object { + fun fromCode(code: Int?): OrderStatus? { + if (code == null) + return null + return values().find { it.code == code } + } + } +} + +enum class OrderType { + LIMIT, // timeInForce, quantity, price + MARKET, // quantity or quoteOrderQty + STOP_LOSS, // quantity, stopPrice + STOP_LOSS_LIMIT, // timeInForce, quantity, price, stopPrice + TAKE_PROFIT, // quantity, stopPrice + TAKE_PROFIT_LIMIT, // timeInForce, quantity, price, stopPrice + LIMIT_MAKER; // quantity, price + + companion object { + fun activeTypes() = listOf(LIMIT, MARKET) + } +} + +enum class OrderSide { + BUY, + SELL +} + +enum class OrderResponseType { + ACK, RESULT, FULL } \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderMetaData.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderMetaData.kt new file mode 100644 index 000000000..e46e16de8 --- /dev/null +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/inout/OrderMetaData.kt @@ -0,0 +1,17 @@ +package co.nilin.opex.api.core.inout + +enum class OrderDirection { + ASK, BID +} + +enum class MatchConstraint { + GTC, + IOC, + IOC_BUDGET, + FOK, + FOK_BUDGET +} + +enum class MatchingOrderType { + LIMIT_ORDER, MARKET_ORDER +} \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MEGatewayProxy.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MEGatewayProxy.kt index 8fe347450..1ccf2d514 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MEGatewayProxy.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/MEGatewayProxy.kt @@ -1,24 +1,21 @@ -package co.nilin.opex.api.core.spi - -import co.nilin.opex.api.core.inout.CancelOrderRequest -import co.nilin.opex.api.core.inout.OrderSubmitResult -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType -import java.math.BigDecimal - -interface MEGatewayProxy { - data class CreateOrderRequest( - var uuid: String?, - val pair: String, - val price: BigDecimal, - val quantity: BigDecimal, - val direction: OrderDirection, - val matchConstraint: MatchConstraint?, - val orderType: OrderType - ) - - suspend fun createNewOrder(order: CreateOrderRequest, token: String?): OrderSubmitResult? - - suspend fun cancelOrder(request: CancelOrderRequest, token: String?): OrderSubmitResult? +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.inout.* +import java.math.BigDecimal + +interface MEGatewayProxy { + + data class CreateOrderRequest( + var uuid: String?, + val pair: String, + val price: BigDecimal, + val quantity: BigDecimal, + val direction: OrderDirection, + val matchConstraint: MatchConstraint?, + val orderType: MatchingOrderType + ) + + suspend fun createNewOrder(order: CreateOrderRequest, token: String?): OrderSubmitResult? + + suspend fun cancelOrder(request: CancelOrderRequest, token: String?): OrderSubmitResult? } \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/OrderPersister.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/OrderPersister.kt index dc621f72b..f357d8a71 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/OrderPersister.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/OrderPersister.kt @@ -1,11 +1,11 @@ -package co.nilin.opex.api.core.spi - -import co.nilin.opex.accountant.core.inout.RichOrder -import co.nilin.opex.accountant.core.inout.RichOrderUpdate - -interface OrderPersister { - - suspend fun save(order: RichOrder) - - suspend fun update(orderUpdate: RichOrderUpdate) +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.event.RichOrder +import co.nilin.opex.api.core.event.RichOrderUpdate + +interface OrderPersister { + + suspend fun save(order: RichOrder) + + suspend fun update(orderUpdate: RichOrderUpdate) } \ No newline at end of file diff --git a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/TradePersister.kt b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/TradePersister.kt index d07d91a1f..736b7858f 100644 --- a/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/TradePersister.kt +++ b/api/api-core/src/main/kotlin/co/nilin/opex/api/core/spi/TradePersister.kt @@ -1,7 +1,7 @@ -package co.nilin.opex.api.core.spi - -import co.nilin.opex.accountant.core.inout.RichTrade - -interface TradePersister { - suspend fun save(trade: RichTrade) +package co.nilin.opex.api.core.spi + +import co.nilin.opex.api.core.event.RichTrade + +interface TradePersister { + suspend fun save(trade: RichTrade) } \ No newline at end of file diff --git a/api/api-ports/api-binance-rest/pom.xml b/api/api-ports/api-binance-rest/pom.xml index 72d6d77c8..0a046b6ee 100644 --- a/api/api-ports/api-binance-rest/pom.xml +++ b/api/api-ports/api-binance-rest/pom.xml @@ -1,105 +1,101 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - ../../pom.xml - - - co.nilin.opex.api.ports.binance - api-binance-rest - api-binance-rest - Api Binance Rest - - - - org.jetbrains.kotlin - kotlin-reflect - - - co.nilin.opex.matching.engine.core - matching-engine-core - - - co.nilin.opex.api.core - api-core - - - co.nilin.opex.utility.log - logging-handler - - - co.nilin.opex.utility.error - error-handler - - - co.nilin.opex.utility.interceptors - interceptors - - - org.springframework.boot - spring-boot-starter-webflux - - - org.springframework.cloud - spring-cloud-starter-consul-all - - - org.springframework.boot - spring-boot-starter-actuator - - - org.springframework.boot - spring-boot-starter-data-r2dbc - - - io.r2dbc - r2dbc-postgresql - runtime - - - org.postgresql - postgresql - runtime - - - io.projectreactor.kotlin - reactor-kotlin-extensions - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactor - - - org.springframework.boot - spring-boot-starter-security - - - org.springframework.boot - spring-boot-starter-oauth2-resource-server - - - org.bouncycastle - bcprov-jdk15on - 1.60 - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - io.projectreactor - reactor-test - test - - - io.swagger - swagger-annotations - 1.5.20 - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + ../../pom.xml + + + co.nilin.opex.api.ports.binance + api-binance-rest + api-binance-rest + Api Binance Rest + + + + org.jetbrains.kotlin + kotlin-reflect + + + co.nilin.opex.api.core + api-core + + + co.nilin.opex.utility.log + logging-handler + + + co.nilin.opex.utility.error + error-handler + + + co.nilin.opex.utility.interceptors + interceptors + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.cloud + spring-cloud-starter-consul-all + + + org.springframework.boot + spring-boot-starter-actuator + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + io.r2dbc + r2dbc-postgresql + runtime + + + org.postgresql + postgresql + runtime + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.bouncycastle + bcprov-jdk15on + 1.60 + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + io.projectreactor + reactor-test + test + + + io.swagger + swagger-annotations + 1.5.20 + + + diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/EnumExtensions.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/EnumExtensions.kt index 546cc78c0..fcaf4a257 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/EnumExtensions.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/util/EnumExtensions.kt @@ -1,36 +1,32 @@ -package co.nilin.opex.api.ports.binance.util - -import co.nilin.opex.api.core.inout.OrderSide -import co.nilin.opex.api.core.inout.TimeInForce -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType - -fun OrderSide.asOrderDirection(): OrderDirection { - if (this == OrderSide.BUY) - return OrderDirection.BID - return OrderDirection.ASK -} - -fun TimeInForce.asMatchConstraint(): MatchConstraint { - return when (this) { - TimeInForce.GTC -> MatchConstraint.GTC - TimeInForce.IOC -> MatchConstraint.IOC - TimeInForce.FOK -> MatchConstraint.FOK - } -} - -fun co.nilin.opex.api.core.inout.OrderType.asMatchingOrderType(): OrderType { - return when (this) { - co.nilin.opex.api.core.inout.OrderType.LIMIT -> OrderType.LIMIT_ORDER - co.nilin.opex.api.core.inout.OrderType.MARKET -> OrderType.MARKET_ORDER - else -> OrderType.LIMIT_ORDER - } -} - -fun > R.equalsAny(vararg equals: R): Boolean { - for (e in equals) - if (this == e) - return true - return false +package co.nilin.opex.api.ports.binance.util + +import co.nilin.opex.api.core.inout.* + +fun OrderSide.asOrderDirection(): OrderDirection { + if (this == OrderSide.BUY) + return OrderDirection.BID + return OrderDirection.ASK +} + +fun TimeInForce.asMatchConstraint(): MatchConstraint { + return when (this) { + TimeInForce.GTC -> MatchConstraint.GTC + TimeInForce.IOC -> MatchConstraint.IOC + TimeInForce.FOK -> MatchConstraint.FOK + } +} + +fun OrderType.asMatchingOrderType(): MatchingOrderType { + return when (this) { + OrderType.LIMIT -> MatchingOrderType.LIMIT_ORDER + OrderType.MARKET -> MatchingOrderType.MARKET_ORDER + else -> MatchingOrderType.LIMIT_ORDER + } +} + +fun > R.equalsAny(vararg equals: R): Boolean { + for (e in equals) + if (this == e) + return true + return false } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/pom.xml b/api/api-ports/api-eventlistener-kafka/pom.xml index 4763cdd35..51fdd04b9 100644 --- a/api/api-ports/api-eventlistener-kafka/pom.xml +++ b/api/api-ports/api-eventlistener-kafka/pom.xml @@ -1,65 +1,57 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - ../../pom.xml - - - co.nilin.opex.api.ports.kafka.listener - api-eventlistener-kafka - api-eventlistener-kafka - Api kafka listener of Opex - - - - org.jetbrains.kotlin - kotlin-reflect - - - org.springframework.boot - spring-boot-starter - - - org.springframework.boot - spring-boot-starter-webflux - - - co.nilin.opex.matching.engine.core - matching-engine-core - - - co.nilin.opex.accountant.core - accountant-core - - - co.nilin.opex.api.core - api-core - - - org.springframework.kafka - spring-kafka - - - io.projectreactor.kotlin - reactor-kotlin-extensions - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactor - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - org.springframework.kafka - spring-kafka-test - test - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + ../../pom.xml + + + co.nilin.opex.api.ports.kafka.listener + api-eventlistener-kafka + api-eventlistener-kafka + Api kafka listener of Opex + + + + org.jetbrains.kotlin + kotlin-reflect + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-webflux + + + co.nilin.opex.api.core + api-core + + + org.springframework.kafka + spring-kafka + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + org.springframework.kafka + spring-kafka-test + test + + + diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/ApiKafkaConfig.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/ApiKafkaConfig.kt index 0af4a06ce..ccf7eb01b 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/ApiKafkaConfig.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/ApiKafkaConfig.kt @@ -1,114 +1,93 @@ -package co.nilin.opex.api.ports.kafka.listener.config - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.api.ports.kafka.listener.consumer.EventKafkaListener -import co.nilin.opex.api.ports.kafka.listener.consumer.OrderKafkaListener -import co.nilin.opex.api.ports.kafka.listener.consumer.TradeKafkaListener -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import org.apache.kafka.clients.consumer.ConsumerConfig -import org.apache.kafka.common.TopicPartition -import org.apache.kafka.common.serialization.StringDeserializer -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.kafka.core.ConsumerFactory -import org.springframework.kafka.core.DefaultKafkaConsumerFactory -import org.springframework.kafka.core.KafkaTemplate -import org.springframework.kafka.listener.* -import org.springframework.kafka.support.serializer.JsonDeserializer -import org.springframework.util.backoff.FixedBackOff -import java.util.regex.Pattern - -@Configuration -class ApiKafkaConfig { - - @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String - - @Value("\${spring.kafka.consumer.group-id}") - private lateinit var groupId: String - - @Bean("apiConsumerConfig") - fun consumerConfigs(): Map { - return mapOf( - ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, - ConsumerConfig.GROUP_ID_CONFIG to groupId, - ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, - ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java, - JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*", - ) - } - - @Bean("eventsConsumerFactory") - fun consumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Bean("richTradeConsumerFactory") - fun richTradeConsumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Bean("richOrderConsumerFactory") - fun richOrderConsumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { - return DefaultKafkaConsumerFactory(consumerConfigs) - } - - @Autowired - @ConditionalOnBean(TradeKafkaListener::class) - fun configureTradeListener( - tradeListener: TradeKafkaListener, - template: KafkaTemplate, - @Qualifier("richTradeConsumerFactory") consumerFactory: ConsumerFactory - ) { - val containerProps = ContainerProperties(Pattern.compile("richTrade")) - containerProps.messageListener = tradeListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.setBeanName("ApiTradeKafkaListenerContainer") - container.commonErrorHandler = createConsumerErrorHandler(template, "richTrade.DLT") - container.start() - } - - @Autowired - @ConditionalOnBean(EventKafkaListener::class) - fun configureEventListener( - eventListener: EventKafkaListener, - template: KafkaTemplate, - @Qualifier("eventsConsumerFactory") consumerFactory: ConsumerFactory - ) { - val containerProps = ContainerProperties(Pattern.compile("events_.*")) - containerProps.messageListener = eventListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.setBeanName("ApiEventKafkaListenerContainer") - container.commonErrorHandler = createConsumerErrorHandler(template, "events.DLT") - container.start() - } - - @Autowired - @ConditionalOnBean(OrderKafkaListener::class) - fun configureOrderListener( - orderListener: OrderKafkaListener, - template: KafkaTemplate, - @Qualifier("richOrderConsumerFactory") consumerFactory: ConsumerFactory - ) { - val containerProps = ContainerProperties(Pattern.compile("richOrder")) - containerProps.messageListener = orderListener - val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) - container.setBeanName("ApiOrderKafkaListenerContainer") - container.commonErrorHandler = createConsumerErrorHandler(template, "richOrder.DLT") - container.start() - } - - private fun createConsumerErrorHandler(kafkaTemplate: KafkaTemplate<*, *>, dltTopic: String): CommonErrorHandler { - val recoverer = DeadLetterPublishingRecoverer(kafkaTemplate) { cr, _ -> - cr.headers().add("dlt-origin-module", "API".toByteArray()) - TopicPartition(dltTopic, cr.partition()) - } - return DefaultErrorHandler(recoverer, FixedBackOff(5_000, 20)) - } - +package co.nilin.opex.api.ports.kafka.listener.config + +import co.nilin.opex.api.core.event.RichOrderEvent +import co.nilin.opex.api.core.event.RichTrade +import co.nilin.opex.api.ports.kafka.listener.consumer.OrderKafkaListener +import co.nilin.opex.api.ports.kafka.listener.consumer.TradeKafkaListener +import org.apache.kafka.clients.consumer.ConsumerConfig +import org.apache.kafka.common.TopicPartition +import org.apache.kafka.common.serialization.StringDeserializer +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.ConsumerFactory +import org.springframework.kafka.core.DefaultKafkaConsumerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.listener.* +import org.springframework.kafka.support.serializer.JsonDeserializer +import org.springframework.util.backoff.FixedBackOff +import java.util.regex.Pattern + +@Configuration +class ApiKafkaConfig { + + @Value("\${spring.kafka.bootstrap-servers}") + private lateinit var bootstrapServers: String + + @Value("\${spring.kafka.consumer.group-id}") + private lateinit var groupId: String + + @Bean("apiConsumerConfig") + fun consumerConfigs(): Map { + return mapOf( + ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, + ConsumerConfig.GROUP_ID_CONFIG to groupId, + ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG to StringDeserializer::class.java, + ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG to JsonDeserializer::class.java, + JsonDeserializer.TRUSTED_PACKAGES to "co.nilin.opex.*", + JsonDeserializer.TYPE_MAPPINGS to "rich_order_event:co.nilin.opex.api.core.event.RichOrderEvent,rich_order:co.nilin.opex.api.core.event.RichOrder,rich_order_update:co.nilin.opex.api.core.event.RichOrderUpdate,rich_trade:co.nilin.opex.api.core.event.RichTrade" + ) + } + + @Bean("richTradeConsumerFactory") + fun richTradeConsumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs) + } + + @Bean("richOrderConsumerFactory") + fun richOrderConsumerFactory(@Qualifier("apiConsumerConfig") consumerConfigs: Map): ConsumerFactory { + return DefaultKafkaConsumerFactory(consumerConfigs) + } + + @Autowired + @ConditionalOnBean(TradeKafkaListener::class) + fun configureTradeListener( + tradeListener: TradeKafkaListener, + template: KafkaTemplate, + @Qualifier("richTradeConsumerFactory") consumerFactory: ConsumerFactory + ) { + val containerProps = ContainerProperties(Pattern.compile("richTrade")) + containerProps.messageListener = tradeListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.setBeanName("ApiTradeKafkaListenerContainer") + container.commonErrorHandler = createConsumerErrorHandler(template, "richTrade.DLT") + container.start() + } + + @Autowired + @ConditionalOnBean(OrderKafkaListener::class) + fun configureOrderListener( + orderListener: OrderKafkaListener, + template: KafkaTemplate, + @Qualifier("richOrderConsumerFactory") consumerFactory: ConsumerFactory + ) { + val containerProps = ContainerProperties(Pattern.compile("richOrder")) + containerProps.messageListener = orderListener + val container = ConcurrentMessageListenerContainer(consumerFactory, containerProps) + container.setBeanName("ApiOrderKafkaListenerContainer") + container.commonErrorHandler = createConsumerErrorHandler(template, "richOrder.DLT") + container.start() + } + + private fun createConsumerErrorHandler(kafkaTemplate: KafkaTemplate<*, *>, dltTopic: String): CommonErrorHandler { + val recoverer = DeadLetterPublishingRecoverer(kafkaTemplate) { cr, _ -> + cr.headers().add("dlt-origin-module", "API".toByteArray()) + TopicPartition(dltTopic, cr.partition()) + } + return DefaultErrorHandler(recoverer, FixedBackOff(5_000, 20)) + } + } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/KafkaProducerConfig.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/KafkaProducerConfig.kt index d44bbd266..ebab34a8b 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/KafkaProducerConfig.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/config/KafkaProducerConfig.kt @@ -1,63 +1,52 @@ -package co.nilin.opex.api.ports.kafka.listener.config - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import org.apache.kafka.clients.producer.ProducerConfig -import org.apache.kafka.common.serialization.StringSerializer -import org.springframework.beans.factory.annotation.Qualifier -import org.springframework.beans.factory.annotation.Value -import org.springframework.context.annotation.Bean -import org.springframework.context.annotation.Configuration -import org.springframework.kafka.core.DefaultKafkaProducerFactory -import org.springframework.kafka.core.KafkaTemplate -import org.springframework.kafka.core.ProducerFactory -import org.springframework.kafka.support.serializer.JsonSerializer - -@Configuration -class KafkaProducerConfig { - - @Value("\${spring.kafka.bootstrap-servers}") - private lateinit var bootstrapServers: String - - @Bean("apiProducerConfigs") - fun producerConfigs(): Map { - return mapOf( - ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, - ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, - ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, - ProducerConfig.ACKS_CONFIG to "all" - ) - } - - @Bean("eventsProducerFactory") - fun producerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("eventKafkaTemplate") - fun kafkaTemplate(@Qualifier("eventsProducerFactory") producerFactory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(producerFactory) - } - - @Bean("richTradeProducerFactory") - fun richTradeProducerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("richTradeKafkaTemplate") - fun richTradeTemplate(@Qualifier("richTradeProducerFactory") factory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(factory) - } - - @Bean("richOrderProducerFactory") - fun richOrderProducerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { - return DefaultKafkaProducerFactory(producerConfigs) - } - - @Bean("richOrderKafkaTemplate") - fun richOrderTemplate(@Qualifier("richOrderProducerFactory") factory: ProducerFactory): KafkaTemplate { - return KafkaTemplate(factory) - } - +package co.nilin.opex.api.ports.kafka.listener.config + +import co.nilin.opex.api.core.event.RichOrderEvent +import co.nilin.opex.api.core.event.RichTrade +import org.apache.kafka.clients.producer.ProducerConfig +import org.apache.kafka.common.serialization.StringSerializer +import org.springframework.beans.factory.annotation.Qualifier +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.kafka.core.DefaultKafkaProducerFactory +import org.springframework.kafka.core.KafkaTemplate +import org.springframework.kafka.core.ProducerFactory +import org.springframework.kafka.support.serializer.JsonSerializer + +@Configuration +class KafkaProducerConfig { + + @Value("\${spring.kafka.bootstrap-servers}") + private lateinit var bootstrapServers: String + + @Bean("apiProducerConfigs") + fun producerConfigs(): Map { + return mapOf( + ProducerConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers, + ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG to StringSerializer::class.java, + ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG to JsonSerializer::class.java, + ProducerConfig.ACKS_CONFIG to "all" + ) + } + + @Bean("richTradeProducerFactory") + fun richTradeProducerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("richTradeKafkaTemplate") + fun richTradeTemplate(@Qualifier("richTradeProducerFactory") factory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(factory) + } + + @Bean("richOrderProducerFactory") + fun richOrderProducerFactory(@Qualifier("apiProducerConfigs") producerConfigs: Map): ProducerFactory { + return DefaultKafkaProducerFactory(producerConfigs) + } + + @Bean("richOrderKafkaTemplate") + fun richOrderTemplate(@Qualifier("richOrderProducerFactory") factory: ProducerFactory): KafkaTemplate { + return KafkaTemplate(factory) + } + } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/EventKafkaListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/EventKafkaListener.kt deleted file mode 100644 index 6c01fa606..000000000 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/EventKafkaListener.kt +++ /dev/null @@ -1,28 +0,0 @@ -package co.nilin.opex.api.ports.kafka.listener.consumer - - -import co.nilin.opex.api.ports.kafka.listener.spi.EventListener -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.springframework.kafka.listener.MessageListener -import org.springframework.stereotype.Component - -@Component -class EventKafkaListener : MessageListener { - val eventListeners = arrayListOf() - override fun onMessage(data: ConsumerRecord) { - eventListeners.forEach { tl -> - tl.onEvent(data.value(), data.partition(), data.offset(), data.timestamp()) - } - } - - fun addEventListener(tl: EventListener) { - eventListeners.add(tl) - } - - fun removeEventListener(tl: EventListener) { - eventListeners.removeIf { item -> - item.id() == tl.id() - } - } -} \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/OrderKafkaListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/OrderKafkaListener.kt index e9492a732..e88ba3f55 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/OrderKafkaListener.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/OrderKafkaListener.kt @@ -1,29 +1,29 @@ -package co.nilin.opex.api.ports.kafka.listener.consumer - -import co.nilin.opex.accountant.core.inout.RichOrderEvent -import co.nilin.opex.api.ports.kafka.listener.spi.RichOrderListener -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.springframework.kafka.listener.MessageListener -import org.springframework.stereotype.Component - -@Component -class OrderKafkaListener : MessageListener { - - val orderListeners = arrayListOf() - - override fun onMessage(data: ConsumerRecord) { - orderListeners.forEach { tl -> - tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) - } - } - - fun addOrderListener(tl: RichOrderListener) { - orderListeners.add(tl) - } - - fun removeOrderListener(tl: RichOrderListener) { - orderListeners.removeIf { item -> - item.id() == tl.id() - } - } +package co.nilin.opex.api.ports.kafka.listener.consumer + +import co.nilin.opex.api.core.event.RichOrderEvent +import co.nilin.opex.api.ports.kafka.listener.spi.RichOrderListener +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.kafka.listener.MessageListener +import org.springframework.stereotype.Component + +@Component +class OrderKafkaListener : MessageListener { + + val orderListeners = arrayListOf() + + override fun onMessage(data: ConsumerRecord) { + orderListeners.forEach { tl -> + tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) + } + } + + fun addOrderListener(tl: RichOrderListener) { + orderListeners.add(tl) + } + + fun removeOrderListener(tl: RichOrderListener) { + orderListeners.removeIf { item -> + item.id() == tl.id() + } + } } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/TradeKafkaListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/TradeKafkaListener.kt index ace560b20..024771d80 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/TradeKafkaListener.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/consumer/TradeKafkaListener.kt @@ -1,27 +1,29 @@ -package co.nilin.opex.api.ports.kafka.listener.consumer - -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.api.ports.kafka.listener.spi.RichTradeListener -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.springframework.kafka.listener.MessageListener -import org.springframework.stereotype.Component - -@Component -class TradeKafkaListener : MessageListener { - val tradeListeners = arrayListOf() - override fun onMessage(data: ConsumerRecord) { - tradeListeners.forEach { tl -> - tl.onTrade(data.value(), data.partition(), data.offset(), data.timestamp()) - } - } - - fun addTradeListener(tl: RichTradeListener) { - tradeListeners.add(tl) - } - - fun removeTradeListener(tl: RichTradeListener) { - tradeListeners.removeIf { item -> - item.id() == tl.id() - } - } +package co.nilin.opex.api.ports.kafka.listener.consumer + +import co.nilin.opex.api.core.event.RichTrade +import co.nilin.opex.api.ports.kafka.listener.spi.RichTradeListener +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.kafka.listener.MessageListener +import org.springframework.stereotype.Component + +@Component +class TradeKafkaListener : MessageListener { + + val tradeListeners = arrayListOf() + + override fun onMessage(data: ConsumerRecord) { + tradeListeners.forEach { tl -> + tl.onTrade(data.value(), data.partition(), data.offset(), data.timestamp()) + } + } + + fun addTradeListener(tl: RichTradeListener) { + tradeListeners.add(tl) + } + + fun removeTradeListener(tl: RichTradeListener) { + tradeListeners.removeIf { item -> + item.id() == tl.id() + } + } } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/EventListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/EventListener.kt deleted file mode 100644 index e98cc1212..000000000 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/EventListener.kt +++ /dev/null @@ -1,9 +0,0 @@ -package co.nilin.opex.api.ports.kafka.listener.spi - -import co.nilin.opex.matching.engine.core.eventh.events.CoreEvent - - -interface EventListener { - fun id(): String - fun onEvent(coreEvent: CoreEvent, partition: Int, offset: Long, timestamp: Long) -} \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichOrderListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichOrderListener.kt index a87a85c5b..8809dced7 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichOrderListener.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichOrderListener.kt @@ -1,11 +1,11 @@ -package co.nilin.opex.api.ports.kafka.listener.spi - -import co.nilin.opex.accountant.core.inout.RichOrderEvent - -interface RichOrderListener { - - fun id(): String - - fun onOrder(order: RichOrderEvent, partition: Int, offset: Long, timestamp: Long) - +package co.nilin.opex.api.ports.kafka.listener.spi + +import co.nilin.opex.api.core.event.RichOrderEvent + +interface RichOrderListener { + + fun id(): String + + fun onOrder(order: RichOrderEvent, partition: Int, offset: Long, timestamp: Long) + } \ No newline at end of file diff --git a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichTradeListener.kt b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichTradeListener.kt index de14026e7..c852f831e 100644 --- a/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichTradeListener.kt +++ b/api/api-ports/api-eventlistener-kafka/src/main/kotlin/co/nilin/opex/api/ports/kafka/listener/spi/RichTradeListener.kt @@ -1,8 +1,8 @@ -package co.nilin.opex.api.ports.kafka.listener.spi - -import co.nilin.opex.accountant.core.inout.RichTrade - -interface RichTradeListener { - fun id(): String - fun onTrade(trade: RichTrade, partition: Int, offset: Long, timestamp: Long) +package co.nilin.opex.api.ports.kafka.listener.spi + +import co.nilin.opex.api.core.event.RichTrade + +interface RichTradeListener { + fun id(): String + fun onTrade(trade: RichTrade, partition: Int, offset: Long, timestamp: Long) } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/pom.xml b/api/api-ports/api-persister-postgres/pom.xml index d00fff280..28ded2b84 100644 --- a/api/api-ports/api-persister-postgres/pom.xml +++ b/api/api-ports/api-persister-postgres/pom.xml @@ -1,75 +1,67 @@ - - - 4.0.0 - - - co.nilin.opex.api - api - 1.0-SNAPSHOT - ../../pom.xml - - - co.nilin.opex.api.ports.postgres - api-persister-postgres - api-persister-postgres - Persist items of Opex api on Postgres - - - - org.jetbrains.kotlin - kotlin-reflect - - - co.nilin.opex.matching.engine.core - matching-engine-core - - - co.nilin.opex.api.core - api-core - - - co.nilin.opex.accountant.core - accountant-core - - - co.nilin.opex.utility.error - error-handler - - - org.springframework.boot - spring-boot-starter-data-r2dbc - - - io.r2dbc - r2dbc-postgresql - runtime - - - org.postgresql - postgresql - runtime - - - io.projectreactor.kotlin - reactor-kotlin-extensions - - - org.jetbrains.kotlinx - kotlinx-coroutines-reactor - - - org.jetbrains.kotlinx - kotlinx-coroutines-core - - - com.google.code.gson - gson - - - io.projectreactor - reactor-test - test - - - + + + 4.0.0 + + + co.nilin.opex.api + api + 1.0-SNAPSHOT + ../../pom.xml + + + co.nilin.opex.api.ports.postgres + api-persister-postgres + api-persister-postgres + Persist items of Opex api on Postgres + + + + org.jetbrains.kotlin + kotlin-reflect + + + co.nilin.opex.api.core + api-core + + + co.nilin.opex.utility.error + error-handler + + + org.springframework.boot + spring-boot-starter-data-r2dbc + + + io.r2dbc + r2dbc-postgresql + runtime + + + org.postgresql + postgresql + runtime + + + io.projectreactor.kotlin + reactor-kotlin-extensions + + + org.jetbrains.kotlinx + kotlinx-coroutines-reactor + + + org.jetbrains.kotlinx + kotlinx-coroutines-core + + + com.google.code.gson + gson + + + io.projectreactor + reactor-test + test + + + diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/OrderRepository.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/OrderRepository.kt index 6dd73ce68..1a2f0c57d 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/OrderRepository.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/dao/OrderRepository.kt @@ -1,118 +1,118 @@ -package co.nilin.opex.api.ports.postgres.dao - -import co.nilin.opex.api.core.inout.AggregatedOrderPriceModel -import co.nilin.opex.api.ports.postgres.model.OrderModel -import co.nilin.opex.matching.engine.core.model.OrderDirection -import kotlinx.coroutines.flow.Flow -import org.springframework.data.r2dbc.repository.Query -import org.springframework.data.repository.query.Param -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Flux -import reactor.core.publisher.Mono -import java.util.* - -@Repository -interface OrderRepository : ReactiveCrudRepository { - - @Query("select * from orders where ouid = :ouid") - fun findByOuid(@Param("ouid") ouid: String): Mono - - @Query("select * from orders where symbol = :symbol and order_id = :orderId") - fun findBySymbolAndOrderId( - @Param("symbol") - symbol: String, @Param("orderId") - orderId: Long - ): Mono - - @Query("select * from orders where symbol = :symbol and client_order_id = :origClientOrderId") - fun findBySymbolAndClientOrderId( - @Param("symbol") - symbol: String, - @Param("origClientOrderId") - origClientOrderId: String - ): Mono - - @Query( - """ - select * from orders - join order_status os on orders.ouid = os.ouid - where uuid = :uuid and (:symbol is null or symbol = :symbol) and status in (:statuses) - and appearance = (select max(appearance) from order_status where ouid = orders.ouid) - and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) - """ - ) - fun findByUuidAndSymbolAndStatus( - @Param("uuid") - uuid: String, - @Param("symbol") - symbol: String?, @Param("statuses") - status: Collection - ): Flow - - @Query( - "select * from orders where uuid = :uuid " + - "and (:symbol is null or symbol = :symbol) " + - "and (:startTime is null or update_date >= :startTime)" + - "and (:endTime is null or update_date < :endTime)" - ) - fun findByUuidAndSymbolAndTimeBetween( - @Param("uuid") - uuid: String, - @Param("symbol") - symbol: String?, - @Param("startTime") - startTime: Date?, - @Param("endTime") - endTime: Date? - ): Flow - - @Query( - """ - select price, (sum(quantity) - sum(os.executed_quantity)) as quantity from orders - join order_status os on orders.ouid = os.ouid - where symbol = :symbol and side = :direction and os.status in (:statuses) - and appearance = (select max(appearance) from order_status where ouid = orders.ouid) - and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) - group by price - order by price asc - limit :limit - """ - ) - fun findBySymbolAndDirectionAndStatusSortAscendingByPrice( - @Param("symbol") - symbol: String, - @Param("direction") - direction: OrderDirection, - @Param("limit") - limit: Int, - @Param("statuses") - status: Collection - ): Flux - - @Query( - """ - select price, (sum(quantity) - sum(executed_quantity)) as quantity from orders - join order_status os on orders.ouid = os.ouid - where symbol = :symbol and side = :direction and status in (:statuses) - and appearance = (select max(appearance) from order_status where ouid = orders.ouid) - and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) - group by price - order by price desc - limit :limit - """ - ) - fun findBySymbolAndDirectionAndStatusSortDescendingByPrice( - @Param("symbol") - symbol: String, - @Param("direction") - direction: OrderDirection, - @Param("limit") - limit: Int, - @Param("statuses") - status: Collection - ): Flux - - @Query("select * from orders where symbol = :symbol order by create_date desc limit 1") - fun findLastOrderBySymbol(@Param("symbol") symbol: String): Mono +package co.nilin.opex.api.ports.postgres.dao + +import co.nilin.opex.api.core.inout.AggregatedOrderPriceModel +import co.nilin.opex.api.core.inout.OrderDirection +import co.nilin.opex.api.ports.postgres.model.OrderModel +import kotlinx.coroutines.flow.Flow +import org.springframework.data.r2dbc.repository.Query +import org.springframework.data.repository.query.Param +import org.springframework.data.repository.reactive.ReactiveCrudRepository +import org.springframework.stereotype.Repository +import reactor.core.publisher.Flux +import reactor.core.publisher.Mono +import java.util.* + +@Repository +interface OrderRepository : ReactiveCrudRepository { + + @Query("select * from orders where ouid = :ouid") + fun findByOuid(@Param("ouid") ouid: String): Mono + + @Query("select * from orders where symbol = :symbol and order_id = :orderId") + fun findBySymbolAndOrderId( + @Param("symbol") + symbol: String, @Param("orderId") + orderId: Long + ): Mono + + @Query("select * from orders where symbol = :symbol and client_order_id = :origClientOrderId") + fun findBySymbolAndClientOrderId( + @Param("symbol") + symbol: String, + @Param("origClientOrderId") + origClientOrderId: String + ): Mono + + @Query( + """ + select * from orders + join order_status os on orders.ouid = os.ouid + where uuid = :uuid and (:symbol is null or symbol = :symbol) and status in (:statuses) + and appearance = (select max(appearance) from order_status where ouid = orders.ouid) + and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) + """ + ) + fun findByUuidAndSymbolAndStatus( + @Param("uuid") + uuid: String, + @Param("symbol") + symbol: String?, @Param("statuses") + status: Collection + ): Flow + + @Query( + "select * from orders where uuid = :uuid " + + "and (:symbol is null or symbol = :symbol) " + + "and (:startTime is null or update_date >= :startTime)" + + "and (:endTime is null or update_date < :endTime)" + ) + fun findByUuidAndSymbolAndTimeBetween( + @Param("uuid") + uuid: String, + @Param("symbol") + symbol: String?, + @Param("startTime") + startTime: Date?, + @Param("endTime") + endTime: Date? + ): Flow + + @Query( + """ + select price, (sum(quantity) - sum(os.executed_quantity)) as quantity from orders + join order_status os on orders.ouid = os.ouid + where symbol = :symbol and side = :direction and os.status in (:statuses) + and appearance = (select max(appearance) from order_status where ouid = orders.ouid) + and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) + group by price + order by price asc + limit :limit + """ + ) + fun findBySymbolAndDirectionAndStatusSortAscendingByPrice( + @Param("symbol") + symbol: String, + @Param("direction") + direction: OrderDirection, + @Param("limit") + limit: Int, + @Param("statuses") + status: Collection + ): Flux + + @Query( + """ + select price, (sum(quantity) - sum(executed_quantity)) as quantity from orders + join order_status os on orders.ouid = os.ouid + where symbol = :symbol and side = :direction and status in (:statuses) + and appearance = (select max(appearance) from order_status where ouid = orders.ouid) + and executed_quantity = (select max(executed_quantity) from order_status where ouid = orders.ouid) + group by price + order by price desc + limit :limit + """ + ) + fun findBySymbolAndDirectionAndStatusSortDescendingByPrice( + @Param("symbol") + symbol: String, + @Param("direction") + direction: OrderDirection, + @Param("limit") + limit: Int, + @Param("statuses") + status: Collection + ): Flux + + @Query("select * from orders where symbol = :symbol order by create_date desc limit 1") + fun findLastOrderBySymbol(@Param("symbol") symbol: String): Mono } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerImpl.kt index 4f87631b7..19ced19fd 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/MarketQueryHandlerImpl.kt @@ -1,216 +1,215 @@ -package co.nilin.opex.api.ports.postgres.impl - -import co.nilin.opex.api.core.inout.* -import co.nilin.opex.api.core.spi.MarketQueryHandler -import co.nilin.opex.api.core.spi.SymbolMapper -import co.nilin.opex.api.ports.postgres.dao.OrderRepository -import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository -import co.nilin.opex.api.ports.postgres.dao.TradeRepository -import co.nilin.opex.api.ports.postgres.model.OrderModel -import co.nilin.opex.api.ports.postgres.model.OrderStatusModel -import co.nilin.opex.api.ports.postgres.model.TradeTickerData -import co.nilin.opex.api.ports.postgres.util.* -import co.nilin.opex.matching.engine.core.model.OrderDirection -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrElse -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.stereotype.Component -import java.math.BigDecimal -import java.time.Instant -import java.time.LocalDateTime -import java.time.ZoneId -import java.time.ZoneOffset -import java.util.* -import kotlin.math.max -import kotlin.math.min - -@Component -class MarketQueryHandlerImpl( - private val orderRepository: OrderRepository, - private val tradeRepository: TradeRepository, - private val orderStatusRepository: OrderStatusRepository, - private val symbolMapper: SymbolMapper, -) : MarketQueryHandler { - - override suspend fun getTradeTickerData(startFrom: LocalDateTime): List { - return tradeRepository.tradeTicker(startFrom) - .collectList() - .awaitFirstOrElse { emptyList() } - .map { it.asPriceChangeResponse(Date().time, startFrom.toInstant(ZoneOffset.UTC).toEpochMilli()) } - - } - - override suspend fun getTradeTickerDataBySymbol(symbol: String, startFrom: LocalDateTime): PriceChangeResponse { - return tradeRepository.tradeTickerBySymbol(symbol, startFrom) - .awaitFirstOrNull() - ?.asPriceChangeResponse(Date().time, startFrom.toInstant(ZoneOffset.UTC).toEpochMilli()) - ?: PriceChangeResponse( - symbol = symbol, - openTime = Date().time, - closeTime = startFrom.toInstant(ZoneOffset.UTC).toEpochMilli() - ) - } - - override suspend fun openBidOrders(symbol: String, limit: Int): List { - return orderRepository.findBySymbolAndDirectionAndStatusSortDescendingByPrice( - symbol, - OrderDirection.BID, - limit, - listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) - ).collectList() - .awaitFirstOrElse { emptyList() } - .map { OrderBookResponse(it.price?.toBigDecimal(), it.quantity?.toBigDecimal()) } - } - - override suspend fun openAskOrders(symbol: String, limit: Int): List { - return orderRepository.findBySymbolAndDirectionAndStatusSortAscendingByPrice( - symbol, - OrderDirection.ASK, - limit, - listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) - ).collectList() - .awaitFirstOrElse { emptyList() } - .map { OrderBookResponse(it.price?.toBigDecimal(), it.quantity?.toBigDecimal()) } - } - - override suspend fun lastOrder(symbol: String): QueryOrderResponse? { - val order = orderRepository.findLastOrderBySymbol(symbol).awaitFirstOrNull() ?: return null - val status = orderStatusRepository.findMostRecentByOUID(order.ouid).awaitFirstOrNull() - return order.asQueryOrderResponse(status) - } - - override suspend fun recentTrades(symbol: String, limit: Int): Flow { - return tradeRepository.findBySymbolSortDescendingByCreateDate(symbol, limit) - .map { - val takerOrder = orderRepository.findByOuid(it.takerOuid).awaitFirst() - val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst() - val isMakerBuyer = makerOrder.direction == OrderDirection.BID - MarketTradeResponse( - it.symbol, - it.tradeId, - if (isMakerBuyer) it.makerPrice.toBigDecimal() else it.takerPrice.toBigDecimal(), - it.matchedQuantity.toBigDecimal(), - if (isMakerBuyer) - makerOrder.quoteQuantity!!.toBigDecimal() - else - takerOrder.quoteQuantity!!.toBigDecimal(), - Date.from(it.createDate.atZone(ZoneId.systemDefault()).toInstant()), - true, - isMakerBuyer - ) - } - } - - override suspend fun lastPrice(symbol: String?): List { - val list = if (symbol.isNullOrEmpty()) - tradeRepository.findAllGroupBySymbol() - else - tradeRepository.findBySymbolGroupBySymbol(symbol) - return list.collectList() - .awaitFirstOrElse { emptyList() } - .map { - val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst() - val apiSymbol = try { - symbolMapper.map(it.symbol) - } catch (e: Exception) { - it.symbol - } - val isMakerBuyer = makerOrder.direction == OrderDirection.BID - PriceTickerResponse( - apiSymbol, - if (isMakerBuyer) - min(it.takerPrice, it.makerPrice).toString() - else - max(it.takerPrice, it.makerPrice).toString() - ) - } - - } - - override suspend fun getCandleInfo( - symbol: String, - interval: String, - startTime: Long?, - endTime: Long?, - limit: Int - ): List { - val st = if (startTime == null) - tradeRepository.findFirstByCreateDate().awaitFirstOrNull()?.createDate - ?: LocalDateTime.now() - else - with(Instant.ofEpochMilli(startTime)) { - LocalDateTime.ofInstant(this, ZoneId.systemDefault()) - } - - val et = if (endTime == null) - tradeRepository.findLastByCreateDate().awaitFirstOrNull()?.createDate - ?: LocalDateTime.now() - else - with(Instant.ofEpochMilli(endTime)) { - LocalDateTime.ofInstant(this, ZoneId.systemDefault()) - } - - return tradeRepository.candleData(symbol, interval, st, et, limit) - .collectList() - .awaitFirstOrElse { emptyList() } - .map { - CandleData( - it.openTime, - it.closeTime, - it.open ?: 0.0, - it.close ?: 0.0, - it.high ?: 0.0, - it.low ?: 0.0, - it.volume ?: 0.0, - 0.0, - it.trades, - 0.0, - 0.0 - ) - } - } - - private fun OrderModel.asQueryOrderResponse(orderStatusModel: OrderStatusModel?) = QueryOrderResponse( - symbol, - ouid, - orderId ?: -1, - -1, - clientOrderId ?: "", - price!!.toBigDecimal(), - quantity!!.toBigDecimal(), - orderStatusModel?.executedQuantity?.toBigDecimal() ?: BigDecimal.ZERO, - orderStatusModel?.accumulativeQuoteQty?.toBigDecimal() ?: BigDecimal.ZERO, - orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW, - constraint!!.toTimeInForce(), - type!!.toApiOrderType(), - direction!!.toOrderSide(), - null, - null, - Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), - Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), - (orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW).isWorking(), - quoteQuantity!!.toBigDecimal() - ) - - private fun TradeTickerData.asPriceChangeResponse(openTime: Long, closeTime: Long) = PriceChangeResponse( - symbol, - priceChange ?: 0.0, - priceChangePercent ?: 0.0, - weightedAvgPrice ?: 0.0, - lastPrice ?: 0.0, - lastQty ?: 0.0, - bidPrice ?: 0.0, - askPrice ?: 0.0, - openPrice ?: 0.0, - highPrice ?: 0.0, - lowPrice ?: 0.0, - volume ?: 0.0, - openTime, - closeTime, - firstId ?: -1, - lastId ?: -1, - count ?: 0 - ) +package co.nilin.opex.api.ports.postgres.impl + +import co.nilin.opex.api.core.inout.* +import co.nilin.opex.api.core.spi.MarketQueryHandler +import co.nilin.opex.api.core.spi.SymbolMapper +import co.nilin.opex.api.ports.postgres.dao.OrderRepository +import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository +import co.nilin.opex.api.ports.postgres.dao.TradeRepository +import co.nilin.opex.api.ports.postgres.model.OrderModel +import co.nilin.opex.api.ports.postgres.model.OrderStatusModel +import co.nilin.opex.api.ports.postgres.model.TradeTickerData +import co.nilin.opex.api.ports.postgres.util.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.time.Instant +import java.time.LocalDateTime +import java.time.ZoneId +import java.time.ZoneOffset +import java.util.* +import kotlin.math.max +import kotlin.math.min + +@Component +class MarketQueryHandlerImpl( + private val orderRepository: OrderRepository, + private val tradeRepository: TradeRepository, + private val orderStatusRepository: OrderStatusRepository, + private val symbolMapper: SymbolMapper, +) : MarketQueryHandler { + + override suspend fun getTradeTickerData(startFrom: LocalDateTime): List { + return tradeRepository.tradeTicker(startFrom) + .collectList() + .awaitFirstOrElse { emptyList() } + .map { it.asPriceChangeResponse(Date().time, startFrom.toInstant(ZoneOffset.UTC).toEpochMilli()) } + + } + + override suspend fun getTradeTickerDataBySymbol(symbol: String, startFrom: LocalDateTime): PriceChangeResponse { + return tradeRepository.tradeTickerBySymbol(symbol, startFrom) + .awaitFirstOrNull() + ?.asPriceChangeResponse(Date().time, startFrom.toInstant(ZoneOffset.UTC).toEpochMilli()) + ?: PriceChangeResponse( + symbol = symbol, + openTime = Date().time, + closeTime = startFrom.toInstant(ZoneOffset.UTC).toEpochMilli() + ) + } + + override suspend fun openBidOrders(symbol: String, limit: Int): List { + return orderRepository.findBySymbolAndDirectionAndStatusSortDescendingByPrice( + symbol, + OrderDirection.BID, + limit, + listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) + ).collectList() + .awaitFirstOrElse { emptyList() } + .map { OrderBookResponse(it.price?.toBigDecimal(), it.quantity?.toBigDecimal()) } + } + + override suspend fun openAskOrders(symbol: String, limit: Int): List { + return orderRepository.findBySymbolAndDirectionAndStatusSortAscendingByPrice( + symbol, + OrderDirection.ASK, + limit, + listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) + ).collectList() + .awaitFirstOrElse { emptyList() } + .map { OrderBookResponse(it.price?.toBigDecimal(), it.quantity?.toBigDecimal()) } + } + + override suspend fun lastOrder(symbol: String): QueryOrderResponse? { + val order = orderRepository.findLastOrderBySymbol(symbol).awaitFirstOrNull() ?: return null + val status = orderStatusRepository.findMostRecentByOUID(order.ouid).awaitFirstOrNull() + return order.asQueryOrderResponse(status) + } + + override suspend fun recentTrades(symbol: String, limit: Int): Flow { + return tradeRepository.findBySymbolSortDescendingByCreateDate(symbol, limit) + .map { + val takerOrder = orderRepository.findByOuid(it.takerOuid).awaitFirst() + val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst() + val isMakerBuyer = makerOrder.direction == OrderDirection.BID + MarketTradeResponse( + it.symbol, + it.tradeId, + if (isMakerBuyer) it.makerPrice.toBigDecimal() else it.takerPrice.toBigDecimal(), + it.matchedQuantity.toBigDecimal(), + if (isMakerBuyer) + makerOrder.quoteQuantity!!.toBigDecimal() + else + takerOrder.quoteQuantity!!.toBigDecimal(), + Date.from(it.createDate.atZone(ZoneId.systemDefault()).toInstant()), + true, + isMakerBuyer + ) + } + } + + override suspend fun lastPrice(symbol: String?): List { + val list = if (symbol.isNullOrEmpty()) + tradeRepository.findAllGroupBySymbol() + else + tradeRepository.findBySymbolGroupBySymbol(symbol) + return list.collectList() + .awaitFirstOrElse { emptyList() } + .map { + val makerOrder = orderRepository.findByOuid(it.makerOuid).awaitFirst() + val apiSymbol = try { + symbolMapper.map(it.symbol) + } catch (e: Exception) { + it.symbol + } + val isMakerBuyer = makerOrder.direction == OrderDirection.BID + PriceTickerResponse( + apiSymbol, + if (isMakerBuyer) + min(it.takerPrice, it.makerPrice).toString() + else + max(it.takerPrice, it.makerPrice).toString() + ) + } + + } + + override suspend fun getCandleInfo( + symbol: String, + interval: String, + startTime: Long?, + endTime: Long?, + limit: Int + ): List { + val st = if (startTime == null) + tradeRepository.findFirstByCreateDate().awaitFirstOrNull()?.createDate + ?: LocalDateTime.now() + else + with(Instant.ofEpochMilli(startTime)) { + LocalDateTime.ofInstant(this, ZoneId.systemDefault()) + } + + val et = if (endTime == null) + tradeRepository.findLastByCreateDate().awaitFirstOrNull()?.createDate + ?: LocalDateTime.now() + else + with(Instant.ofEpochMilli(endTime)) { + LocalDateTime.ofInstant(this, ZoneId.systemDefault()) + } + + return tradeRepository.candleData(symbol, interval, st, et, limit) + .collectList() + .awaitFirstOrElse { emptyList() } + .map { + CandleData( + it.openTime, + it.closeTime, + it.open ?: 0.0, + it.close ?: 0.0, + it.high ?: 0.0, + it.low ?: 0.0, + it.volume ?: 0.0, + 0.0, + it.trades, + 0.0, + 0.0 + ) + } + } + + private fun OrderModel.asQueryOrderResponse(orderStatusModel: OrderStatusModel?) = QueryOrderResponse( + symbol, + ouid, + orderId ?: -1, + -1, + clientOrderId ?: "", + price!!.toBigDecimal(), + quantity!!.toBigDecimal(), + orderStatusModel?.executedQuantity?.toBigDecimal() ?: BigDecimal.ZERO, + orderStatusModel?.accumulativeQuoteQty?.toBigDecimal() ?: BigDecimal.ZERO, + orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW, + constraint!!.toTimeInForce(), + type!!.toApiOrderType(), + direction!!.toOrderSide(), + null, + null, + Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), + Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), + (orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW).isWorking(), + quoteQuantity!!.toBigDecimal() + ) + + private fun TradeTickerData.asPriceChangeResponse(openTime: Long, closeTime: Long) = PriceChangeResponse( + symbol, + priceChange ?: 0.0, + priceChangePercent ?: 0.0, + weightedAvgPrice ?: 0.0, + lastPrice ?: 0.0, + lastQty ?: 0.0, + bidPrice ?: 0.0, + askPrice ?: 0.0, + openPrice ?: 0.0, + highPrice ?: 0.0, + lowPrice ?: 0.0, + volume ?: 0.0, + openTime, + closeTime, + firstId ?: -1, + lastId ?: -1, + count ?: 0 + ) } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt index 45b7f1049..0abbf12e1 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt @@ -1,74 +1,74 @@ -package co.nilin.opex.api.ports.postgres.impl - -import co.nilin.opex.accountant.core.inout.OrderStatus -import co.nilin.opex.accountant.core.inout.RichOrder -import co.nilin.opex.accountant.core.inout.RichOrderUpdate -import co.nilin.opex.api.core.spi.OrderPersister -import co.nilin.opex.api.ports.postgres.dao.OrderRepository -import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository -import co.nilin.opex.api.ports.postgres.model.OrderModel -import co.nilin.opex.api.ports.postgres.model.OrderStatusModel -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component -import java.time.LocalDateTime - -@Component -class OrderPersisterImpl( - private val orderRepository: OrderRepository, - private val orderStatusRepository: OrderStatusRepository -) : OrderPersister { - - private val logger = LoggerFactory.getLogger(OrderPersisterImpl::class.java) - - override suspend fun save(order: RichOrder) { - orderRepository.save( - OrderModel( - null, - order.ouid, - order.uuid, - null, - order.pair, - order.orderId, - order.makerFee.toDouble(), - order.takerFee.toDouble(), - order.leftSideFraction.toDouble(), - order.rightSideFraction.toDouble(), - order.userLevel, - order.direction, - order.constraint, - order.type, - order.price.toDouble(), - order.quantity.toDouble(), - order.quoteQuantity.toDouble(), - LocalDateTime.now(), - LocalDateTime.now() - ) - ).awaitFirstOrNull() - logger.info("order ${order.ouid} saved") - - orderStatusRepository.save( - OrderStatusModel( - order.ouid, - order.executedQuantity.toDouble(), - order.accumulativeQuoteQty.toDouble(), - OrderStatus.NEW.code, - OrderStatus.NEW.orderOfAppearance - ) - ).awaitFirstOrNull() - logger.info("OrderStatus ${order.ouid} saved with status of 'NEW'") - } - - override suspend fun update(orderUpdate: RichOrderUpdate) { - orderStatusRepository.save( - OrderStatusModel( - orderUpdate.ouid, - orderUpdate.executedQuantity().toDouble(), - orderUpdate.accumulativeQuoteQuantity().toDouble(), - orderUpdate.status.code, - orderUpdate.status.orderOfAppearance - ) - ).awaitFirstOrNull() - logger.info("OrderStatus ${orderUpdate.ouid} updated with status of ${orderUpdate.status}") - } +package co.nilin.opex.api.ports.postgres.impl + +import co.nilin.opex.api.core.event.RichOrder +import co.nilin.opex.api.core.event.RichOrderUpdate +import co.nilin.opex.api.core.inout.OrderStatus +import co.nilin.opex.api.core.spi.OrderPersister +import co.nilin.opex.api.ports.postgres.dao.OrderRepository +import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository +import co.nilin.opex.api.ports.postgres.model.OrderModel +import co.nilin.opex.api.ports.postgres.model.OrderStatusModel +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import java.time.LocalDateTime + +@Component +class OrderPersisterImpl( + private val orderRepository: OrderRepository, + private val orderStatusRepository: OrderStatusRepository +) : OrderPersister { + + private val logger = LoggerFactory.getLogger(OrderPersisterImpl::class.java) + + override suspend fun save(order: RichOrder) { + orderRepository.save( + OrderModel( + null, + order.ouid, + order.uuid, + null, + order.pair, + order.orderId, + order.makerFee.toDouble(), + order.takerFee.toDouble(), + order.leftSideFraction.toDouble(), + order.rightSideFraction.toDouble(), + order.userLevel, + order.direction, + order.constraint, + order.type, + order.price.toDouble(), + order.quantity.toDouble(), + order.quoteQuantity.toDouble(), + LocalDateTime.now(), + LocalDateTime.now() + ) + ).awaitFirstOrNull() + logger.info("order ${order.ouid} saved") + + orderStatusRepository.save( + OrderStatusModel( + order.ouid, + order.executedQuantity.toDouble(), + order.accumulativeQuoteQty.toDouble(), + OrderStatus.NEW.code, + OrderStatus.NEW.orderOfAppearance + ) + ).awaitFirstOrNull() + logger.info("OrderStatus ${order.ouid} saved with status of 'NEW'") + } + + override suspend fun update(orderUpdate: RichOrderUpdate) { + orderStatusRepository.save( + OrderStatusModel( + orderUpdate.ouid, + orderUpdate.executedQuantity().toDouble(), + orderUpdate.accumulativeQuoteQuantity().toDouble(), + orderUpdate.status.code, + orderUpdate.status.orderOfAppearance + ) + ).awaitFirstOrNull() + logger.info("OrderStatus ${orderUpdate.ouid} updated with status of ${orderUpdate.status}") + } } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterImpl.kt index 309cf9488..019c00df4 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/TradePersisterImpl.kt @@ -1,42 +1,42 @@ -package co.nilin.opex.api.ports.postgres.impl - -import co.nilin.opex.accountant.core.inout.RichTrade -import co.nilin.opex.api.core.spi.TradePersister -import co.nilin.opex.api.ports.postgres.dao.TradeRepository -import co.nilin.opex.api.ports.postgres.model.TradeModel -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.slf4j.LoggerFactory -import org.springframework.stereotype.Component -import org.springframework.transaction.annotation.Transactional -import java.time.LocalDateTime - -@Component -class TradePersisterImpl(private val tradeRepository: TradeRepository) : TradePersister { - - private val logger = LoggerFactory.getLogger(TradePersisterImpl::class.java) - - @Transactional - override suspend fun save(trade: RichTrade) { - tradeRepository.save( - TradeModel( - null, - trade.id, - trade.pair, - trade.matchedQuantity.toDouble(), - trade.takerPrice.toDouble(), - trade.makerPrice.toDouble(), - trade.takerCommision.toDouble(), - trade.makerCommision.toDouble(), - trade.takerCommisionAsset, - trade.makerCommisionAsset, - trade.tradeDateTime, - trade.makerOuid, - trade.takerOuid, - trade.makerUuid, - trade.takerUuid, - LocalDateTime.now() - ) - ).awaitFirstOrNull() - logger.info("RichTrade ${trade.id} saved") - } +package co.nilin.opex.api.ports.postgres.impl + +import co.nilin.opex.api.core.event.RichTrade +import co.nilin.opex.api.core.spi.TradePersister +import co.nilin.opex.api.ports.postgres.dao.TradeRepository +import co.nilin.opex.api.ports.postgres.model.TradeModel +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.slf4j.LoggerFactory +import org.springframework.stereotype.Component +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime + +@Component +class TradePersisterImpl(private val tradeRepository: TradeRepository) : TradePersister { + + private val logger = LoggerFactory.getLogger(TradePersisterImpl::class.java) + + @Transactional + override suspend fun save(trade: RichTrade) { + tradeRepository.save( + TradeModel( + null, + trade.id, + trade.pair, + trade.matchedQuantity.toDouble(), + trade.takerPrice.toDouble(), + trade.makerPrice.toDouble(), + trade.takerCommision.toDouble(), + trade.makerCommision.toDouble(), + trade.takerCommisionAsset, + trade.makerCommisionAsset, + trade.tradeDateTime, + trade.makerOuid, + trade.takerOuid, + trade.makerUuid, + trade.takerUuid, + LocalDateTime.now() + ) + ).awaitFirstOrNull() + logger.info("RichTrade ${trade.id} saved") + } } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerImpl.kt index 8088bc2fc..656335de5 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/UserQueryHandlerImpl.kt @@ -1,139 +1,138 @@ -package co.nilin.opex.api.ports.postgres.impl - -import co.nilin.opex.api.core.inout.* -import co.nilin.opex.api.core.spi.UserQueryHandler -import co.nilin.opex.api.ports.postgres.dao.OrderRepository -import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository -import co.nilin.opex.api.ports.postgres.dao.TradeRepository -import co.nilin.opex.api.ports.postgres.model.OrderModel -import co.nilin.opex.api.ports.postgres.model.OrderStatusModel -import co.nilin.opex.api.ports.postgres.util.* -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.utility.error.data.OpexError -import co.nilin.opex.utility.error.data.OpexException -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.reactive.awaitFirst -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.stereotype.Component -import java.math.BigDecimal -import java.security.Principal -import java.time.ZoneId -import java.util.* - -@Component -class UserQueryHandlerImpl( - private val orderRepository: OrderRepository, - private val tradeRepository: TradeRepository, - private val orderStatusRepository: OrderStatusRepository -) : UserQueryHandler { - - override suspend fun queryOrder(principal: Principal, request: QueryOrderRequest): QueryOrderResponse? { - val order = (if (request.origClientOrderId != null) { - orderRepository.findBySymbolAndClientOrderId(request.symbol, request.origClientOrderId!!) - } else { - orderRepository.findBySymbolAndOrderId(request.symbol, request.orderId!!) - - }).awaitFirstOrNull() ?: return null - - if (order.uuid != principal.name) - throw OpexException(OpexError.Forbidden) - - return order.asQueryResponse(orderStatusRepository.findMostRecentByOUID(order.ouid).awaitFirstOrNull()) - } - - override suspend fun openOrders(principal: Principal, symbol: String?): Flow { - return orderRepository.findByUuidAndSymbolAndStatus( - principal.name, - symbol, - listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) - ).filter { orderModel -> orderModel.constraint != null } - .map { it.asQueryResponse(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) } - } - - override suspend fun allOrders(principal: Principal, allOrderRequest: AllOrderRequest): Flow { - return orderRepository.findByUuidAndSymbolAndTimeBetween( - principal.name, - allOrderRequest.symbol, - allOrderRequest.startTime, - allOrderRequest.endTime - ).filter { orderModel -> orderModel.constraint != null } - .map { it.asQueryResponse(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) } - } - - override suspend fun allTrades(principal: Principal, request: TradeRequest): Flow { - return tradeRepository.findByUuidAndSymbolAndTimeBetweenAndTradeIdGreaterThan( - principal.name, request.symbol, request.fromTrade, request.startTime, request.endTime - ).map { trade -> - val takerOrder = orderRepository.findByOuid(trade.takerOuid).awaitFirst() - val makerOrder = orderRepository.findByOuid(trade.makerOuid).awaitFirst() - val isMakerBuyer = makerOrder.direction == OrderDirection.BID - TradeResponse( - trade.symbol, - trade.tradeId, - if (trade.takerUuid == principal.name) { - takerOrder.orderId!! - } else { - makerOrder.orderId!! - }, - -1, - if (trade.takerUuid == principal.name) { - trade.takerPrice.toBigDecimal() - } else { - trade.makerPrice.toBigDecimal() - }, - trade.matchedQuantity.toBigDecimal(), - if (isMakerBuyer) { - makerOrder.quoteQuantity!!.toBigDecimal() - } else { - takerOrder.quoteQuantity!!.toBigDecimal() - }, - if (trade.takerUuid == principal.name) { - trade.takerCommision!!.toBigDecimal() - } else { - trade.makerCommision!!.toBigDecimal() - }, - if (trade.takerUuid == principal.name) { - trade.takerCommisionAsset!! - } else { - trade.makerCommisionAsset!! - }, - Date.from( - trade.createDate.atZone(ZoneId.systemDefault()).toInstant() - ), - if (trade.takerUuid == principal.name) { - OrderDirection.ASK == takerOrder.direction - } else { - OrderDirection.ASK == makerOrder.direction - }, - trade.makerUuid == principal.name, - true, - isMakerBuyer - ) - } - } - - - private fun OrderModel.asQueryResponse(orderStatusModel: OrderStatusModel?) = QueryOrderResponse( - symbol, - ouid, - orderId ?: -1, - -1, - clientOrderId ?: "", - price!!.toBigDecimal(), - quantity!!.toBigDecimal(), - orderStatusModel?.executedQuantity?.toBigDecimal() ?: BigDecimal.ZERO, - orderStatusModel?.accumulativeQuoteQty?.toBigDecimal() ?: BigDecimal.ZERO, - orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW, - constraint!!.toTimeInForce(), - type!!.toApiOrderType(), - direction!!.toOrderSide(), - null, - null, - Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), - Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), - (orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW).isWorking(), - quoteQuantity!!.toBigDecimal() - ) +package co.nilin.opex.api.ports.postgres.impl + +import co.nilin.opex.api.core.inout.* +import co.nilin.opex.api.core.spi.UserQueryHandler +import co.nilin.opex.api.ports.postgres.dao.OrderRepository +import co.nilin.opex.api.ports.postgres.dao.OrderStatusRepository +import co.nilin.opex.api.ports.postgres.dao.TradeRepository +import co.nilin.opex.api.ports.postgres.model.OrderModel +import co.nilin.opex.api.ports.postgres.model.OrderStatusModel +import co.nilin.opex.api.ports.postgres.util.* +import co.nilin.opex.utility.error.data.OpexError +import co.nilin.opex.utility.error.data.OpexException +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.reactive.awaitFirst +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.springframework.stereotype.Component +import java.math.BigDecimal +import java.security.Principal +import java.time.ZoneId +import java.util.* + +@Component +class UserQueryHandlerImpl( + private val orderRepository: OrderRepository, + private val tradeRepository: TradeRepository, + private val orderStatusRepository: OrderStatusRepository +) : UserQueryHandler { + + override suspend fun queryOrder(principal: Principal, request: QueryOrderRequest): QueryOrderResponse? { + val order = (if (request.origClientOrderId != null) { + orderRepository.findBySymbolAndClientOrderId(request.symbol, request.origClientOrderId!!) + } else { + orderRepository.findBySymbolAndOrderId(request.symbol, request.orderId!!) + + }).awaitFirstOrNull() ?: return null + + if (order.uuid != principal.name) + throw OpexException(OpexError.Forbidden) + + return order.asQueryResponse(orderStatusRepository.findMostRecentByOUID(order.ouid).awaitFirstOrNull()) + } + + override suspend fun openOrders(principal: Principal, symbol: String?): Flow { + return orderRepository.findByUuidAndSymbolAndStatus( + principal.name, + symbol, + listOf(OrderStatus.NEW.code, OrderStatus.PARTIALLY_FILLED.code) + ).filter { orderModel -> orderModel.constraint != null } + .map { it.asQueryResponse(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) } + } + + override suspend fun allOrders(principal: Principal, allOrderRequest: AllOrderRequest): Flow { + return orderRepository.findByUuidAndSymbolAndTimeBetween( + principal.name, + allOrderRequest.symbol, + allOrderRequest.startTime, + allOrderRequest.endTime + ).filter { orderModel -> orderModel.constraint != null } + .map { it.asQueryResponse(orderStatusRepository.findMostRecentByOUID(it.ouid).awaitFirstOrNull()) } + } + + override suspend fun allTrades(principal: Principal, request: TradeRequest): Flow { + return tradeRepository.findByUuidAndSymbolAndTimeBetweenAndTradeIdGreaterThan( + principal.name, request.symbol, request.fromTrade, request.startTime, request.endTime + ).map { trade -> + val takerOrder = orderRepository.findByOuid(trade.takerOuid).awaitFirst() + val makerOrder = orderRepository.findByOuid(trade.makerOuid).awaitFirst() + val isMakerBuyer = makerOrder.direction == OrderDirection.BID + TradeResponse( + trade.symbol, + trade.tradeId, + if (trade.takerUuid == principal.name) { + takerOrder.orderId!! + } else { + makerOrder.orderId!! + }, + -1, + if (trade.takerUuid == principal.name) { + trade.takerPrice.toBigDecimal() + } else { + trade.makerPrice.toBigDecimal() + }, + trade.matchedQuantity.toBigDecimal(), + if (isMakerBuyer) { + makerOrder.quoteQuantity!!.toBigDecimal() + } else { + takerOrder.quoteQuantity!!.toBigDecimal() + }, + if (trade.takerUuid == principal.name) { + trade.takerCommision!!.toBigDecimal() + } else { + trade.makerCommision!!.toBigDecimal() + }, + if (trade.takerUuid == principal.name) { + trade.takerCommisionAsset!! + } else { + trade.makerCommisionAsset!! + }, + Date.from( + trade.createDate.atZone(ZoneId.systemDefault()).toInstant() + ), + if (trade.takerUuid == principal.name) { + OrderDirection.ASK == takerOrder.direction + } else { + OrderDirection.ASK == makerOrder.direction + }, + trade.makerUuid == principal.name, + true, + isMakerBuyer + ) + } + } + + + private fun OrderModel.asQueryResponse(orderStatusModel: OrderStatusModel?) = QueryOrderResponse( + symbol, + ouid, + orderId ?: -1, + -1, + clientOrderId ?: "", + price!!.toBigDecimal(), + quantity!!.toBigDecimal(), + orderStatusModel?.executedQuantity?.toBigDecimal() ?: BigDecimal.ZERO, + orderStatusModel?.accumulativeQuoteQty?.toBigDecimal() ?: BigDecimal.ZERO, + orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW, + constraint!!.toTimeInForce(), + type!!.toApiOrderType(), + direction!!.toOrderSide(), + null, + null, + Date.from(createDate!!.atZone(ZoneId.systemDefault()).toInstant()), + Date.from(updateDate.atZone(ZoneId.systemDefault()).toInstant()), + (orderStatusModel?.status?.toOrderStatus() ?: OrderStatus.NEW).isWorking(), + quoteQuantity!!.toBigDecimal() + ) } \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/OrderModel.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/OrderModel.kt index b0a44a50b..c3950a26d 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/OrderModel.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/model/OrderModel.kt @@ -1,38 +1,38 @@ -package co.nilin.opex.api.ports.postgres.model - -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType -import org.springframework.data.annotation.Id -import org.springframework.data.annotation.Version -import org.springframework.data.relational.core.mapping.Column -import org.springframework.data.relational.core.mapping.Table -import java.time.LocalDateTime - -@Table("orders") -class OrderModel( - @Id var id: Long?, - @Column(value = "ouid") - val ouid: String, - val uuid: String, - @Column(value = "client_order_id") - val clientOrderId: String?, - val symbol: String, - @Column(value = "order_id") val orderId: Long?, - @Column("maker_fee") val makerFee: Double?, - @Column("taker_fee") val takerFee: Double?, - @Column("left_side_fraction") val leftSideFraction: Double?, - @Column("right_side_fraction") val rightSideFraction: Double?, - @Column("user_level") val userLevel: String?, - @Column("side") val direction: OrderDirection?, - @Column("match_constraint") val constraint: MatchConstraint?, - @Column("order_type") val type: OrderType?, - @Column("price") val price: Double?, - @Column("quantity") val quantity: Double?, - @Column("quote_quantity") val quoteQuantity: Double?, - @Column("create_date") val createDate: LocalDateTime?, - @Column("update_date") val updateDate: LocalDateTime, - @Version - @Column("version") - var version: Long? = null +package co.nilin.opex.api.ports.postgres.model + +import co.nilin.opex.api.core.inout.MatchConstraint +import co.nilin.opex.api.core.inout.MatchingOrderType +import co.nilin.opex.api.core.inout.OrderDirection +import org.springframework.data.annotation.Id +import org.springframework.data.annotation.Version +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table +import java.time.LocalDateTime + +@Table("orders") +class OrderModel( + @Id var id: Long?, + @Column(value = "ouid") + val ouid: String, + val uuid: String, + @Column(value = "client_order_id") + val clientOrderId: String?, + val symbol: String, + @Column(value = "order_id") val orderId: Long?, + @Column("maker_fee") val makerFee: Double?, + @Column("taker_fee") val takerFee: Double?, + @Column("left_side_fraction") val leftSideFraction: Double?, + @Column("right_side_fraction") val rightSideFraction: Double?, + @Column("user_level") val userLevel: String?, + @Column("side") val direction: OrderDirection?, + @Column("match_constraint") val constraint: MatchConstraint?, + @Column("order_type") val type: MatchingOrderType?, + @Column("price") val price: Double?, + @Column("quantity") val quantity: Double?, + @Column("quote_quantity") val quoteQuantity: Double?, + @Column("create_date") val createDate: LocalDateTime?, + @Column("update_date") val updateDate: LocalDateTime, + @Version + @Column("version") + var version: Long? = null ) \ No newline at end of file diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/EnumExtensions.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/EnumExtensions.kt index dc02cea43..1b38babd4 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/EnumExtensions.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/util/EnumExtensions.kt @@ -1,45 +1,39 @@ -package co.nilin.opex.api.ports.postgres.util - -import co.nilin.opex.api.core.inout.OrderSide -import co.nilin.opex.api.core.inout.OrderStatus -import co.nilin.opex.api.core.inout.TimeInForce -import co.nilin.opex.matching.engine.core.model.MatchConstraint -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.matching.engine.core.model.OrderType - -fun MatchConstraint.toTimeInForce(): TimeInForce { - if (this == MatchConstraint.FOK_BUDGET) - return TimeInForce.FOK - if (this == MatchConstraint.IOC_BUDGET) - return TimeInForce.IOC - return TimeInForce.valueOf(this.name) -} - - -fun TimeInForce.toMatchConstraint(): MatchConstraint { - return MatchConstraint.valueOf(this.name) -} - -fun OrderType.toApiOrderType(): co.nilin.opex.api.core.inout.OrderType { - if (this == OrderType.LIMIT_ORDER) - return co.nilin.opex.api.core.inout.OrderType.LIMIT - if (this == OrderType.MARKET_ORDER) - return co.nilin.opex.api.core.inout.OrderType.MARKET - throw IllegalArgumentException("OrderType $this is not supported!") -} - -fun OrderDirection.toOrderSide(): OrderSide { - if (this == OrderDirection.BID) - return OrderSide.BUY - return OrderSide.SELL -} - -fun OrderStatus.isWorking(): Boolean { - return listOf(OrderStatus.NEW, OrderStatus.PARTIALLY_FILLED).contains(this) -} - -fun Int.toOrderStatus(): OrderStatus { - val status = co.nilin.opex.accountant.core.inout.OrderStatus.values() - .find { s -> s.code == this } - return OrderStatus.valueOf(status!!.name) +package co.nilin.opex.api.ports.postgres.util + +import co.nilin.opex.api.core.inout.* + +fun MatchConstraint.toTimeInForce(): TimeInForce { + if (this == MatchConstraint.FOK_BUDGET) + return TimeInForce.FOK + if (this == MatchConstraint.IOC_BUDGET) + return TimeInForce.IOC + return TimeInForce.valueOf(this.name) +} + + +fun TimeInForce.toMatchConstraint(): MatchConstraint { + return MatchConstraint.valueOf(this.name) +} + +fun MatchingOrderType.toApiOrderType(): OrderType { + if (this == MatchingOrderType.LIMIT_ORDER) + return OrderType.LIMIT + if (this == MatchingOrderType.MARKET_ORDER) + return OrderType.MARKET + throw IllegalArgumentException("OrderType $this is not supported!") +} + +fun OrderDirection.toOrderSide(): OrderSide { + if (this == OrderDirection.BID) + return OrderSide.BUY + return OrderSide.SELL +} + +fun OrderStatus.isWorking(): Boolean { + return listOf(OrderStatus.NEW, OrderStatus.PARTIALLY_FILLED).contains(this) +} + +fun Int.toOrderStatus(): OrderStatus { + val status = OrderStatus.values().find { s -> s.code == this } + return OrderStatus.valueOf(status!!.name) } \ No newline at end of file diff --git a/api/pom.xml b/api/pom.xml index 701612a51..718ce68de 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -32,16 +32,6 @@ - - co.nilin.opex.matching.engine.core - matching-engine-core - ${project.version} - - - co.nilin.opex.accountant.core - accountant-core - ${project.version} - co.nilin.opex.api.core api-core diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt index 7a1923dd9..61186d837 100644 --- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt +++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaAdminConfig.kt @@ -1,11 +1,7 @@ package co.nilin.opex.matching.engine.ports.kafka.submitter.config -import org.apache.kafka.clients.admin.AdminClient -import org.apache.kafka.clients.admin.DescribeClusterOptions +import org.apache.kafka.clients.admin.AdminClientConfig import org.springframework.beans.factory.annotation.Value -import org.springframework.boot.actuate.health.AbstractHealthIndicator -import org.springframework.boot.actuate.health.Health -import org.springframework.boot.actuate.health.HealthIndicator import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.kafka.core.KafkaAdmin @@ -17,24 +13,9 @@ class KafkaAdminConfig( ) { @Bean - fun adminClient(admin: KafkaAdmin): AdminClient { - return AdminClient.create(admin.configurationProperties) - } - - @Bean - fun kafkaHealthIndicator(adminClient: AdminClient): HealthIndicator { - val options = DescribeClusterOptions().timeoutMs(1000) - return object : AbstractHealthIndicator() { - - override fun doHealthCheck(builder: Health.Builder) { - try { - adminClient.describeCluster(options) - builder.up().build() - }catch (e:Exception){ - builder.down(e).build() - } - } - } + fun admin(): KafkaAdmin? { + val configs = hashMapOf(AdminClientConfig.BOOTSTRAP_SERVERS_CONFIG to bootstrapServers) + return KafkaAdmin(configs) } } \ No newline at end of file diff --git a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt index e757f3bbc..438d63e95 100644 --- a/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt +++ b/matching-engine/matching-engine-ports/matching-engine-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/engine/ports/kafka/submitter/config/KafkaTopicConfig.kt @@ -20,7 +20,7 @@ class KafkaTopicConfig( private val logger = LoggerFactory.getLogger(KafkaTopicConfig::class.java) - @EventListener(ApplicationReadyEvent::class) + @Autowired fun createTopics(applicationContext: GenericApplicationContext) { logger.info("Creating kafka topics...") diff --git a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/MatchingGatewayApp.kt b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/MatchingGatewayApp.kt index e27767f48..6b91aa20e 100644 --- a/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/MatchingGatewayApp.kt +++ b/matching-gateway/matching-gateway-app/src/main/kotlin/co/nilin/opex/matching/gateway/app/MatchingGatewayApp.kt @@ -1,16 +1,18 @@ -package co.nilin.opex.matching.gateway.app - -import co.nilin.opex.utility.error.EnableOpexErrorHandler -import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.runApplication -import org.springframework.context.annotation.ComponentScan -import springfox.documentation.swagger2.annotations.EnableSwagger2 - -@SpringBootApplication -@ComponentScan("co.nilin.opex") -@EnableOpexErrorHandler -class MatchingGatewayApp - -fun main(args: Array) { - runApplication(*args) -} +package co.nilin.opex.matching.gateway.app + +import co.nilin.opex.utility.error.EnableOpexErrorHandler +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.context.annotation.ComponentScan +import org.springframework.scheduling.annotation.EnableScheduling +import springfox.documentation.swagger2.annotations.EnableSwagger2 + +@SpringBootApplication +@ComponentScan("co.nilin.opex") +@EnableOpexErrorHandler +@EnableScheduling +class MatchingGatewayApp + +fun main(args: Array) { + runApplication(*args) +} diff --git a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt index 2f194e229..13f497b30 100644 --- a/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt +++ b/matching-gateway/matching-gateway-port/matching-gateway-submitter-kafka/src/main/kotlin/co/nilin/opex/matching/gateway/ports/kafka/submitter/service/KafkaHealthIndicator.kt @@ -10,14 +10,17 @@ import org.springframework.stereotype.Component class KafkaHealthIndicator(private val adminClient: AdminClient) { private val logger = LoggerFactory.getLogger(KafkaHealthIndicator::class.java) - - var isHealthy = true private val options = DescribeClusterOptions().timeoutMs(1000) + private val healthyNodeSize = 3 + final var isHealthy = true + private set @Scheduled(fixedDelay = 5000, initialDelay = 5000) fun check() { isHealthy = try { - adminClient.describeCluster(options) + val description = adminClient.describeCluster(options) + if (description.nodes().get().size < healthyNodeSize) + throw IllegalStateException("Insufficient nodes") true } catch (e: Exception) { logger.warn("Kafka is not healthy!: ${e.message}") From 4d0111269ce2f73d9c5a168053390c954fcabe53 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 15 May 2022 13:50:26 +0430 Subject: [PATCH 6/9] Close #239 and #246 --- .../core/service/OrderManagerImpl.kt | 89 +++++++++++-------- .../listener/consumer/OrderKafkaListener.kt | 57 ++++++------ .../binance/controller/AccountController.kt | 4 + .../ports/postgres/impl/OrderPersisterImpl.kt | 22 +++-- 4 files changed, 98 insertions(+), 74 deletions(-) diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt index b8a2ef704..9b6f756bf 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/OrderManagerImpl.kt @@ -3,6 +3,7 @@ package co.nilin.opex.accountant.core.service import co.nilin.opex.accountant.core.api.OrderManager import co.nilin.opex.accountant.core.inout.OrderStatus import co.nilin.opex.accountant.core.inout.RichOrder +import co.nilin.opex.accountant.core.inout.RichOrderUpdate import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.Order import co.nilin.opex.accountant.core.spi.* @@ -105,40 +106,6 @@ open class OrderManagerImpl( return emptyList() } - private suspend fun publishRichOrder(order: Order, remainedQuantity: BigDecimal, status: OrderStatus? = null) { - richOrderPublisher.publish( - RichOrder( - order.id, - order.pair, - order.ouid, - order.uuid, - order.userLevel, - order.makerFee.toBigDecimal(), - order.takerFee.toBigDecimal(), - order.leftSideFraction.toBigDecimal(), - order.rightSideFraction.toBigDecimal(), - order.direction, - order.matchConstraint, - order.orderType, - order.origPrice, - order.origQuantity, - order.origPrice.multiply(order.origQuantity), - order.quantity.toBigDecimal().subtract(remainedQuantity) - .multiply(order.leftSideFraction.toBigDecimal()), - order.origPrice.multiply( - order.quantity.toBigDecimal().subtract(remainedQuantity) - ), - status?.code ?: if (remainedQuantity.compareTo(BigDecimal.ZERO) == 0) { - OrderStatus.FILLED.code - } else if (remainedQuantity.compareTo(order.quantity.toBigDecimal()) == 0) { - OrderStatus.NEW.code - } else { - OrderStatus.PARTIALLY_FILLED.code - } - ) - ) - } - override suspend fun handleUpdateOrder(updatedOrderEvent: UpdatedOrderEvent): List { TODO("Not yet implemented") } @@ -175,7 +142,15 @@ open class OrderManagerImpl( //update order status order.status = OrderStatus.REJECTED.code orderPersister.save(order) - publishRichOrder(order, order.quantity.toBigDecimal(), OrderStatus.REJECTED) + richOrderPublisher.publish( + RichOrderUpdate( + order.ouid, + order.price.toBigDecimal(), + order.quantity.toBigDecimal(), + BigDecimal.ZERO, + OrderStatus.REJECTED + ) + ) return financialActionPersister.persist(listOf(financialAction)) } @@ -211,7 +186,49 @@ open class OrderManagerImpl( //update order status order.status = OrderStatus.CANCELED.code orderPersister.save(order) - publishRichOrder(order, cancelOrderEvent.quantity.toBigDecimal(), OrderStatus.CANCELED) + richOrderPublisher.publish( + RichOrderUpdate( + order.ouid, + order.price.toBigDecimal(), + order.quantity.toBigDecimal(), + cancelOrderEvent.remainedQuantity.toBigDecimal(), + OrderStatus.REJECTED + ) + ) return financialActionPersister.persist(listOf(financialAction)) } + + private suspend fun publishRichOrder(order: Order, remainedQuantity: BigDecimal, status: OrderStatus? = null) { + richOrderPublisher.publish( + RichOrder( + order.id, + order.pair, + order.ouid, + order.uuid, + order.userLevel, + order.makerFee.toBigDecimal(), + order.takerFee.toBigDecimal(), + order.leftSideFraction.toBigDecimal(), + order.rightSideFraction.toBigDecimal(), + order.direction, + order.matchConstraint, + order.orderType, + order.origPrice, + order.origQuantity, + order.origPrice.multiply(order.origQuantity), + order.quantity.toBigDecimal().subtract(remainedQuantity) + .multiply(order.leftSideFraction.toBigDecimal()), + order.origPrice.multiply( + order.quantity.toBigDecimal().subtract(remainedQuantity) + ), + status?.code ?: if (remainedQuantity.compareTo(BigDecimal.ZERO) == 0) { + OrderStatus.FILLED.code + } else if (remainedQuantity.compareTo(order.quantity.toBigDecimal()) == 0) { + OrderStatus.NEW.code + } else { + OrderStatus.PARTIALLY_FILLED.code + } + ) + ) + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/OrderKafkaListener.kt b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/OrderKafkaListener.kt index fe0ab8f0d..3123608f7 100644 --- a/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/OrderKafkaListener.kt +++ b/accountant/accountant-ports/accountant-eventlistener-kafka/src/main/kotlin/co/nilin/opex/accountant/ports/kafka/listener/consumer/OrderKafkaListener.kt @@ -1,30 +1,29 @@ -package co.nilin.opex.accountant.ports.kafka.listener.consumer - -import co.nilin.opex.accountant.ports.kafka.listener.inout.OrderSubmitRequest -import co.nilin.opex.accountant.ports.kafka.listener.spi.OrderSubmitRequestListener -import org.apache.kafka.clients.consumer.ConsumerRecord -import org.springframework.kafka.listener.MessageListener -import org.springframework.stereotype.Component - -@Component -class OrderKafkaListener : MessageListener { - - val orderListeners = arrayListOf() - - override fun onMessage(data: ConsumerRecord) { - orderListeners.forEach { tl -> - tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) - } - - } - - fun addOrderListener(tl: OrderSubmitRequestListener) { - orderListeners.add(tl) - } - - fun removeOrderListener(tl: OrderSubmitRequestListener) { - orderListeners.removeIf { item -> - item.id() == tl.id() - } - } +package co.nilin.opex.accountant.ports.kafka.listener.consumer + +import co.nilin.opex.accountant.ports.kafka.listener.inout.OrderSubmitRequest +import co.nilin.opex.accountant.ports.kafka.listener.spi.OrderSubmitRequestListener +import org.apache.kafka.clients.consumer.ConsumerRecord +import org.springframework.kafka.listener.MessageListener +import org.springframework.stereotype.Component + +@Component +class OrderKafkaListener : MessageListener { + + val orderListeners = arrayListOf() + + override fun onMessage(data: ConsumerRecord) { + orderListeners.forEach { tl -> + tl.onOrder(data.value(), data.partition(), data.offset(), data.timestamp()) + } + } + + fun addOrderListener(tl: OrderSubmitRequestListener) { + orderListeners.add(tl) + } + + fun removeOrderListener(tl: OrderSubmitRequestListener) { + orderListeners.removeIf { item -> + item.id() == tl.id() + } + } } \ No newline at end of file diff --git a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt index 0a6a791b7..f8f54f5b2 100644 --- a/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt +++ b/api/api-ports/api-binance-rest/src/main/kotlin/co/nilin/opex/api/ports/binance/controller/AccountController.kt @@ -77,6 +77,7 @@ class AccountController( @JsonInclude(JsonInclude.Include.NON_NULL) data class QueryOrderResponse( val symbol: String, + val ouid: String, val orderId: Long, val orderListId: Long, //Unless part of an OCO, the value will always be -1. val clientOrderId: String, @@ -304,6 +305,7 @@ class AccountController( return QueryOrderResponse( symbol, + response.ouid, response.orderId, response.orderListId, response.clientOrderId, @@ -363,6 +365,7 @@ class AccountController( .map { response -> QueryOrderResponse( symbol ?: "", + response.ouid, response.orderId, response.orderListId, response.clientOrderId, @@ -427,6 +430,7 @@ class AccountController( .map { response -> QueryOrderResponse( symbol ?: "", + response.ouid, response.orderId, response.orderListId, response.clientOrderId, diff --git a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt index 0abbf12e1..cc520af06 100644 --- a/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt +++ b/api/api-ports/api-persister-postgres/src/main/kotlin/co/nilin/opex/api/ports/postgres/impl/OrderPersisterImpl.kt @@ -60,15 +60,19 @@ class OrderPersisterImpl( } override suspend fun update(orderUpdate: RichOrderUpdate) { - orderStatusRepository.save( - OrderStatusModel( - orderUpdate.ouid, - orderUpdate.executedQuantity().toDouble(), - orderUpdate.accumulativeQuoteQuantity().toDouble(), - orderUpdate.status.code, - orderUpdate.status.orderOfAppearance - ) - ).awaitFirstOrNull() + try { + orderStatusRepository.save( + OrderStatusModel( + orderUpdate.ouid, + orderUpdate.executedQuantity().toDouble(), + orderUpdate.accumulativeQuoteQuantity().toDouble(), + orderUpdate.status.code, + orderUpdate.status.orderOfAppearance + ) + ).awaitFirstOrNull() + } catch (e: Exception) { + logger.error("Error updating order status: ${e.message}") + } logger.info("OrderStatus ${orderUpdate.ouid} updated with status of ${orderUpdate.status}") } } \ No newline at end of file From fc4c2298e3ef4db5018032581a65e6e51772e068 Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 15 May 2022 14:10:43 +0430 Subject: [PATCH 7/9] Removed rate and platform coin from calculation --- .../app/config/InitializeService.kt | 3 +- .../src/main/resources/application.yml | 1 - .../core/service/FeeCalculatorImpl.kt | 116 +++--------- .../core/service/TradeManagerImplTest.kt | 17 +- .../postgres/dao/PairConfigRepository.kt | 6 +- .../postgres/impl/PairConfigLoaderImpl.kt | 148 ++++++++-------- .../postgres/impl/PairStaticRateLoaderImpl.kt | 17 -- .../ports/postgres/model/PairConfigModel.kt | 27 ++- .../src/main/resources/schema.sql | 167 +++++++++--------- 9 files changed, 201 insertions(+), 301 deletions(-) delete mode 100644 accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairStaticRateLoaderImpl.kt diff --git a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt index fb7d4a172..d0767d767 100644 --- a/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt +++ b/accountant/accountant-app/src/main/kotlin/co/nilin/opex/accountant/app/config/InitializeService.kt @@ -33,8 +33,7 @@ class InitializeService( it.leftSide, it.rightSide, leftSideFraction, - rightSideFraction, - 0.0 + rightSideFraction ).awaitSingleOrNull() it.feeConfigs.forEach { f -> runCatching { diff --git a/accountant/accountant-app/src/main/resources/application.yml b/accountant/accountant-app/src/main/resources/application.yml index 9bf8bbb6a..028a81a7c 100644 --- a/accountant/accountant-app/src/main/resources/application.yml +++ b/accountant/accountant-app/src/main/resources/application.yml @@ -45,7 +45,6 @@ spring: config: import: vault://secret/${spring.application.name} app: - coin: IRT address: 1 wallet: url: lb://opex-wallet/ diff --git a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt index 75c6eba07..e1ccfe5e4 100644 --- a/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt +++ b/accountant/accountant-core/src/main/kotlin/co/nilin/opex/accountant/core/service/FeeCalculatorImpl.kt @@ -4,22 +4,14 @@ import co.nilin.opex.accountant.core.api.FeeCalculator import co.nilin.opex.accountant.core.model.FeeFinancialActions import co.nilin.opex.accountant.core.model.FinancialAction import co.nilin.opex.accountant.core.model.Order -import co.nilin.opex.accountant.core.spi.PairStaticRateLoader -import co.nilin.opex.accountant.core.spi.WalletProxy import co.nilin.opex.matching.engine.core.eventh.events.TradeEvent import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component -import java.math.BigDecimal import java.time.LocalDateTime @Component -class FeeCalculatorImpl( - private val pairStaticRateLoader: PairStaticRateLoader, - private val walletProxy: WalletProxy, - @Value("\${app.coin}") private val platformCoin: String, - @Value("\${app.address}") private val platformAddress: String -) : FeeCalculator { +class FeeCalculatorImpl(@Value("\${app.address}") private val platformAddress: String) : FeeCalculator { private val logger = LoggerFactory.getLogger(FeeCalculatorImpl::class.java) @@ -32,13 +24,6 @@ class FeeCalculatorImpl( ): FeeFinancialActions { logger.info("Start fee calculation for trade ${trade.takerUuid}") - // TODO cache this - val leftSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.leftSideName) ?: 0.0 - val rightSidePCRate = pairStaticRateLoader.calculateStaticRate(platformCoin, trade.pair.rightSideName) ?: 0.0 - - val makerPCFeeCoefficient = if (makerOrder.isAsk()) leftSidePCRate else rightSidePCRate - val takerPCFeeCoefficient = if (takerOrder.isAsk()) leftSidePCRate else rightSidePCRate - val makerMatchedAmount = if (makerOrder.isAsk()) { trade.matchedQuantity.toBigDecimal().multiply(makerOrder.leftSideFraction.toBigDecimal()) } else { @@ -58,84 +43,33 @@ class FeeCalculatorImpl( } //calculate maker fee - val makerFee = makerOrder.makerFee - val makerTotalFeeWithPlatformCoin = takerMatchedAmount - .multiply(makerFee.toBigDecimal()) - .multiply(makerPCFeeCoefficient.toBigDecimal()) - - //check if maker uuid can pay the fee with platform coin - val canMakerFulfil = runCatching { - walletProxy.canFulfil(platformCoin, "main", trade.makerUuid, makerTotalFeeWithPlatformCoin) - }.onFailure { logger.error(it.message) }.getOrElse { false } - - val makerFeeAction = if (makerTotalFeeWithPlatformCoin > BigDecimal.ZERO && canMakerFulfil) { - FinancialAction( - makerParentFA, - TradeEvent::class.simpleName!!, - trade.takerOuid, - platformCoin, - makerTotalFeeWithPlatformCoin, - trade.makerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - } else { - FinancialAction( - makerParentFA, - TradeEvent::class.simpleName!!, - trade.takerOuid, - if (takerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, - takerMatchedAmount.multiply(makerFee.toBigDecimal()), - trade.makerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - } + val makerFeeAction = FinancialAction( + makerParentFA, + TradeEvent::class.simpleName!!, + trade.takerOuid, + if (takerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + takerMatchedAmount.multiply(makerOrder.makerFee.toBigDecimal()), + trade.makerUuid, + "main", + platformAddress, + "exchange", + LocalDateTime.now() + ) logger.info("trade event makerFeeAction $makerFeeAction") //calculate taker fee - val takerFee = takerOrder.takerFee - val takerTotalFeeWithPlatformCoin = takerMatchedAmount - .multiply(takerFee.toBigDecimal()) - .multiply(takerPCFeeCoefficient.toBigDecimal()) - - //check if taker uuid can pay the fee with platform coin - val canTakerFulfil = runCatching { - walletProxy.canFulfil(platformCoin, "main", trade.takerUuid, takerTotalFeeWithPlatformCoin) - }.onFailure { logger.error(it.message) }.getOrElse { false } - - val takerFeeAction = if (takerTotalFeeWithPlatformCoin > BigDecimal.ZERO && canTakerFulfil) { - FinancialAction( - takerParentFA, - TradeEvent::class.simpleName!!, - trade.makerOuid, - if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, - takerTotalFeeWithPlatformCoin, - trade.takerUuid, - "main", - platformAddress, - "", - LocalDateTime.now() - ) - } else { - FinancialAction( - takerParentFA, - TradeEvent::class.simpleName!!, - trade.makerOuid, - if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, - makerMatchedAmount.multiply(takerFee.toBigDecimal()), - trade.takerUuid, - "main", - platformAddress, - "exchange", - LocalDateTime.now() - ) - - } + val takerFeeAction = FinancialAction( + takerParentFA, + TradeEvent::class.simpleName!!, + trade.makerOuid, + if (makerOrder.isAsk()) trade.pair.leftSideName else trade.pair.rightSideName, + makerMatchedAmount.multiply(takerOrder.takerFee.toBigDecimal()), + trade.takerUuid, + "main", + platformAddress, + "exchange", + LocalDateTime.now() + ) logger.info("trade event takerFeeAction $takerFeeAction") return FeeFinancialActions(makerFeeAction, takerFeeAction) diff --git a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt index 548f0d73c..c03c25330 100644 --- a/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt +++ b/accountant/accountant-core/src/test/kotlin/co/nilin/opex/accountant/core/service/TradeManagerImplTest.kt @@ -36,12 +36,6 @@ internal class TradeManagerImplTest { @Mock lateinit var pairConfigLoader: PairConfigLoader - @Mock - lateinit var pairStaticRateLoader: PairStaticRateLoader - - @Mock - lateinit var walletProxy: WalletProxy - @Mock lateinit var tempEventPersister: TempEventPersister @@ -56,7 +50,6 @@ internal class TradeManagerImplTest { private val orderManager: OrderManager private val tradeManager: TradeManager - private val feeCalculator: FeeCalculator init { MockitoAnnotations.openMocks(this) @@ -71,13 +64,6 @@ internal class TradeManagerImplTest { richOrderPublisher ) - feeCalculator = FeeCalculatorImpl( - pairStaticRateLoader, - walletProxy, - "pcoin", - "0x0" - ) - tradeManager = TradeManagerImpl( financialActionPersister, financeActionLoader, @@ -85,8 +71,9 @@ internal class TradeManagerImplTest { tempEventPersister, richTradePublisher, richOrderPublisher, - feeCalculator + FeeCalculatorImpl("0x0") ) + runBlocking { Mockito.`when`(tempEventPersister.loadTempEvents(ArgumentMatchers.anyString())).thenReturn(emptyList()) } diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairConfigRepository.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairConfigRepository.kt index c3d9ca417..4ebd1eee2 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairConfigRepository.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/dao/PairConfigRepository.kt @@ -8,13 +8,13 @@ import reactor.core.publisher.Mono @Repository interface PairConfigRepository : ReactiveCrudRepository { - @Query("insert into pair_config values (:pair, :leftSide, :rightSide, :leftSideFraction, :rightSideFraction, :rate) on conflict do nothing") + + @Query("insert into pair_config values (:pair, :leftSide, :rightSide, :leftSideFraction, :rightSideFraction) on conflict do nothing") fun insert( pair: String, leftSide: String, rightSide: String, leftSideFraction: Double, - rightSideFraction: Double, - rate: Double + rightSideFraction: Double ): Mono } diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt index 947d8f898..66ab2a7f5 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairConfigLoaderImpl.kt @@ -1,75 +1,75 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.model.PairConfig -import co.nilin.opex.accountant.core.model.PairFeeConfig -import co.nilin.opex.accountant.core.spi.PairConfigLoader -import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository -import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository -import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel -import co.nilin.opex.matching.engine.core.model.OrderDirection -import co.nilin.opex.utility.error.data.OpexError -import co.nilin.opex.utility.error.data.OpexException -import kotlinx.coroutines.reactive.awaitFirstOrElse -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.stereotype.Component - -@Component -class PairConfigLoaderImpl( - val pairConfigRepository: PairConfigRepository, val pairFeeConfigRepository: PairFeeConfigRepository -) : PairConfigLoader { - - override suspend fun loadPairConfigs(): List { - return pairConfigRepository.findAll() - .collectList() - .awaitFirstOrElse { emptyList() } - .map { - PairConfig( - it.pair, - it.leftSideWalletSymbol, - it.rightSideWalletSymbol, - it.leftSideFraction, - it.rightSideFraction - ) - } - } - - override suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig { - val pairConfig = pairConfigRepository - .findById(pair).awaitFirstOrElse { - val error = OpexError.InvalidPair - throw OpexException(error, String.format(error.message!!, pair)) - } - var pairFeeConfig: PairFeeConfigModel? - if (userLevel.isEmpty()) { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, "*") - .awaitFirstOrElse { - val error = OpexError.InvalidPair - throw OpexException(error, String.format(error.message!!, pair)) - } - } else { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, userLevel) - .awaitFirstOrNull() - if (pairFeeConfig == null) { - pairFeeConfig = pairFeeConfigRepository - .findByPairAndDirectionAndUserLevel(pair, direction, "*") - .awaitFirstOrElse { - val error = OpexError.InvalidPairFee - throw OpexException(error, String.format(error.message!!, pair)) - } - } - } - - return PairFeeConfig( - PairConfig( - pair, - pairConfig.leftSideWalletSymbol, - pairConfig.rightSideWalletSymbol, - pairConfig.leftSideFraction, - pairConfig.rightSideFraction - ), pairFeeConfig!!.direction, pairFeeConfig.userLevel, pairFeeConfig.makerFee, pairFeeConfig.takerFee - ) - - } +package co.nilin.opex.accountant.ports.postgres.impl + +import co.nilin.opex.accountant.core.model.PairConfig +import co.nilin.opex.accountant.core.model.PairFeeConfig +import co.nilin.opex.accountant.core.spi.PairConfigLoader +import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository +import co.nilin.opex.accountant.ports.postgres.dao.PairFeeConfigRepository +import co.nilin.opex.accountant.ports.postgres.model.PairFeeConfigModel +import co.nilin.opex.matching.engine.core.model.OrderDirection +import co.nilin.opex.utility.error.data.OpexError +import co.nilin.opex.utility.error.data.OpexException +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitFirstOrNull +import org.springframework.stereotype.Component + +@Component +class PairConfigLoaderImpl( + val pairConfigRepository: PairConfigRepository, + val pairFeeConfigRepository: PairFeeConfigRepository +) : PairConfigLoader { + + override suspend fun loadPairConfigs(): List { + return pairConfigRepository.findAll() + .collectList() + .awaitFirstOrElse { emptyList() } + .map { + PairConfig( + it.pair, + it.leftSideWalletSymbol, + it.rightSideWalletSymbol, + it.leftSideFraction, + it.rightSideFraction + ) + } + } + + override suspend fun load(pair: String, direction: OrderDirection, userLevel: String): PairFeeConfig { + val pairConfig = pairConfigRepository + .findById(pair).awaitFirstOrElse { + val error = OpexError.InvalidPair + throw OpexException(error, String.format(error.message!!, pair)) + } + var pairFeeConfig: PairFeeConfigModel? + if (userLevel.isEmpty()) { + pairFeeConfig = pairFeeConfigRepository + .findByPairAndDirectionAndUserLevel(pair, direction, "*") + .awaitFirstOrElse { + val error = OpexError.InvalidPair + throw OpexException(error, String.format(error.message!!, pair)) + } + } else { + pairFeeConfig = pairFeeConfigRepository + .findByPairAndDirectionAndUserLevel(pair, direction, userLevel) + .awaitFirstOrNull() + if (pairFeeConfig == null) { + pairFeeConfig = pairFeeConfigRepository + .findByPairAndDirectionAndUserLevel(pair, direction, "*") + .awaitFirstOrElse { + val error = OpexError.InvalidPairFee + throw OpexException(error, String.format(error.message!!, pair)) + } + } + } + + return PairFeeConfig( + PairConfig( + pair, + pairConfig.leftSideWalletSymbol, + pairConfig.rightSideWalletSymbol, + pairConfig.leftSideFraction, + pairConfig.rightSideFraction + ), pairFeeConfig!!.direction, pairFeeConfig.userLevel, pairFeeConfig.makerFee, pairFeeConfig.takerFee + ) + } } \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairStaticRateLoaderImpl.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairStaticRateLoaderImpl.kt deleted file mode 100644 index 1f0a9ae3d..000000000 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/impl/PairStaticRateLoaderImpl.kt +++ /dev/null @@ -1,17 +0,0 @@ -package co.nilin.opex.accountant.ports.postgres.impl - -import co.nilin.opex.accountant.core.spi.PairStaticRateLoader -import co.nilin.opex.accountant.ports.postgres.dao.PairConfigRepository -import kotlinx.coroutines.reactive.awaitFirstOrElse -import org.springframework.stereotype.Component - -@Component -class PairStaticRateLoaderImpl(val pairConfigRepository: PairConfigRepository) : PairStaticRateLoader { - - override suspend fun calculateStaticRate(leftSide: String, rightSide: String): Double? { - val pairConfig = pairConfigRepository - .findById("${leftSide}_$rightSide") - .awaitFirstOrElse { throw IllegalArgumentException("${leftSide}_$rightSide is not available") } - return pairConfig.rate - } -} \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairConfigModel.kt b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairConfigModel.kt index 131f13268..fa07a137d 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairConfigModel.kt +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/kotlin/co/nilin/opex/accountant/ports/postgres/model/PairConfigModel.kt @@ -1,15 +1,14 @@ -package co.nilin.opex.accountant.ports.postgres.model - -import org.springframework.data.annotation.Id -import org.springframework.data.relational.core.mapping.Column -import org.springframework.data.relational.core.mapping.Table - -@Table("pair_config") -data class PairConfigModel( - @Id val pair: String, - @Column("left_side_wallet_symbol") val leftSideWalletSymbol: String, //can be same as pair left side - @Column("right_side_wallet_symbol") val rightSideWalletSymbol: String, //can be same as pair right side - @Column("left_side_fraction") val leftSideFraction: Double, - @Column("right_side_fraction") val rightSideFraction: Double, - @Column("rate") val rate: Double +package co.nilin.opex.accountant.ports.postgres.model + +import org.springframework.data.annotation.Id +import org.springframework.data.relational.core.mapping.Column +import org.springframework.data.relational.core.mapping.Table + +@Table("pair_config") +data class PairConfigModel( + @Id val pair: String, + @Column("left_side_wallet_symbol") val leftSideWalletSymbol: String, //can be same as pair left side + @Column("right_side_wallet_symbol") val rightSideWalletSymbol: String, //can be same as pair right side + @Column("left_side_fraction") val leftSideFraction: Double, + @Column("right_side_fraction") val rightSideFraction: Double ) \ No newline at end of file diff --git a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql index c87930e62..7db637242 100644 --- a/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql +++ b/accountant/accountant-ports/accountant-persister-postgres/src/main/resources/schema.sql @@ -1,84 +1,83 @@ -CREATE TABLE IF NOT EXISTS orders -( - id SERIAL PRIMARY KEY, - ouid VARCHAR(72) NOT NULL UNIQUE, - uuid VARCHAR(72) NOT NULL, - pair VARCHAR(72) NOT NULL, - matching_engine_id INTEGER, - maker_fee DECIMAL NOT NULL, - taker_fee DECIMAL NOT NULL, - left_side_fraction DECIMAL NOT NULL, - right_side_fraction DECIMAL NOT NULL, - user_level VARCHAR(20) NOT NULL, - direction VARCHAR(20) NOT NULL, - match_constraint VARCHAR(30) NOT NULL, - order_type VARCHAR(30) NOT NULL, - price DECIMAL NOT NULL, - quantity DECIMAL NOT NULL, - filled_quantity DECIMAL NOT NULL, - orig_price DECIMAL NOT NULL, - orig_quantity DECIMAL NOT NULL, - filled_orig_quantity DECIMAL NOT NULL, - first_transfer_amount DECIMAL NOT NULL, - remained_transfer_amount DECIMAL NOT NULL, - status INTEGER NOT NULL, - agent VARCHAR(20), - ip VARCHAR(11), - create_date TIMESTAMP NOT NULL -); - -CREATE TABLE IF NOT EXISTS fi_actions -( - id SERIAL PRIMARY KEY, - parent_id INTEGER, - event_type VARCHAR(72) NOT NULL, - pointer VARCHAR(72) NOT NULL, - symbol VARCHAR(36) NOT NULL, - amount DECIMAL NOT NULL, - sender VARCHAR(36) NOT NULL, - sender_wallet_type VARCHAR(36) NOT NULL, - receiver VARCHAR(36) NOT NULL, - receiver_wallet_type VARCHAR(36) NOT NULL, - agent VARCHAR(20), - ip VARCHAR(11), - create_date TIMESTAMP NOT NULL, - status VARCHAR(20), - retry_count DECIMAL, - last_try_date TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS pair_config -( - pair VARCHAR(72) PRIMARY KEY, - left_side_wallet_symbol VARCHAR(36) NOT NULL, - right_side_wallet_symbol VARCHAR(36) NOT NULL, - left_side_fraction DECIMAL NOT NULL, - right_side_fraction DECIMAL NOT NULL, - rate DECIMAL NOT NULL, - UNIQUE ( - left_side_wallet_symbol, - right_side_wallet_symbol - ) -); - -CREATE TABLE IF NOT EXISTS pair_fee_config -( - id SERIAL PRIMARY KEY, - pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair), - direction VARCHAR(36) NOT NULL, - user_level VARCHAR(36) NOT NULL, - maker_fee DECIMAL NOT NULL, - taker_fee DECIMAL NOT NULL, - UNIQUE (direction, user_level, pair_config_id) -); - -CREATE TABLE IF NOT EXISTS temp_events -( - id SERIAL PRIMARY KEY, - ouid VARCHAR(72) NOT NULL, - event_type VARCHAR(72) NOT NULL, - event_body TEXT NOT NULL, - event_date TIMESTAMP NOT NULL -); - -COMMIT; +CREATE TABLE IF NOT EXISTS orders +( + id SERIAL PRIMARY KEY, + ouid VARCHAR(72) NOT NULL UNIQUE, + uuid VARCHAR(72) NOT NULL, + pair VARCHAR(72) NOT NULL, + matching_engine_id INTEGER, + maker_fee DECIMAL NOT NULL, + taker_fee DECIMAL NOT NULL, + left_side_fraction DECIMAL NOT NULL, + right_side_fraction DECIMAL NOT NULL, + user_level VARCHAR(20) NOT NULL, + direction VARCHAR(20) NOT NULL, + match_constraint VARCHAR(30) NOT NULL, + order_type VARCHAR(30) NOT NULL, + price DECIMAL NOT NULL, + quantity DECIMAL NOT NULL, + filled_quantity DECIMAL NOT NULL, + orig_price DECIMAL NOT NULL, + orig_quantity DECIMAL NOT NULL, + filled_orig_quantity DECIMAL NOT NULL, + first_transfer_amount DECIMAL NOT NULL, + remained_transfer_amount DECIMAL NOT NULL, + status INTEGER NOT NULL, + agent VARCHAR(20), + ip VARCHAR(11), + create_date TIMESTAMP NOT NULL +); + +CREATE TABLE IF NOT EXISTS fi_actions +( + id SERIAL PRIMARY KEY, + parent_id INTEGER, + event_type VARCHAR(72) NOT NULL, + pointer VARCHAR(72) NOT NULL, + symbol VARCHAR(36) NOT NULL, + amount DECIMAL NOT NULL, + sender VARCHAR(36) NOT NULL, + sender_wallet_type VARCHAR(36) NOT NULL, + receiver VARCHAR(36) NOT NULL, + receiver_wallet_type VARCHAR(36) NOT NULL, + agent VARCHAR(20), + ip VARCHAR(11), + create_date TIMESTAMP NOT NULL, + status VARCHAR(20), + retry_count DECIMAL, + last_try_date TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS pair_config +( + pair VARCHAR(72) PRIMARY KEY, + left_side_wallet_symbol VARCHAR(36) NOT NULL, + right_side_wallet_symbol VARCHAR(36) NOT NULL, + left_side_fraction DECIMAL NOT NULL, + right_side_fraction DECIMAL NOT NULL, + UNIQUE ( + left_side_wallet_symbol, + right_side_wallet_symbol + ) +); + +CREATE TABLE IF NOT EXISTS pair_fee_config +( + id SERIAL PRIMARY KEY, + pair_config_id VARCHAR(72) NOT NULL REFERENCES pair_config (pair), + direction VARCHAR(36) NOT NULL, + user_level VARCHAR(36) NOT NULL, + maker_fee DECIMAL NOT NULL, + taker_fee DECIMAL NOT NULL, + UNIQUE (direction, user_level, pair_config_id) +); + +CREATE TABLE IF NOT EXISTS temp_events +( + id SERIAL PRIMARY KEY, + ouid VARCHAR(72) NOT NULL, + event_type VARCHAR(72) NOT NULL, + event_body TEXT NOT NULL, + event_date TIMESTAMP NOT NULL +); + +COMMIT; From fd0e9d1a462a24c1940f91b02f3c76188bbd687b Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 15 May 2022 15:49:19 +0430 Subject: [PATCH 8/9] Removed currency rate in wallet --- .../wallet/core/service/TransferService.kt | 179 +++++++++--------- .../postgres/dao/CurrencyRateRepository.kt | 16 -- .../postgres/impl/CurrencyRateServiceImpl.kt | 32 ---- .../src/main/resources/schema.sql | 9 - 4 files changed, 89 insertions(+), 147 deletions(-) delete mode 100644 wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRateRepository.kt delete mode 100644 wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyRateServiceImpl.kt diff --git a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt index d60441401..ea5cb334f 100644 --- a/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt +++ b/wallet/wallet-core/src/main/kotlin/co/nilin/opex/wallet/core/service/TransferService.kt @@ -1,91 +1,90 @@ -package co.nilin.opex.wallet.core.service - -import co.nilin.opex.wallet.core.exc.CurrencyNotMatchedException -import co.nilin.opex.wallet.core.exc.DepositLimitExceededException -import co.nilin.opex.wallet.core.exc.NotEnoughBalanceException -import co.nilin.opex.wallet.core.exc.WithdrawLimitExceededException -import co.nilin.opex.wallet.core.inout.TransferCommand -import co.nilin.opex.wallet.core.inout.TransferResult -import co.nilin.opex.wallet.core.inout.TransferResultDetailed -import co.nilin.opex.wallet.core.model.Amount -import co.nilin.opex.wallet.core.model.Transaction -import co.nilin.opex.wallet.core.spi.* -import org.springframework.stereotype.Service -import org.springframework.transaction.annotation.Transactional -import java.time.LocalDateTime -import java.util.* - -@Service -class TransferService( - val currencyRateService: CurrencyRateService, - val walletManager: WalletManager, - val walletListener: WalletListener, - val walletOwnerManager: WalletOwnerManager, - val transactionManager: TransactionManager -) { - @Transactional - suspend fun transfer(transferCommand: TransferCommand): TransferResultDetailed { - //pre transfer hook (dispatch pre transfer event) - val srcWallet = transferCommand.sourceWallet - val srcWalletOwner = srcWallet.owner() - val srcWalletBalance = srcWallet.balance() - if (srcWallet.currency() != transferCommand.amount.currency) - throw CurrencyNotMatchedException() - if (srcWalletBalance.amount < transferCommand.amount.amount) - throw NotEnoughBalanceException() - if (!walletOwnerManager.isWithdrawAllowed(srcWalletOwner, transferCommand.amount)) - throw WithdrawLimitExceededException() - if (!walletManager.isWithdrawAllowed(srcWallet, transferCommand.amount.amount)) - throw WithdrawLimitExceededException() - - val destWallet = transferCommand.destWallet - val destWalletOwner = destWallet.owner() - //check wallet if can accept the value type - val amountToTransfer = currencyRateService.convert(transferCommand.amount, destWallet.currency()) - - if (!walletOwnerManager.isDepositAllowed(destWalletOwner, Amount(destWallet.currency(), amountToTransfer))) - throw DepositLimitExceededException() - if (!walletManager.isDepositAllowed(destWallet, amountToTransfer)) - throw DepositLimitExceededException() - - walletManager.decreaseBalance(srcWallet, transferCommand.amount.amount) - walletManager.increaseBalance(destWallet, amountToTransfer) - val tx = transactionManager.save( - Transaction( - srcWallet, - destWallet, - transferCommand.amount.amount, - amountToTransfer, - transferCommand.description, - transferCommand.transferRef, - LocalDateTime.now() - ) - ) - //get the result and add to return result type - walletListener.onDeposit( - destWallet, - srcWallet, - transferCommand.amount, - amountToTransfer, - tx, - transferCommand.additionalData - ) - walletListener.onWithdraw(srcWallet, destWallet, transferCommand.amount, tx, transferCommand.additionalData) - //post transfer hook(dispatch post transfer event) - - //notify balance change - return TransferResultDetailed( - TransferResult( - Date().time, - srcWalletOwner.uuid(), - srcWallet.type(), - srcWalletBalance, - walletManager.findWalletById(srcWallet.id()!!)!!.balance(), - transferCommand.amount, - destWalletOwner.uuid(), - destWallet.type(), - Amount(destWallet.currency(), amountToTransfer) - ), tx - ) - } +package co.nilin.opex.wallet.core.service + +import co.nilin.opex.wallet.core.exc.CurrencyNotMatchedException +import co.nilin.opex.wallet.core.exc.DepositLimitExceededException +import co.nilin.opex.wallet.core.exc.NotEnoughBalanceException +import co.nilin.opex.wallet.core.exc.WithdrawLimitExceededException +import co.nilin.opex.wallet.core.inout.TransferCommand +import co.nilin.opex.wallet.core.inout.TransferResult +import co.nilin.opex.wallet.core.inout.TransferResultDetailed +import co.nilin.opex.wallet.core.model.Amount +import co.nilin.opex.wallet.core.model.Transaction +import co.nilin.opex.wallet.core.spi.* +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import java.time.LocalDateTime +import java.util.* + +@Service +class TransferService( + val walletManager: WalletManager, + val walletListener: WalletListener, + val walletOwnerManager: WalletOwnerManager, + val transactionManager: TransactionManager +) { + @Transactional + suspend fun transfer(transferCommand: TransferCommand): TransferResultDetailed { + //pre transfer hook (dispatch pre transfer event) + val srcWallet = transferCommand.sourceWallet + val srcWalletOwner = srcWallet.owner() + val srcWalletBalance = srcWallet.balance() + if (srcWallet.currency() != transferCommand.amount.currency) + throw CurrencyNotMatchedException() + if (srcWalletBalance.amount < transferCommand.amount.amount) + throw NotEnoughBalanceException() + if (!walletOwnerManager.isWithdrawAllowed(srcWalletOwner, transferCommand.amount)) + throw WithdrawLimitExceededException() + if (!walletManager.isWithdrawAllowed(srcWallet, transferCommand.amount.amount)) + throw WithdrawLimitExceededException() + + val destWallet = transferCommand.destWallet + val destWalletOwner = destWallet.owner() + //check wallet if can accept the value type + val amountToTransfer = transferCommand.amount.amount + + if (!walletOwnerManager.isDepositAllowed(destWalletOwner, Amount(destWallet.currency(), amountToTransfer))) + throw DepositLimitExceededException() + if (!walletManager.isDepositAllowed(destWallet, amountToTransfer)) + throw DepositLimitExceededException() + + walletManager.decreaseBalance(srcWallet, transferCommand.amount.amount) + walletManager.increaseBalance(destWallet, amountToTransfer) + val tx = transactionManager.save( + Transaction( + srcWallet, + destWallet, + transferCommand.amount.amount, + amountToTransfer, + transferCommand.description, + transferCommand.transferRef, + LocalDateTime.now() + ) + ) + //get the result and add to return result type + walletListener.onDeposit( + destWallet, + srcWallet, + transferCommand.amount, + amountToTransfer, + tx, + transferCommand.additionalData + ) + walletListener.onWithdraw(srcWallet, destWallet, transferCommand.amount, tx, transferCommand.additionalData) + //post transfer hook(dispatch post transfer event) + + //notify balance change + return TransferResultDetailed( + TransferResult( + Date().time, + srcWalletOwner.uuid(), + srcWallet.type(), + srcWalletBalance, + walletManager.findWalletById(srcWallet.id()!!)!!.balance(), + transferCommand.amount, + destWalletOwner.uuid(), + destWallet.type(), + Amount(destWallet.currency(), amountToTransfer) + ), tx + ) + } } \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRateRepository.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRateRepository.kt deleted file mode 100644 index 27625d195..000000000 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/dao/CurrencyRateRepository.kt +++ /dev/null @@ -1,16 +0,0 @@ -package co.nilin.opex.wallet.ports.postgres.dao - -import co.nilin.opex.wallet.ports.postgres.model.CurrencyRateModel -import org.springframework.data.r2dbc.repository.Query -import org.springframework.data.repository.query.Param -import org.springframework.data.repository.reactive.ReactiveCrudRepository -import org.springframework.stereotype.Repository -import reactor.core.publisher.Mono - -@Repository -interface CurrencyRateRepository : ReactiveCrudRepository { - @Query("select * from currency_rate where source_currency = :sourceCurrency and dest_currency = :destCurrency") - fun findBySourceAndDest( - @Param("source") sourceCurrency: String, @Param("dest") destCurrency: String - ): Mono -} diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyRateServiceImpl.kt b/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyRateServiceImpl.kt deleted file mode 100644 index df8d8bbab..000000000 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/kotlin/co/nilin/opex/wallet/ports/postgres/impl/CurrencyRateServiceImpl.kt +++ /dev/null @@ -1,32 +0,0 @@ -package co.nilin.opex.wallet.ports.postgres.impl - -import co.nilin.opex.wallet.core.model.Amount -import co.nilin.opex.wallet.core.model.Currency -import co.nilin.opex.wallet.core.spi.CurrencyRateService -import co.nilin.opex.wallet.ports.postgres.dao.CurrencyRateRepository -import kotlinx.coroutines.reactive.awaitFirstOrDefault -import kotlinx.coroutines.reactive.awaitFirstOrNull -import org.springframework.stereotype.Service -import java.math.BigDecimal - -@Service -class CurrencyRateServiceImpl(val currencyRateRepository: CurrencyRateRepository) : CurrencyRateService { - override suspend fun convert(amount: Amount, targetCurrency: Currency): BigDecimal { - if (amount.currency.getSymbol() == targetCurrency.getSymbol()) - return amount.amount - - var rate = currencyRateRepository.findBySourceAndDest( - amount.currency.getSymbol(), targetCurrency.getSymbol() - ) - .map { BigDecimal.valueOf(it!!.rate) } - .awaitFirstOrNull() - if (rate != null) { - rate = currencyRateRepository.findBySourceAndDest( - targetCurrency.getSymbol(), amount.currency.getSymbol() - ) - .map { BigDecimal.valueOf(it!!.rate) } - .awaitFirstOrDefault(BigDecimal.ZERO) - } - return amount.amount.multiply(rate) - } -} \ No newline at end of file diff --git a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql index e37ea43c1..78e30bd31 100644 --- a/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql +++ b/wallet/wallet-ports/wallet-persister-postgres/src/main/resources/schema.sql @@ -5,15 +5,6 @@ CREATE TABLE IF NOT EXISTS currency precision DECIMAL NOT NULL ); -CREATE TABLE IF NOT EXISTS currency_rate -( - id SERIAL PRIMARY KEY, - source_currency VARCHAR(25) NOT NULL REFERENCES currency (symbol), - dest_currency VARCHAR(25) NOT NULL REFERENCES currency (symbol), - rate DECIMAL NOT NULL, - UNIQUE (source_currency, dest_currency) -); - CREATE TABLE IF NOT EXISTS wallet_owner ( id SERIAL PRIMARY KEY, From ef94eab6837ef6f84a1e9cc7340ed7f8e41ab03f Mon Sep 17 00:00:00 2001 From: Peyman Date: Sun, 15 May 2022 15:58:06 +0430 Subject: [PATCH 9/9] Service unavailable location --- .../main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt index bf0e6359d..06400af37 100644 --- a/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt +++ b/utility/error-handler/src/main/kotlin/co/nilin/opex/utility/error/data/OpexError.kt @@ -11,6 +11,7 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus UnAuthorized(1003, "Unauthorized", HttpStatus.UNAUTHORIZED), Forbidden(1004, "Forbidden", HttpStatus.FORBIDDEN), NotFound(1005, "Not found", HttpStatus.NOT_FOUND), + ServiceUnavailable(1006, null, HttpStatus.SERVICE_UNAVAILABLE), InvalidRequestParam(1020, "Parameter '%s' is either missing or invalid", HttpStatus.BAD_REQUEST), InvalidRequestBody(1021, "Request body is invalid", HttpStatus.BAD_REQUEST), @@ -22,7 +23,6 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus // code 4000: matching-gateway SubmitOrderForbiddenByAccountant(4001, null, HttpStatus.BAD_REQUEST), - ServiceUnavailable(4002, null, HttpStatus.SERVICE_UNAVAILABLE), // code 5000: user-management EmailAlreadyVerified(5001, "Email is already verified", HttpStatus.BAD_REQUEST),