├── README ├── project └── build.properties ├── src ├── test │ ├── resources │ │ ├── akka.conf │ │ ├── log4j.properties │ │ └── logback.xml │ └── scala │ │ └── TradeLifecycleSpec.scala └── main │ └── scala │ ├── Event.scala │ ├── QueryStore.scala │ ├── RefModel.scala │ ├── TradeLifecycle.scala │ ├── TradeSnapshot.scala │ ├── InMemoryEventLog.scala │ ├── RedisEventLog.scala │ ├── TradeModel.scala │ └── SerializationProtocol.scala ├── .gitignore └── presentation.txt /README: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.11.2 2 | -------------------------------------------------------------------------------- /src/test/resources/akka.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | event-handlers = ["akka.event.slf4j.Slf4jEventHandler"] 3 | event-handler-level = "DEBUG" 4 | actor { 5 | timeout = 20 6 | my-pinned-dispatcher { 7 | executor = "thread-pool-executor" 8 | type = PinnedDispatcher 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/scala/Event.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package event 3 | 4 | import akka.dispatch.Future 5 | 6 | trait Event 7 | trait State 8 | 9 | case class EventLogEntry(entryId: Long, objectId: String, inState: State, withData: Option[Any], event: Event) 10 | 11 | trait EventLog extends Iterable[EventLogEntry] { 12 | def iterator: Iterator[EventLogEntry] 13 | def iterator(fromEntryId: Long): Iterator[EventLogEntry] 14 | def appendAsync(id: String, state: State, data: Option[Any], event: Event): Future[EventLogEntry] 15 | } 16 | -------------------------------------------------------------------------------- /src/test/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # for production, you should probably set the root to INFO 2 | # and the pattern to %c instead of %l. (%l is slower.) 3 | 4 | # output messages into a rolling log file as well as stdout 5 | log4j.rootLogger=DEBUG,stdout 6 | 7 | # stdout 8 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 9 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 10 | log4j.appender.stdout.layout.ConversionPattern=%5p [%t] %d{HH:mm:ss,SSS} %m%n 11 | 12 | # Application logging options 13 | log4j.logger.com.redis=DEBUG,stdout 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *# 3 | src_managed 4 | activemq-data 5 | project/plugins/project 6 | project/boot/* 7 | */project/build/target 8 | */project/boot 9 | lib_managed 10 | etags 11 | TAGS 12 | reports 13 | dist 14 | target 15 | deploy/*.jar 16 | data 17 | out 18 | logs 19 | .codefellow 20 | storage 21 | .codefellow 22 | _dump 23 | .manager 24 | manifest.mf 25 | semantic.cache 26 | tm*.log 27 | tm*.lck 28 | tm.out 29 | *.tm.epoch 30 | .DS_Store 31 | *.iws 32 | *.ipr 33 | *.iml 34 | .project 35 | .settings 36 | .classpath 37 | .idea 38 | .scala_dependencies 39 | *.swp 40 | multiverse.log 41 | -------------------------------------------------------------------------------- /src/main/scala/QueryStore.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package service 3 | 4 | import akka.actor.{Actor, FSM} 5 | import FSM._ 6 | import model.TradeModel 7 | import TradeModel._ 8 | 9 | case object QueryAllTrades 10 | 11 | // QueryStore modeled as an actor 12 | class TradeQueryStore extends Actor { 13 | private var trades = new collection.immutable.TreeSet[Trade]()(Ordering.by(_.refNo)) 14 | 15 | def receive = { 16 | case Transition(_, _, _) => 17 | case CurrentState(_, _) => 18 | 19 | case QueryAllTrades => 20 | sender ! trades.toList 21 | 22 | case trade: Trade => 23 | trades += trades.find(_ == trade).map(_ => trade).getOrElse(trade) 24 | } 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/main/scala/RefModel.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package model 3 | 4 | trait RefModel extends Serializable { 5 | type Instrument = String 6 | type Account = String 7 | type NetAmount = BigDecimal 8 | type Customer = String 9 | type Broker = String 10 | 11 | sealed trait Market 12 | case object HongKong extends Market 13 | case object Singapore extends Market 14 | case object NewYork extends Market 15 | case object Tokyo extends Market 16 | case object Other extends Market 17 | 18 | def makeMarket(m: String) = m match { 19 | case "HongKong" => HongKong 20 | case "Singapore" => Singapore 21 | case "NewYork" => NewYork 22 | case "Tokyo" => Tokyo 23 | case _ => Other 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/scala/TradeLifecycle.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package service 3 | 4 | import event.{EventLogEntry, EventLog} 5 | 6 | import akka.actor.{Actor, FSM} 7 | import akka.util.Duration 8 | 9 | import model.TradeModel._ 10 | 11 | class TradeLifecycle(trade: Trade, timeout: Duration, log: Option[EventLog]) 12 | extends Actor with FSM[TradeState, Trade] { 13 | import FSM._ 14 | 15 | startWith(Created, trade) 16 | 17 | when(Created) { 18 | case Event(e@AddValueDate, data) => 19 | log.map(_.appendAsync(data.refNo, Created, Some(data), e)) 20 | val trd = addValueDate(data) 21 | gossip(trd) 22 | goto(ValueDateAdded) using trd forMax(timeout) 23 | } 24 | 25 | when(ValueDateAdded) { 26 | case Event(StateTimeout, _) => 27 | stay 28 | 29 | case Event(e@EnrichTrade, data) => 30 | log.map(_.appendAsync(data.refNo, ValueDateAdded, None, e)) 31 | val trd = enrichTrade(data) 32 | gossip(trd) 33 | goto(Enriched) using trd forMax(timeout) 34 | } 35 | 36 | when(Enriched) { 37 | case Event(StateTimeout, _) => 38 | stay 39 | 40 | case Event(e@SendOutContractNote, data) => 41 | log.map(_.appendAsync(data.refNo, Enriched, None, e)) 42 | sender ! data 43 | stop 44 | } 45 | 46 | initialize 47 | } 48 | -------------------------------------------------------------------------------- /src/main/scala/TradeSnapshot.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package service 3 | 4 | import event.{EventLog, EventLogEntry} 5 | import akka.actor.{ActorRef, ActorSystem, Props} 6 | import akka.dispatch._ 7 | import akka.pattern.ask 8 | import akka.util.{Timeout, Duration} 9 | import akka.util.duration._ 10 | import model.TradeModel._ 11 | 12 | trait TradeSnapshot { 13 | def doSnapshot(log: EventLog, system: ActorSystem): List[Trade] = { 14 | // implicit val timeout = system.settings.ActorTimeout 15 | implicit val timeout = Timeout(20 seconds) 16 | val l = new collection.mutable.ListBuffer[Trade] 17 | var mar = Map.empty[String, ActorRef] 18 | log.foreach {entry => 19 | val EventLogEntry(id, oid, state, d, ev) = entry 20 | if (state.toString == "Created") { 21 | mar += ((oid, system.actorOf(Props(new TradeLifecycle(d.asInstanceOf[Option[Trade]].get, timeout.duration, None)), name = "tlc-" + oid))) 22 | mar(oid) ! ev 23 | } else if (state.toString == "Enriched") { 24 | val future = mar(oid) ? SendOutContractNote 25 | l += Await.result(future, timeout.duration).asInstanceOf[Trade] 26 | } else { 27 | mar(oid) ! ev 28 | } 29 | } 30 | l.toList 31 | } 32 | } 33 | 34 | object TradeSnapshot extends TradeSnapshot { 35 | def snapshot(log: EventLog, system: ActorSystem) = doSnapshot(log, system) 36 | } 37 | -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | [%t] [%4p] [%d{ISO8601}] %c{1}: %m%n 14 | 15 | 16 | 17 | ./logs/akka.log 18 | 19 | [%t] [%4p] [%d{ISO8601}] %c{1}: %m%n 20 | 21 | 22 | ./logs/akka.log.slf4j.%d{yyyy-MM-dd-HH} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /presentation.txt: -------------------------------------------------------------------------------- 1 | 1. Immutable domain model using 2 | - algebraic data types for representation 3 | - pure functions to model domain behaviors 4 | - immutable structures like lenses for functional updates 5 | - no in place mutation 6 | 7 | 2. model becomes declarative & expressive 8 | 9 | 3. How domain objects change state ? 10 | - through events (aka commands) 11 | - an event transitions a domain object from one state to another 12 | o no mutation 13 | o returns a new instance with new state (can be made efficient through implementation of persistent data structures. We use type lenses.) 14 | 15 | 4. But how does the domain object reflect the current state ? 16 | - through repeated state transitions 17 | - all events are saved (event sourcing) 18 | - ability to rollback to any point in time 19 | - ability to clear all states and replay 20 | - events are immutable 21 | 22 | 5. The essence of an application is to change states of domain objects and observe these changes and do some actions (which may be side-effects). 23 | - state changes are done in the service layer through domain service APIs 24 | - we model the service layer as a state machine (FSM) 25 | - and the state machine as an actor (akka) 26 | 27 | 6. State changes => 28 | aka commands => 29 | modeled as events => 30 | controlled through an FSM => 31 | isolation guaranteed through execution within an actor model 32 | 33 | 7. What about queries ? 34 | - CQRS (command query responsibility segregation) 35 | - queries are modeled as subscribers of events 36 | - commands publish events, queries subscribe to them 37 | - a query store is also modeled as an actor 38 | o scalability 39 | o distribution (if remote actors are supported) 40 | 41 | 8. Other subscribers ? 42 | - logging 43 | -------------------------------------------------------------------------------- /src/main/scala/InMemoryEventLog.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package event 3 | 4 | import akka.dispatch._ 5 | import akka.util.Timeout 6 | import akka.util.duration._ 7 | import akka.actor.{Actor, ActorRef, Props, ActorSystem} 8 | import akka.pattern.ask 9 | import Actor._ 10 | 11 | class InMemoryEventLog(as: ActorSystem) extends EventLog { 12 | val loggerActorName = "memory-event-logger" 13 | 14 | // need a pinned dispatcher to maintain order of log entries 15 | // val dispatcher = as.dispatcherFactory.newPinnedDispatcher(loggerActorName) 16 | 17 | lazy val logger = as.actorOf(Props(new Logger).withDispatcher("my-pinned-dispatcher"), name = loggerActorName) 18 | // implicit val timeout = as.settings.ActorTimeout 19 | implicit val timeout = Timeout(20 seconds) 20 | 21 | def iterator = iterator(0L) 22 | 23 | def iterator(fromEntryId: Long) = 24 | getEntries.drop(fromEntryId.toInt).iterator 25 | 26 | def appendAsync(id: String, state: State, data: Option[Any], event: Event): Future[EventLogEntry] = 27 | (logger ? LogEvent(id, state, data, event)).asInstanceOf[Future[EventLogEntry]] 28 | 29 | def getEntries: List[EventLogEntry] = { 30 | val future = logger ? GetEntries() 31 | Await.result(future, timeout.duration).asInstanceOf[List[EventLogEntry]] 32 | } 33 | 34 | case class LogEvent(objectId: String, state: State, data: Option[Any], event: Event) 35 | case class GetEntries() 36 | 37 | class Logger extends Actor { 38 | private var entries = List.empty[EventLogEntry] 39 | def receive = { 40 | case LogEvent(id, state, data, event) => 41 | val entry = EventLogEntry(InMemoryEventLog.nextId(), id, state, data, event) 42 | entries = entry :: entries 43 | sender ! entry 44 | 45 | case GetEntries() => 46 | sender ! entries.reverse 47 | } 48 | } 49 | } 50 | 51 | object InMemoryEventLog { 52 | var current = 0L 53 | def nextId() = { 54 | current = current + 1 55 | current 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/scala/RedisEventLog.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package event 3 | 4 | import serialization.Serialization._ 5 | import serialization.Util._ 6 | import akka.dispatch._ 7 | import akka.util.Timeout 8 | import akka.util.duration._ 9 | import akka.actor.{Actor, ActorRef, Props, ActorSystem} 10 | import akka.pattern.ask 11 | import Actor._ 12 | import com.redis._ 13 | import com.redis.serialization._ 14 | 15 | class RedisEventLog(clients: RedisClientPool, as: ActorSystem) extends EventLog { 16 | val loggerActorName = "redis-event-logger" 17 | 18 | // need a pinned dispatcher to maintain order of log entries 19 | lazy val logger = as.actorOf(Props(new Logger(clients)).withDispatcher("my-pinned-dispatcher"), name = loggerActorName) 20 | // implicit val timeout = as.settings.ActorTimeout 21 | implicit val timeout = Timeout(20 seconds) 22 | 23 | def iterator = iterator(0L) 24 | 25 | def iterator(fromEntryId: Long) = 26 | getEntries.drop(fromEntryId.toInt).iterator 27 | 28 | def appendAsync(id: String, state: State, data: Option[Any], event: Event): Future[EventLogEntry] = 29 | (logger ? LogEvent(id, state, data, event)).asInstanceOf[Future[EventLogEntry]] 30 | 31 | def getEntries: List[EventLogEntry] = { 32 | val future = logger ? GetEntries() 33 | Await.result(future, timeout.duration).asInstanceOf[List[EventLogEntry]] 34 | } 35 | 36 | case class LogEvent(objectId: String, state: State, data: Option[Any], event: Event) 37 | case class GetEntries() 38 | 39 | class Logger(clients: RedisClientPool) extends Actor { 40 | implicit val format = Format {case l: EventLogEntry => serializeEventLogEntry(l)} 41 | implicit val parseList = Parse[EventLogEntry](deSerializeEventLogEntry(_)) 42 | 43 | def receive = { 44 | case LogEvent(id, state, data, event) => 45 | val entry = EventLogEntry(RedisEventLog.nextId(), id, state, data, event) 46 | clients.withClient {client => 47 | client.lpush(RedisEventLog.logName, entry) 48 | } 49 | sender ! entry 50 | 51 | case GetEntries() => 52 | import Parse.Implicits.parseByteArray 53 | val entries = 54 | clients.withClient {client => 55 | client.lrange[EventLogEntry](RedisEventLog.logName, 0, -1) 56 | } 57 | val ren = entries.map(_.map(e => e.get)).getOrElse(List.empty[EventLogEntry]).reverse 58 | sender ! ren 59 | } 60 | } 61 | } 62 | 63 | import Parse.Implicits.parseDouble 64 | 65 | 66 | 67 | object RedisEventLog { 68 | var current = 0L 69 | def logName = "events" 70 | def nextId() = { 71 | current = current + 1 72 | current 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/test/scala/TradeLifecycleSpec.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package service 3 | 4 | import event.{InMemoryEventLog, RedisEventLog} 5 | import org.scalatest.{Spec, BeforeAndAfterAll} 6 | import org.scalatest.matchers.ShouldMatchers 7 | import org.scalatest.junit.JUnitRunner 8 | import org.junit.runner.RunWith 9 | import akka.pattern.ask 10 | import akka.util.Timeout 11 | import akka.util.duration._ 12 | 13 | import com.redis._ 14 | 15 | @RunWith(classOf[JUnitRunner]) 16 | class TradeLifecycleSpec extends Spec with ShouldMatchers with BeforeAndAfterAll { 17 | import java.util.Calendar 18 | import akka.actor.{Actor, ActorRef, Props, ActorSystem, FSM} 19 | import akka.dispatch.Await 20 | import akka.util.Timeout 21 | import akka.util.duration._ 22 | import akka.routing.Listen 23 | import Actor._ 24 | import FSM._ 25 | import model.TradeModel._ 26 | 27 | val system = ActorSystem("TradingSystem") 28 | // implicit val timeout = system.settings.ActorTimeout 29 | implicit val timeout = Timeout(20 seconds) 30 | override def afterAll = { system.shutdown() } 31 | 32 | describe("trade lifecycle") { 33 | it("should work with in memory event logging") { 34 | val log = new InMemoryEventLog(system) 35 | val finalTrades = new collection.mutable.ListBuffer[Trade] 36 | 37 | // make trades 38 | val trds = 39 | List( 40 | Trade("a-123", "google", "r-123", HongKong, 12.25, 200), 41 | Trade("a-124", "ibm", "r-124", Tokyo, 22.25, 250), 42 | Trade("a-125", "cisco", "r-125", NewYork, 20.25, 150), 43 | Trade("a-126", "ibm", "r-127", Singapore, 22.25, 250)) 44 | 45 | // set up listeners 46 | val qry = system.actorOf(Props(new TradeQueryStore)) 47 | 48 | // do service 49 | trds.foreach {trd => 50 | val tlc = system.actorOf(Props(new TradeLifecycle(trd, timeout.duration, Some(log)))) 51 | tlc ! SubscribeTransitionCallBack(qry) 52 | tlc ! AddValueDate 53 | tlc ! EnrichTrade 54 | val future = tlc ? SendOutContractNote 55 | finalTrades += Await.result(future, timeout.duration).asInstanceOf[Trade] 56 | } 57 | Thread.sleep(1000) 58 | 59 | // get snapshot 60 | import TradeSnapshot._ 61 | val trades = snapshot(log, system) 62 | finalTrades should equal(trades) 63 | 64 | // check query store 65 | val f = qry ? QueryAllTrades 66 | val qtrades = Await.result(f, timeout.duration).asInstanceOf[List[Trade]] 67 | qtrades should equal(finalTrades) 68 | } 69 | 70 | it("should work with redis based event logging") { 71 | val clients = new RedisClientPool("localhost", 6379) 72 | val log = new RedisEventLog(clients, system) 73 | val finalTrades = new collection.mutable.ListBuffer[Trade] 74 | 75 | // make trades 76 | val trds = 77 | List( 78 | Trade("a-123", "google", "r-123", HongKong, 12.25, 200), 79 | Trade("a-124", "ibm", "r-124", Tokyo, 22.25, 250), 80 | Trade("a-125", "cisco", "r-125", NewYork, 20.25, 150), 81 | Trade("a-126", "ibm", "r-127", Singapore, 22.25, 250)) 82 | 83 | // set up listeners 84 | val qry = system.actorOf(Props(new TradeQueryStore)) 85 | 86 | // do service 87 | trds.foreach {trd => 88 | val tlc = system.actorOf(Props(new TradeLifecycle(trd, timeout.duration, Some(log)))) 89 | tlc ! SubscribeTransitionCallBack(qry) 90 | tlc ! AddValueDate 91 | tlc ! EnrichTrade 92 | val future = tlc ? SendOutContractNote 93 | finalTrades += Await.result(future, timeout.duration).asInstanceOf[Trade] 94 | } 95 | Thread.sleep(1000) 96 | 97 | // get snapshot 98 | import TradeSnapshot._ 99 | val trades = snapshot(log, system) 100 | finalTrades should equal(trades) 101 | 102 | // check query store 103 | val f = qry ? QueryAllTrades 104 | val qtrades = Await.result(f, timeout.duration).asInstanceOf[List[Trade]] 105 | qtrades should equal(finalTrades) 106 | clients.withClient{ client => client.flushdb } 107 | clients.withClient {client => client.disconnect} 108 | clients.close 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/scala/TradeModel.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package model 3 | 4 | import event.{Event, State} 5 | import scalaz._ 6 | import Scalaz._ 7 | 8 | import java.util.{Date, Calendar} 9 | 10 | trait TradeModel extends Serializable {this: RefModel => 11 | 12 | // the main domain class 13 | case class Trade(account: Account, instrument: Instrument, refNo: String, market: Market, 14 | unitPrice: BigDecimal, quantity: BigDecimal, tradeDate: Date = Calendar.getInstance.getTime, 15 | valueDate: Option[Date] = None, taxFees: Option[List[(TaxFeeId, BigDecimal)]] = None, 16 | netAmount: Option[BigDecimal] = None) { 17 | override def equals(that: Any) = refNo == that.asInstanceOf[Trade].refNo 18 | override def hashCode = refNo.hashCode 19 | } 20 | 21 | // various tax/fees to be paid when u do a trade 22 | sealed trait TaxFeeId extends Serializable 23 | case object TradeTax extends TaxFeeId 24 | case object Commission extends TaxFeeId 25 | case object VAT extends TaxFeeId 26 | 27 | // rates of tax/fees expressed as fractions of the principal of the trade 28 | val rates: Map[TaxFeeId, BigDecimal] = Map(TradeTax -> 0.2, Commission -> 0.15, VAT -> 0.1) 29 | 30 | // tax and fees applicable for each market 31 | // Other signifies the general rule 32 | val taxFeeForMarket: Map[Market, List[TaxFeeId]] = Map(Other -> List(TradeTax, Commission), Singapore -> List(TradeTax, Commission, VAT)) 33 | 34 | // get the list of tax/fees applicable for this trade 35 | // depends on the market 36 | val forTrade: Trade => Option[List[TaxFeeId]] = {trade => 37 | taxFeeForMarket.get(trade.market) <+> taxFeeForMarket.get(Other) 38 | } 39 | 40 | def principal(trade: Trade) = trade.unitPrice * trade.quantity 41 | 42 | // combinator to value a tax/fee for a specific trade 43 | private[model] val valueAs: Trade => TaxFeeId => BigDecimal = {trade => {tid => 44 | ((rates get tid) map (_ * principal(trade))) getOrElse (BigDecimal(0)) }} 45 | 46 | // all tax/fees for a specific trade 47 | val taxFeeCalculate: Trade => List[TaxFeeId] => List[(TaxFeeId, BigDecimal)] = {t => {tids => 48 | tids zip (tids ∘ valueAs(t)) 49 | }} 50 | 51 | // validate quantity 52 | def validQuantity(qty: BigDecimal): Validation[String, BigDecimal] = 53 | if (qty <= 0) "qty must be > 0".fail 54 | else if (qty > 500) "qty must be <= 500".fail 55 | else qty.success 56 | 57 | // validate unit price 58 | def validUnitPrice(price: BigDecimal): Validation[String, BigDecimal] = 59 | if (price <= 0) "price must be > 0".fail 60 | else if (price > 100) "price must be <= 100".fail 61 | else price.success 62 | 63 | // using Validation as an applicative 64 | // can be combined to accumulate exceptions 65 | def makeTrade(account: Account, instrument: Instrument, refNo: String, market: Market, 66 | unitPrice: BigDecimal, quantity: BigDecimal) = 67 | (validUnitPrice(unitPrice).liftFailNel |@| 68 | validQuantity(quantity).liftFailNel) { (u, q) => Trade(account, instrument, refNo, market, u, q) } 69 | 70 | /** 71 | * define a set of lenses for functional updation 72 | */ 73 | 74 | // change ref no 75 | val refNoLens: Lens[Trade, String] = Lens((t: Trade) => t.refNo, (t: Trade, r: String) => t.copy(refNo = r)) 76 | 77 | // add tax/fees 78 | val taxFeeLens: Lens[Trade, Option[List[(TaxFeeId, BigDecimal)]]] = 79 | Lens((t: Trade) => t.taxFees, (t: Trade, tfs: Option[List[(TaxFeeId, BigDecimal)]]) => t.copy(taxFees = tfs)) 80 | 81 | // add net amount 82 | val netAmountLens: Lens[Trade, Option[BigDecimal]] = 83 | Lens((t: Trade) => t.netAmount, (t: Trade, n: Option[BigDecimal]) => t.copy(netAmount = n)) 84 | 85 | // add value date 86 | val valueDateLens: Lens[Trade, Option[Date]] = 87 | Lens((t: Trade) => t.valueDate, (t: Trade, d: Option[Date]) => t.copy(valueDate = d)) 88 | 89 | /** 90 | * a set of closures 91 | */ 92 | 93 | // closure that enriches a trade 94 | val enrichTrade: Trade => Trade = {trade => 95 | val taxes = for { 96 | taxFeeIds <- forTrade // get the tax/fee ids for a trade 97 | taxFeeValues <- taxFeeCalculate // calculate tax fee values 98 | } 99 | yield(taxFeeIds ∘ taxFeeValues) 100 | val t = taxFeeLens.set(trade, taxes(trade)) 101 | netAmountLens.set(t, t.taxFees.map(_.foldl(principal(t))((a, b) => a + b._2))) 102 | } 103 | 104 | // closure for adding a value date 105 | val addValueDate: Trade => Trade = {trade => 106 | val c = Calendar.getInstance 107 | c.setTime(trade.tradeDate) 108 | c.add(Calendar.DAY_OF_MONTH, 3) 109 | valueDateLens.set(trade, Some(c.getTime)) 110 | } 111 | 112 | sealed trait TradingEvent extends Event 113 | 114 | case object NewTrade extends TradingEvent 115 | case object EnrichTrade extends TradingEvent 116 | case object AddValueDate extends TradingEvent 117 | case object SendOutContractNote extends TradingEvent 118 | 119 | sealed trait TradeState extends State 120 | 121 | case object Created extends TradeState 122 | case object Enriched extends TradeState 123 | case object ValueDateAdded extends TradeState 124 | } 125 | 126 | object TradeModel extends TradeModel with RefModel 127 | -------------------------------------------------------------------------------- /src/main/scala/SerializationProtocol.scala: -------------------------------------------------------------------------------- 1 | package net.debasishg.domain.trade 2 | package serialization 3 | 4 | import java.util.Date 5 | import sjson.json.Format 6 | import sjson.json.DefaultProtocol._ 7 | import dispatch.json._ 8 | import sjson.json.JsonSerialization._ 9 | 10 | import event.{Event, State, EventLogEntry} 11 | import model.TradeModel._ 12 | 13 | object Serialization { 14 | implicit val EventFormat: Format[Event] = new Format[Event] { 15 | def reads(json: JsValue): Event = json match { 16 | case JsString("NewTrade") => NewTrade 17 | case JsString("EnrichTrade") => EnrichTrade 18 | case JsString("AddValueDate") => AddValueDate 19 | case JsString("SendOutContractNote") => SendOutContractNote 20 | case _ => sys.error("Invalid Event") 21 | } 22 | def writes(a: Event): JsValue = a match { 23 | case NewTrade => JsString("NewTrade") 24 | case EnrichTrade => JsString("EnrichTrade") 25 | case AddValueDate => JsString("AddValueDate") 26 | case SendOutContractNote => JsString("SendOutContractNote") 27 | } 28 | } 29 | 30 | implicit val StateFormat: Format[State] = new Format[State] { 31 | def reads(json: JsValue): State = json match { 32 | case JsString("Created") => Created 33 | case JsString("Enriched") => Enriched 34 | case JsString("ValueDateAdded") => ValueDateAdded 35 | case _ => sys.error("Invalid State") 36 | } 37 | def writes(a: State): JsValue = a match { 38 | case Created => JsString("Created") 39 | case Enriched => JsString("Enriched") 40 | case ValueDateAdded => JsString("ValueDateAdded") 41 | } 42 | } 43 | 44 | implicit val TaxFeeIdFormat: Format[TaxFeeId] = new Format[TaxFeeId] { 45 | def reads(json: JsValue): TaxFeeId = json match { 46 | case JsString("TradeTax") => TradeTax 47 | case JsString("Commission") => Commission 48 | case JsString("VAT") => VAT 49 | case _ => sys.error("Invalid TaxFeeId") 50 | } 51 | def writes(a: TaxFeeId): JsValue = a match { 52 | case TradeTax => JsString("TradeTax") 53 | case Commission => JsString("Commission") 54 | case VAT => JsString("VAT") 55 | } 56 | } 57 | 58 | implicit val MarketFormat: Format[Market] = new Format[Market] { 59 | def reads(json: JsValue): Market = json match { 60 | case JsString("HongKong") => HongKong 61 | case JsString("Singapore") => Singapore 62 | case JsString("NewYork") => NewYork 63 | case JsString("Tokyo") => Tokyo 64 | case JsString("Other") => Other 65 | case _ => sys.error("Invalid State") 66 | } 67 | def writes(a: Market): JsValue = a match { 68 | case HongKong => JsString("HongKong") 69 | case Singapore => JsString("Singapore") 70 | case NewYork => JsString("NewYork") 71 | case Tokyo => JsString("Tokyo") 72 | case Other => JsString("Other") 73 | } 74 | } 75 | 76 | implicit object BigDecimalFormat extends Format[BigDecimal] { 77 | def writes(o: BigDecimal) = JsValue.apply(o) 78 | def reads(json: JsValue) = json match { 79 | case JsNumber(n) => n 80 | case _ => throw new RuntimeException("BigDecimal expected") 81 | } 82 | } 83 | 84 | implicit object DateFormat extends Format[Date] { 85 | def writes(o: Date) = JsValue.apply(o.getTime.toString) 86 | def reads(json: JsValue) = json match { 87 | case JsString(s) => sjson.json.Util.mkDate(s) 88 | case _ => throw new RuntimeException("Date expected") 89 | } 90 | } 91 | 92 | implicit val TradeFormat: Format[Trade] = new Format[Trade] { 93 | def writes(t: Trade): JsValue = 94 | JsObject(List( 95 | (tojson("account").asInstanceOf[JsString], tojson(t.account)), 96 | (tojson("instrument").asInstanceOf[JsString], tojson(t.instrument)), 97 | (tojson("refNo").asInstanceOf[JsString], tojson(t.refNo)), 98 | (tojson("market").asInstanceOf[JsString], tojson(t.market)), 99 | (tojson("unitPrice").asInstanceOf[JsString], tojson(t.unitPrice)), 100 | (tojson("quantity").asInstanceOf[JsString], tojson(t.quantity)), 101 | (tojson("tradeDate").asInstanceOf[JsString], tojson(t.tradeDate)), 102 | (tojson("valueDate").asInstanceOf[JsString], tojson(t.valueDate)), 103 | (tojson("taxFees").asInstanceOf[JsString], tojson(t.taxFees)), 104 | (tojson("netAmount").asInstanceOf[JsString], tojson(t.netAmount)) )) 105 | 106 | def reads(json: JsValue): Trade = json match { 107 | case JsObject(m) => 108 | Trade(fromjson[Account](m(JsString("account"))), 109 | fromjson[Instrument](m(JsString("instrument"))), 110 | fromjson[String](m(JsString("refNo"))), 111 | fromjson[Market](m(JsString("market"))), 112 | fromjson[BigDecimal](m(JsString("unitPrice"))), 113 | fromjson[BigDecimal](m(JsString("quantity"))), 114 | fromjson[Date](m(JsString("tradeDate"))), 115 | fromjson[Option[Date]](m(JsString("valueDate"))), 116 | fromjson[Option[List[(TaxFeeId, BigDecimal)]]](m(JsString("taxFees"))), 117 | fromjson[Option[BigDecimal]](m(JsString("netAmount")))) 118 | case _ => throw new RuntimeException("JsObject expected") 119 | } 120 | } 121 | 122 | implicit val EventLogEntryFormat: Format[EventLogEntry] = new Format[EventLogEntry] { 123 | def writes(e: EventLogEntry): JsValue = 124 | JsObject(List( 125 | (tojson("entryId").asInstanceOf[JsString], tojson(e.entryId)), 126 | (tojson("objectId").asInstanceOf[JsString], tojson(e.objectId)), 127 | (tojson("inState").asInstanceOf[JsString], tojson(e.inState)), 128 | e.withData match { 129 | case Some(t) => t match { 130 | case trd: Trade => (tojson("withData").asInstanceOf[JsString], tojson(trd)) 131 | case _ => sys.error("invalid trade data") 132 | } 133 | case _ => (tojson("withData").asInstanceOf[JsString], tojson("$notrade$")) 134 | }, 135 | (tojson("event").asInstanceOf[JsString], tojson(e.event)) )) 136 | 137 | def reads(json: JsValue): EventLogEntry = json match { 138 | case JsObject(m) => 139 | EventLogEntry(fromjson[Long](m(JsString("entryId"))), 140 | fromjson[String](m(JsString("objectId"))), 141 | fromjson[State](m(JsString("inState"))), 142 | m(JsString("withData")) match { 143 | case JsString("$notrade$") => None 144 | case t => Some(fromjson[Trade](t)) 145 | }, 146 | fromjson[Event](m(JsString("event")))) 147 | case _ => throw new RuntimeException("JsObject expected") 148 | } 149 | } 150 | } 151 | 152 | object Util { 153 | def serializeEventLogEntry(e: EventLogEntry)(implicit f: Format[EventLogEntry]) 154 | = tobinary(e) 155 | def deSerializeEventLogEntry(bytes: Array[Byte])(implicit f: Format[EventLogEntry]) 156 | = frombinary[EventLogEntry](bytes) 157 | } 158 | --------------------------------------------------------------------------------