├── project
├── build.properties
└── assembly.sbt
├── .idea
├── vcs.xml
├── kotlinc.xml
├── misc.xml
├── hydra.xml
├── scala_compiler.xml
├── modules.xml
└── sbt.xml
├── src
├── main
│ └── scala
│ │ └── me
│ │ └── mbcu
│ │ └── integrated
│ │ └── mmm
│ │ ├── ops
│ │ ├── ddex
│ │ │ ├── DdexActor.scala
│ │ │ └── Ddex.scala
│ │ ├── common
│ │ │ ├── AbsOpActor.scala
│ │ │ ├── BotCache.scala
│ │ │ ├── AbsExchange.scala
│ │ │ ├── AbsRestRequest.scala
│ │ │ ├── AbsWsParser.scala
│ │ │ ├── Offer.scala
│ │ │ ├── AbsRestActor.scala
│ │ │ ├── Config.scala
│ │ │ └── AbsOrder.scala
│ │ ├── hitbtc
│ │ │ ├── HitbtcResponse.scala
│ │ │ ├── Hitbtc.scala
│ │ │ ├── HitbtcRequest.scala
│ │ │ └── HitbtcParser.scala
│ │ ├── fcoin
│ │ │ ├── Fcoin.scala
│ │ │ ├── FcoinRequest.scala
│ │ │ └── FcoinActor.scala
│ │ ├── btcalpha
│ │ │ ├── Btcalpha.scala
│ │ │ ├── BtcalphaRequest.scala
│ │ │ └── BtcalphaActor.scala
│ │ ├── livecoin
│ │ │ ├── Livecoin.scala
│ │ │ ├── LivecoinRequest.scala
│ │ │ └── LivecoinActor.scala
│ │ ├── yobit
│ │ │ ├── Sign.scala
│ │ │ ├── Yobit.scala
│ │ │ └── YobitRequest.scala
│ │ ├── okex
│ │ │ ├── OkexRest.scala
│ │ │ ├── OkexParameters.scala
│ │ │ ├── OkexRestActor.scala
│ │ │ └── OkexRestRequest.scala
│ │ └── Definitions.scala
│ │ ├── utils
│ │ └── MyUtils.scala
│ │ ├── Application.scala
│ │ ├── actors
│ │ ├── OpWsActor.scala
│ │ ├── OpRestActor.scala
│ │ ├── SesActor.scala
│ │ ├── OpGIActor.scala
│ │ ├── FileActor.scala
│ │ ├── BaseActor.scala
│ │ ├── OpDdexActor.scala
│ │ ├── OrderRestActor.scala
│ │ ├── WsActor.scala
│ │ ├── OrderWsActor.scala
│ │ └── OrderGIActor.scala
│ │ └── sequences
│ │ └── Strategy.scala
└── test
│ └── scala
│ └── me
│ └── mbcu
│ └── integrated
│ └── mmm
│ ├── ops
│ ├── yobit
│ │ └── YobitRequestTest.scala
│ ├── hitbtc
│ │ └── HitbtcRequestTest.scala
│ ├── common
│ │ ├── OfferTest.scala
│ │ └── AbsOrderTest.scala
│ ├── okex
│ │ └── OkexRestActorTest.scala
│ ├── fcoin
│ │ └── FcoinActorTest.scala
│ └── livecoin
│ │ └── LivecoinActorTest.scala
│ ├── utils
│ └── MyUtilsTest.scala
│ └── actors
│ └── OrderWsActorTest.scala
├── config-template
├── .gitignore
├── EXCHANGES.md
├── README.md
├── VERSIONS.md
└── OPERATION.md
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version = 1.1.6
--------------------------------------------------------------------------------
/project/assembly.sbt:
--------------------------------------------------------------------------------
1 | addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.6")
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/ddex/DdexActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.ddex
2 |
3 | import akka.actor.Actor
4 |
5 | class DdexActor extends Actor {
6 | override def receive: Receive = ???
7 | }
8 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/AbsOpActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import akka.actor.{Actor, ActorRef}
4 |
5 | abstract case class AbsOpActor(exchangeDef: AbsExchange, bots: Seq[Bot], fileActor: ActorRef) extends Actor
6 |
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/utils/MyUtils.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.utils
2 |
3 |
4 | import scala.util.{Failure, Success, Try}
5 |
6 | object MyUtils {
7 |
8 | def parseToBigDecimal(s: String): Option[BigDecimal] = {
9 | Try(BigDecimal(s)) match {
10 | case Success(r) => Some(r)
11 | case Failure(e) => None
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.idea/hydra.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/hitbtc/HitbtcResponse.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.hitbtc
2 |
3 | import play.api.libs.json.{Json, OFormat}
4 |
5 | object RPCError {
6 | implicit val jsonFormat: OFormat[RPCError] = Json.format[RPCError]
7 | }
8 | case class RPCError (code : Int, message : Option[String], description : Option[String])
9 |
10 | class HitbtcResponse {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/.idea/scala_compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/ops/yobit/YobitRequestTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.yobit
2 |
3 | import org.scalatest.FunSuite
4 |
5 | class YobitRequestTest extends FunSuite{
6 | val secret = "sknfskfw3904239sdf233242"
7 |
8 | test ("test Yobit sha512HMAC sign has to be in lowercase") {
9 | val test = YobitRequest.ownTrades(secret, "trx_eth")
10 | assert(test.sign.contains("a"))
11 | assert(!test.sign.contains("A"))
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/utils/MyUtilsTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.utils
2 |
3 | import org.scalatest.FunSuite
4 |
5 | import scala.collection.mutable.ListBuffer
6 |
7 | class MyUtilsTest extends FunSuite {
8 |
9 |
10 | test("test sorting shutdown code") {
11 | val a = new ListBuffer[(String, Option[Int])]
12 | // a += (("a", Some(1)))
13 | // a += (("b", Some(1)))
14 | a += (("c", Some(-1)))
15 | a += (("d", None))
16 | a += (("d", None))
17 |
18 | val z = a.flatMap(_._2).reduceOption(_ min _)
19 | assert(z === Some(-1))
20 |
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/BotCache.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import play.api.libs.json._
4 |
5 | object BotCache {
6 | def apply(latestCounterId: Option[String]): BotCache = {
7 | latestCounterId match {
8 | case Some(v) => BotCache(v)
9 | case _ => BotCache(defaultLastCounteredId)
10 | }
11 | }
12 |
13 | implicit val jsonFormat: OFormat[BotCache] = Json.format[BotCache]
14 |
15 | val defaultLastCounteredId: String = "-1"
16 | val default: BotCache = BotCache(defaultLastCounteredId)
17 | }
18 |
19 |
20 | case class BotCache(lastCounteredId: String)
21 |
--------------------------------------------------------------------------------
/.idea/sbt.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
17 |
18 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/AbsExchange.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import akka.actor.{ActorRef, Props}
4 | import me.mbcu.integrated.mmm.ops.Definitions.Exchange.Exchange
5 | import me.mbcu.integrated.mmm.ops.Definitions.Op.Protocol
6 |
7 | abstract class AbsExchange {
8 | def name: Exchange
9 |
10 | def protocol : Protocol
11 |
12 | def endpoint : String
13 |
14 | def intervalMillis : Int
15 |
16 | def getActorRefProps: Props
17 |
18 |
19 | }
20 |
21 | trait AbsWsExchange {
22 | def getParser(op: ActorRef): AbsWsParser
23 |
24 | def getRequest: AbsWsRequest
25 |
26 | def orderId(offer: Offer): String
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/ddex/Ddex.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.ddex
2 |
3 | import akka.actor.Props
4 | import me.mbcu.integrated.mmm.ops.Definitions.{Exchange, Op}
5 | import me.mbcu.integrated.mmm.ops.Definitions.Exchange.Exchange
6 | import me.mbcu.integrated.mmm.ops.Definitions.Op.Protocol
7 | import me.mbcu.integrated.mmm.ops.common.AbsExchange
8 |
9 | object Ddex extends AbsExchange {
10 | override def name: Exchange = Exchange.ddex
11 |
12 | override def protocol: Protocol = Op.ddex
13 |
14 | override def endpoint: String = "https://api.ddex.io/v2/%s"
15 |
16 | override def intervalMillis: Int = 10
17 |
18 | override def getActorRefProps: Props = Props(new DdexActor())
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/fcoin/Fcoin.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.fcoin
2 |
3 | import akka.actor.Props
4 | import me.mbcu.integrated.mmm.ops.Definitions.Exchange.Exchange
5 | import me.mbcu.integrated.mmm.ops.Definitions.{Exchange, Op}
6 | import me.mbcu.integrated.mmm.ops.Definitions.Op.Protocol
7 | import me.mbcu.integrated.mmm.ops.common.{AbsExchange, Bot}
8 |
9 | object Fcoin extends AbsExchange {
10 |
11 | override val name: Exchange = Exchange.fcoin
12 |
13 | override val protocol: Protocol = Op.rest
14 |
15 | override val endpoint: String = "https://api.fcoin.com/v2/%s"
16 |
17 | override def intervalMillis: Int = 800
18 |
19 | override def getActorRefProps: Props = Props(new FcoinActor())
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/btcalpha/Btcalpha.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.btcalpha
2 |
3 | import akka.actor.Props
4 | import me.mbcu.integrated.mmm.ops.Definitions.Exchange.Exchange
5 | import me.mbcu.integrated.mmm.ops.Definitions.Op.Protocol
6 | import me.mbcu.integrated.mmm.ops.Definitions.{Exchange, Op}
7 | import me.mbcu.integrated.mmm.ops.common.AbsExchange
8 |
9 | object Btcalpha extends AbsExchange {
10 |
11 | override val name: Exchange = Exchange.btcalpha
12 |
13 | override val protocol: Protocol = Op.restgi
14 |
15 | override val endpoint: String = "https://btc-alpha.com/api/%s"
16 |
17 | override def intervalMillis: Int = 1000
18 |
19 | override def getActorRefProps: Props = Props(new BtcalphaActor())
20 |
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/livecoin/Livecoin.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.livecoin
2 |
3 | import akka.actor.Props
4 | import me.mbcu.integrated.mmm.ops.Definitions.Exchange.Exchange
5 | import me.mbcu.integrated.mmm.ops.Definitions.{Exchange, Op}
6 | import me.mbcu.integrated.mmm.ops.Definitions.Op.Protocol
7 | import me.mbcu.integrated.mmm.ops.common.AbsExchange
8 | import me.mbcu.integrated.mmm.ops.fcoin.FcoinActor
9 |
10 | object Livecoin extends AbsExchange{
11 |
12 | override val name: Exchange = Exchange.livecoin
13 |
14 | override val protocol: Protocol = Op.rest
15 |
16 | override val endpoint: String = "https://api.livecoin.net/%s"
17 |
18 | override def intervalMillis: Int = 900
19 |
20 | override def getActorRefProps: Props = Props(new LivecoinActor())
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/yobit/Sign.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.yobit
2 |
3 | import me.mbcu.integrated.mmm.ops.common.Side
4 |
5 | object Sign extends App {
6 |
7 |
8 | val test = YobitRequest.newOrder(args(0), "trx_eth", Side.buy, BigDecimal("0.00008137"), BigDecimal("10"))
9 | println(test.sign)
10 | println(test.params)
11 |
12 | val test2 = YobitRequest.ownTrades(args(0), "trx_eth")
13 | println(test2.sign)
14 | println(test2.params)
15 |
16 | val test3 =YobitRequest.infoOrder(args(0), "106374985614339")
17 | println(test3.sign)
18 | println(test3.params)
19 |
20 | val test4 = YobitRequest.cancelOrder(args(0), "106374965614339")
21 | println(test4.sign)
22 | println(test4.params)
23 |
24 | val test5 = YobitRequest.activeOrders(args(0), "trx_eth")
25 | println(test5.sign)
26 | println(test5.params)
27 | }
28 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/Application.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm
2 |
3 | import java.util.TimeZone
4 |
5 | import akka.actor.{ActorSystem, Props}
6 | import akka.stream.ActorMaterializer
7 | import me.mbcu.integrated.mmm.actors.BaseActor
8 | import me.mbcu.scala.{MyLogging, MyLoggingSingle}
9 |
10 | object Application extends App with MyLogging {
11 | implicit val system: ActorSystem = akka.actor.ActorSystem("mmm")
12 | implicit val materializer: ActorMaterializer = akka.stream.ActorMaterializer()
13 |
14 | if (args.length != 3){
15 | println("Requires two arguments : ")
16 | System.exit(-1)
17 | }
18 | MyLoggingSingle.init(args(1), TimeZone.getTimeZone("Asia/Tokyo"))
19 | val mainActor = system.actorOf(Props(new BaseActor(args(0), args(2))), name = "main")
20 | mainActor ! "start"
21 | }
22 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/yobit/Yobit.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.yobit
2 |
3 | import akka.actor.Props
4 | import me.mbcu.integrated.mmm.ops.Definitions.Exchange.Exchange
5 | import me.mbcu.integrated.mmm.ops.Definitions.Op.Protocol
6 | import me.mbcu.integrated.mmm.ops.Definitions.{Exchange, Op}
7 | import me.mbcu.integrated.mmm.ops.common.{AbsExchange, Bot}
8 |
9 | object Yobit extends AbsExchange {
10 |
11 | override val name: Exchange = Exchange.yobit
12 |
13 | override val protocol: Protocol = Op.restgi
14 |
15 | override val endpoint: String = "https://yobit.io/api/3/%s/%s"
16 |
17 | override val intervalMillis: Int = 2000
18 |
19 | val endpointTrade: String = "https://yobit.io/tapi/"
20 |
21 | override def getActorRefProps: Props = Props(new YobitActor())
22 |
23 | val nonceFactor : Long = 1530165626l // some random ts June 28
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/okex/OkexRest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.okex
2 |
3 | import akka.actor.Props
4 | import me.mbcu.integrated.mmm.ops.Definitions.{Exchange, Op}
5 | import me.mbcu.integrated.mmm.ops.common.{AbsExchange, Bot}
6 |
7 | object OkexRest extends AbsExchange {
8 |
9 | val name = Exchange.okexRest
10 | val protocol = Op.rest
11 | val endpoint = "https://www.okex.com/api/v1"
12 | override val intervalMillis: Int = 500
13 |
14 | override def getActorRefProps: Props = Props(new OkexRestActor())
15 |
16 | val OKEX_ERRORS = Map(
17 | 1002 -> "The transaction amount exceed the balance",
18 | 1003 -> "The transaction amount is less than the minimum",
19 | 20100 -> "request time out",
20 | 10005 -> "'SecretKey' does not exist. WARNING: This error can be caused by server error",
21 | 10007 -> "Signature does not match. WARNING: This error can be caused by server error",
22 | 10009 -> "Order does not exist",
23 | 10010 -> "Insufficient funds",
24 | 10011 -> "Amount too low",
25 | 10014 -> "Order price must be between 0 and 1,000,000",
26 | 10016 -> "Insufficient coins balance",
27 | 10024 -> "balance not sufficient",
28 | -1000 -> "Server returns html garbage, most likely Cloudflare / SSL issues"
29 | )
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/hitbtc/Hitbtc.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.hitbtc
2 |
3 | import akka.actor.{ActorRef, Props}
4 | import me.mbcu.integrated.mmm.actors.WsActor.SendJs
5 | import me.mbcu.integrated.mmm.ops.Definitions.Exchange.Exchange
6 | import me.mbcu.integrated.mmm.ops.Definitions.Op.Protocol
7 | import me.mbcu.integrated.mmm.ops.Definitions.{Exchange, Op}
8 | import me.mbcu.integrated.mmm.ops.common.AbsWsParser.SendWs
9 | import me.mbcu.integrated.mmm.ops.common._
10 | import me.mbcu.integrated.mmm.ops.yobit.YobitActor
11 | import play.api.libs.json.{JsValue, Json}
12 |
13 | object Hitbtc extends AbsExchange with AbsWsExchange {
14 |
15 | override val name: Exchange = Exchange.hitbtc
16 |
17 | override val protocol: Protocol = Op.ws
18 |
19 | override val endpoint: String = "wss://api.hitbtc.com/api/2/ws"
20 |
21 | override val intervalMillis: Int = 10
22 |
23 | override def getActorRefProps: Props = Props.empty
24 |
25 | // override def login(credentials: Credentials): SendWs = HitbtcRequest.login(credentials)
26 |
27 | override def getParser(op: ActorRef): AbsWsParser = new HitbtcParser(op)
28 |
29 | override val getRequest: AbsWsRequest = HitbtcRequest
30 |
31 | override def orderId(offer: Offer): String = HitbtcRequest.orderId(offer)
32 |
33 | // override def subscribe: SendWs = HitbtcRequest.subscribe
34 |
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/ops/hitbtc/HitbtcRequestTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.hitbtc
2 |
3 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As
4 | import me.mbcu.integrated.mmm.ops.common.{Offer, Side}
5 | import org.scalatest.FunSuite
6 | import play.api.libs.json.Json
7 |
8 | class HitbtcRequestTest extends FunSuite{
9 |
10 | test("cancel order") {
11 | val js = HitbtcRequest.cancelOrder("aaa", As.Trim).jsValue
12 | val res = Json.parse(js.toString())
13 | assert((res \ "params" \ "clientOrderId").as[String] === "aaa")
14 | assert((res \ "id").as[String] === "cancelOrder.aaa")
15 | }
16 |
17 | test("clientOrderId length") {
18 |
19 | val pair1 = "PREMINEPREMINE"
20 |
21 | val o1 = Offer.newOffer(pair1, Side.sell, BigDecimal(100), BigDecimal(100))
22 | val res1= HitbtcRequest.orderId(o1)
23 | assert(res1.length === 32)
24 |
25 | val pair2 = "NOAHETH"
26 | val o2 = Offer.newOffer(pair2, Side.sell, BigDecimal(100), BigDecimal(100))
27 |
28 | val res2= HitbtcRequest.orderId(o2)
29 | assert(res2.length === 32)
30 | }
31 |
32 | test ("clientOrderId from id") {
33 | val pair = "NOAHETH"
34 | val method = "newOrder"
35 | val o = Offer.newOffer(pair, Side.sell, BigDecimal(100), BigDecimal(100))
36 |
37 | val clientOrderId = HitbtcRequest.orderId(o)
38 | val id = HitbtcRequest.requestId(clientOrderId, method)
39 | val res = HitbtcRequest.clientOrderIdFrom(id)
40 | assert(res === clientOrderId)
41 | }
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/actors/OrderWsActorTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import me.mbcu.integrated.mmm.ops.common.{Offer, Side, Status}
4 | import org.scalatest.FunSuite
5 |
6 | class OrderWsActorTest extends FunSuite{
7 |
8 |
9 | test(" match two maps for missing id"){
10 | val o = Seq(
11 | Offer("aaa", "XRPUSD", Side.sell, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None),
12 | Offer("bbb", "XRPUSD", Side.sell, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None),
13 | Offer("ccc", "XRPUSD", Side.sell, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None),
14 | Offer("ddd", "XRPUSD", Side.sell, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None),
15 | Offer("eee", "XRPUSD", Side.buy, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None),
16 | Offer("fff", "XRPUSD", Side.buy, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None),
17 | Offer("ggg", "XRPUSD", Side.buy, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None),
18 | Offer("hhh", "XRPUSD", Side.buy, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None)
19 | )
20 |
21 | val p = Seq(
22 | Offer("ccc", "XRPUSD", Side.sell, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None),
23 | Offer("ddd", "XRPUSD", Side.sell, Status.active, 10L, None, BigDecimal(10), BigDecimal(1), None)
24 | )
25 |
26 | val x = o.map(a => a.id -> a).toMap
27 | val y = p.map(a => a.id -> a).toMap
28 | val res = x.collect{case a @ (_: String, _: Offer) if !y.contains(a._1) => a._2}.map(_.id).toSet
29 | assert(!res.contains("ccc"))
30 | assert(!res.contains("ddd"))
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/AbsRestRequest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import java.math.BigInteger
4 | import java.net.URLEncoder
5 | import java.nio.charset.StandardCharsets
6 | import java.security.MessageDigest
7 |
8 | import javax.crypto.Mac
9 | import javax.crypto.spec.SecretKeySpec
10 |
11 | abstract class AbsRestRequest {
12 |
13 | private val md5: MessageDigest = MessageDigest.getInstance("MD5")
14 |
15 | def md5(secretKey: String, data: String): String = {
16 | import java.nio.charset.StandardCharsets
17 | md5.update(StandardCharsets.UTF_8.encode(data))
18 | String.format("%032x", new BigInteger(1, md5.digest)).toUpperCase
19 | }
20 |
21 | def signHmacSHA512(secret: String, data: String): Array[Byte] = signHmac(secret, data, "HmacSHA512")
22 |
23 | def signHmac(secret: String, data: String, fun: String): Array[Byte] = {
24 | val signedSecret = new SecretKeySpec(secret.getBytes("UTF-8"), fun)
25 | val mac = Mac.getInstance(fun)
26 | mac.init(signedSecret)
27 | mac.doFinal(data.getBytes("UTF-8"))
28 | }
29 |
30 | def toHex(bytes: Array[Byte], isCapital: Boolean = true): String = {
31 | val x = if (isCapital) "X" else "x"
32 | val bi = new BigInteger(1, bytes)
33 | String.format("%0" + (bytes.length << 1) + x, bi)
34 | }
35 |
36 | def signHmacSHA1(secret: String, data: String): Array[Byte] = signHmac(secret, data, "HmacSHA1")
37 |
38 | def signHmacSHA256(secret: String, data: String): Array[Byte] = signHmac(secret, data, "HmacSHA256")
39 |
40 | def sortToForm(params: Map[String, String]) :String = params.toSeq.sortBy(_._1).map(c => s"${c._1}=${urlEncode(c._2)}").mkString("&")
41 |
42 | def urlEncode(in: String): String = URLEncoder.encode(in, StandardCharsets.UTF_8.name())
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/config-template:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "emails" : ["youremail@errorreport.com"],
4 | "sesKey" : "seskey",
5 | "sesSecret" : "sessecret",
6 | "logSeconds" : 10
7 | },
8 | "bots" : [
9 | {
10 | "exchange" : "hitbtc",
11 | "credentials" : {
12 | "pKey" : "apikeyWithTradeAllowed",
13 | "nonce" : "abcdef",
14 | "signature": "secret"
15 | },
16 | "pair": "NOAHBTC",
17 | "seed" : "lastTicker",
18 | "gridSpace": "0.5",
19 | "buyGridLevels":3,
20 | "sellGridLevels": 0,
21 | "buyOrderQuantity": "3000",
22 | "sellOrderQuantity": "3000",
23 | "quantityPower" : 2,
24 | "counterScale" : 0,
25 | "baseScale" : 9,
26 | "isStrictLevels" : true,
27 | "isNoQtyCutoff" : true,
28 | "strategy" : "ppt"
29 | },
30 | {
31 | "exchange" : "yobit",
32 | "credentials" : {
33 | "pKey" : "apikeyWithTradeAllowed",
34 | "nonce" : "",
35 | "signature": "secret"
36 | },
37 | "pair": "noah_eth",
38 | "seed" : "lastTicker",
39 | "gridSpace": "0.5",
40 | "buyGridLevels":3,
41 | "sellGridLevels": 0,
42 | "buyOrderQuantity": "3000",
43 | "sellOrderQuantity": "3000",
44 | "quantityPower" : 2,
45 | "counterScale" :0,
46 | "baseScale" : 8,
47 | "isStrictLevels" : true,
48 | "isNoQtyCutoff" : true,
49 | "strategy" : "ppt"
50 | }
51 | ]
52 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Created by https://www.gitignore.io/api/intellij
2 |
3 | ### Intellij ###
4 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
5 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
6 |
7 | # User-specific stuff:
8 | .idea/**/workspace.xml
9 | .idea/**/tasks.xml
10 | .idea/dictionaries
11 |
12 | # Sensitive or high-churn files:
13 | .idea/**/dataSources/
14 | .idea/**/dataSources.ids
15 | .idea/**/dataSources.xml
16 | .idea/**/dataSources.local.xml
17 | .idea/**/sqlDataSources.xml
18 | .idea/**/dynamic.xml
19 | .idea/**/uiDesigner.xml
20 |
21 | # Gradle:
22 | .idea/**/gradle.xml
23 | .idea/**/libraries
24 |
25 | # CMake
26 | cmake-build-debug/
27 |
28 | # Mongo Explorer plugin:
29 | .idea/**/mongoSettings.xml
30 |
31 | ## File-based project format:
32 | *.iws
33 |
34 | ## Plugin-specific files:
35 |
36 | # IntelliJ
37 | /out/
38 |
39 | # mpeltonen/sbt-idea plugin
40 | .idea_modules/
41 |
42 | # JIRA plugin
43 | atlassian-ide-plugin.xml
44 |
45 | # Cursive Clojure plugin
46 | .idea/replstate.xml
47 |
48 | # Ruby plugin and RubyMine
49 | /.rakeTasks
50 |
51 | # Crashlytics plugin (for Android Studio and IntelliJ)
52 | com_crashlytics_export_strings.xml
53 | crashlytics.properties
54 | crashlytics-build.properties
55 | fabric.properties
56 |
57 | ### Intellij Patch ###
58 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
59 |
60 | # *.iml
61 | # modules.xml
62 | # .idea/misc.xml
63 | # *.ipr
64 |
65 | # Sonarlint plugin
66 | .idea/sonarlint
67 |
68 |
69 | # End of https://www.gitignore.io/api/intellij
70 |
71 | dist/*
72 | target/
73 | lib_managed/
74 | src_managed/
75 | project/boot/
76 | project/plugins/project/
77 | .lib/
78 |
79 | ### Scala ###
80 | *.class
81 | *.log
82 |
83 | # mac
84 | .DS_Store
85 |
86 | # other
87 | .history
88 | .scala_dependencies
89 | .cache
90 | .cache-main
91 |
92 | credentials
93 | stuff
94 | last
95 | config
96 | config-extra
97 | log/
98 | millis/
99 | cache/
100 | .jar
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/AbsWsParser.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import akka.actor.ActorRef
4 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode.ShutdownCode
5 | import me.mbcu.integrated.mmm.ops.Definitions.{ErrorIgnore, ErrorShutdown}
6 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As.As
7 | import me.mbcu.integrated.mmm.ops.common.AbsWsParser.SendWs
8 | import me.mbcu.integrated.mmm.ops.common.Side.Side
9 | import me.mbcu.scala.MyLogging
10 | import play.api.libs.json.JsValue
11 |
12 | trait AbsWsRequest {
13 |
14 | def subscribe: SendWs
15 |
16 | def cancelOrder(orderId: String, as: As): SendWs
17 |
18 | def newOrder(offer: Offer, as: As): SendWs
19 |
20 | def login(credentials: Credentials): SendWs
21 |
22 | def subsTicker(pair: String): SendWs
23 |
24 | def unsubsTicker(pair: String): SendWs
25 |
26 | }
27 |
28 |
29 | object AbsWsParser {
30 |
31 | trait GotWs {
32 | def requestId: String
33 | }
34 |
35 |
36 | case class SendWs(requestId: String, jsValue: JsValue, as: As)
37 |
38 | case class RemoveOfferWs(isRetry: Boolean, orderId: String, side: Side, override val requestId: String) extends GotWs
39 |
40 | case class GotActiveOrdersWs(offers: Seq[Offer], override val requestId: String) extends GotWs
41 |
42 | case class GotOfferWs(offer: Offer, override val requestId: String) extends GotWs
43 |
44 | case class GotTickerPriceWs(price: Option[BigDecimal], override val requestId: String) extends GotWs
45 |
46 | case class RemovePendingWs(override val requestId: String) extends GotWs
47 |
48 | case class RetryPendingWs(override val requestId: String) extends GotWs
49 |
50 | case class GotSubscribe(override val requestId: String) extends GotWs
51 |
52 | case class LoggedIn(override val requestId: String) extends GotWs
53 | }
54 |
55 | abstract class AbsWsParser(op: ActorRef) extends MyLogging{
56 |
57 | def parse(raw: String, botMap: Map[String, ActorRef]): Unit
58 |
59 | def errorShutdown(shutdownCode: ShutdownCode, code: Int, msg: String): Unit = op ! ErrorShutdown(shutdownCode, code, msg)
60 |
61 | def errorIgnore(code: Int, msg: String, shouldEmail: Boolean = false): Unit = op ! ErrorIgnore(code, msg, shouldEmail)
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/EXCHANGES.md:
--------------------------------------------------------------------------------
1 | (the first item in each exchange is the supported seed method)
2 |
3 | ### hitbtc
4 | - ws: supports `lastTicker` and custom start price
5 |
6 | ### okexRest
7 | - rest: supports `lastTicker`, `cont` and custom start price
8 | - *Make sure API key and secret are correct.* Okex returns errors reserved for sign error (10007, 10005) even a Cloudlare website for non-sign error.
9 | As such 10007, 10005 and html response will be retried.
10 | - no mention of API limit
11 | - TradeHistory
12 | - return sorted without key.
13 | - Use order id (Long) as indicator
14 |
15 | ### yobit
16 | - restGI: supports `lastTicker`, `lastOwn` and custom start price
17 | - The HmacSHA512 signature has to be in lowercased hex
18 | - ~Trade returns order status.~ Trade returns order status which always indicates that the order is unfilled.
19 | - ActiveOrder returns only the current amount which might have been partially filled. To get complete info, we still need to call OrderInfo on each order
20 | - "Admissible quantity of requests to API from user's software is 100 units per minute."
21 | - TradeHistory
22 | - return sorted with special key (Long)
23 | - Use this key as indicator
24 | - Trade History response doesn't have filled / partially filled status. To rebuild a fully filled order, need gather multiple fragmented trades.
25 |
26 | ### fcoin
27 | - rest: supports `lastTicker`,`cont` and custom start price
28 | - Timestamp is required to sign
29 | - API limit = 100 / 10 seconds per user
30 | - TradeHistory
31 | - return sorted without key.
32 | - Use order id (String) as indicator
33 |
34 | ### livecoin
35 | - rest: supports `lastTicker`, `cont` and custom start price
36 | - get client orders's pagination is broken (startRow doesn't work)
37 | - may have super long timeout (more than 2 minutes)
38 | - expect all active order to be at most 100 at a time
39 |
40 | ### btcalpha
41 | - restGI: supports `lastTicker`, `lastOwn`, and custom start price
42 | - **All IP 35.* (Google Cloud) are blocked by BtcAlpha**
43 | - **replace official pair name with underscore** (NOAH/BTC to NOAH_BTC)
44 | - api secret has to be escaped
45 | - single-quotes: escaped with //'
46 | - double-quotes: escaped with ///"
47 | - doesn't have partially-filled status
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/ops/common/OfferTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import me.mbcu.integrated.mmm.actors.OrderRestActor
4 | import org.scalatest.FunSuite
5 |
6 | class OfferTest extends FunSuite {
7 |
8 |
9 | test("sort created_at desc") {
10 | val o1 = Offer("a", "eth_noah", Side.buy, Status.debug, 1532579178L, None, BigDecimal("1000"), BigDecimal("0.00000031"), None)
11 | val o2 = Offer("a", "eth_noah", Side.buy, Status.active, 1532579278L, None, BigDecimal("1000"), BigDecimal("0.00000032"), None)
12 | val o3 = Offer("a", "eth_noah", Side.buy, Status.active, 1532579378L, None, BigDecimal("1000"), BigDecimal("0.00000033"), None)
13 | val o4 = Offer("a", "eth_noah", Side.buy, Status.active, 1532579478L, None, BigDecimal("1000"), BigDecimal("0.00000034"), None)
14 | val o5 = Offer("a", "eth_noah", Side.buy, Status.active, 1532679278L, None, BigDecimal("1000"), BigDecimal("0.00000035"), None)
15 | val a = Seq(o1, o2, o3, o4, o5)
16 | scala.util.Random.shuffle(a)
17 | val res = Offer.sortTimeDesc(a)
18 | assert(res.head.createdAt === 1532679278L)
19 | assert(res.last.createdAt === 1532579178L)
20 | }
21 |
22 | test("partition active orders into two sides") {
23 | val o1 = Offer("a", "eth_noah", Side.buy, Status.debug, 1532579178L, None, BigDecimal("1000"), BigDecimal("0.00000031"), None)
24 | val o2 = Offer("a", "eth_noah", Side.sell, Status.active, 1532579278L, None, BigDecimal("1000"), BigDecimal("0.00000032"), None)
25 | val o3 = Offer("a", "eth_noah", Side.buy, Status.active, 1532579378L, None, BigDecimal("1000"), BigDecimal("0.00000033"), None)
26 | val o4 = Offer("a", "eth_noah", Side.sell, Status.active, 1532579478L, None, BigDecimal("1000"), BigDecimal("0.00000034"), None)
27 | val o5 = Offer("a", "eth_noah", Side.buy, Status.active, 1532679278L, None, BigDecimal("1000"), BigDecimal("0.00000035"), None)
28 | val a = Seq(o1, o2, o3, o4, o5)
29 | scala.util.Random.shuffle(a)
30 | val (p,q) = Offer.splitToBuysSels(a)
31 | assert(p.head.side === Side.buy)
32 | assert(p.size === 3)
33 | assert(p.head.price > p(1).price)
34 | assert(p(1).price > p(2).price)
35 | assert(q.head.side === Side.sell)
36 | assert(q.size === 2)
37 | assert(q.head.price < q(1).price)
38 | }
39 |
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/Definitions.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops
2 |
3 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode.ShutdownCode
4 | import me.mbcu.integrated.mmm.ops.btcalpha.Btcalpha
5 | import me.mbcu.integrated.mmm.ops.common.AbsExchange
6 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.SendRest
7 | import me.mbcu.integrated.mmm.ops.ddex.Ddex
8 | import me.mbcu.integrated.mmm.ops.fcoin.Fcoin
9 | import me.mbcu.integrated.mmm.ops.hitbtc.Hitbtc
10 | import me.mbcu.integrated.mmm.ops.livecoin.Livecoin
11 | import me.mbcu.integrated.mmm.ops.okex.OkexRest
12 | import me.mbcu.integrated.mmm.ops.yobit.Yobit
13 | import play.api.libs.json.{Reads, Writes}
14 |
15 | import scala.language.implicitConversions
16 |
17 | object Definitions {
18 |
19 | val exchangeMap = Map[Exchange.Value, AbsExchange](
20 | Exchange.okexRest -> OkexRest,
21 | Exchange.yobit -> Yobit,
22 | Exchange.fcoin -> Fcoin,
23 | Exchange.ddex -> Ddex,
24 | Exchange.livecoin -> Livecoin,
25 | Exchange.btcalpha -> Btcalpha,
26 | Exchange.hitbtc -> Hitbtc
27 | )
28 |
29 | object ShutdownCode extends Enumeration {
30 | type ShutdownCode = Value
31 | val fatal = Value(-1) // auth, login
32 | val recover = Value(1) // server error
33 | }
34 |
35 | object Exchange extends Enumeration {
36 | type Exchange = Value
37 | val okexRest, yobit, fcoin, ddex, livecoin, btcalpha, hitbtc = Value
38 |
39 | implicit val reads = Reads.enumNameReads(Exchange)
40 | implicit val writes = Writes.enumNameWrites
41 | }
42 |
43 | object Op extends Enumeration {
44 | type Protocol = Value
45 | val rest, ws, restgi, ddex = Value
46 | }
47 |
48 | object Strategies extends Enumeration {
49 | type Strategies = Value
50 | val ppt, fullfixed = Value
51 |
52 | implicit val reads = Reads.enumNameReads(Strategies)
53 | implicit val writes = Writes.enumNameWrites
54 | }
55 |
56 |
57 | case class ErrorIgnore(code: Int, msg: String, shouldEmail: Boolean)
58 | case class ErrorShutdown(shutdown: ShutdownCode, code: Int, msg: String)
59 | case class ErrorRetryRest(sendRequest: SendRest, code: Int, msg: String, shouldEmail: Boolean)
60 |
61 |
62 | object Settings {
63 | val cachingEmailSeconds:Int = 60
64 | val getActiveSeconds:Int = 5
65 | val getFilledSeconds:Int = 5
66 | val intervalLogSeconds = 15
67 | val intervalSeedSeconds:Int = 5
68 | val wsInitSeconds:Int = 1
69 | }
70 |
71 | }
72 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/okex/OkexParameters.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.okex
2 |
3 | import java.security.MessageDigest
4 |
5 | import me.mbcu.integrated.mmm.ops.common.Side.Side
6 | import me.mbcu.integrated.mmm.ops.okex.OkexStatus.OkexStatus
7 | import play.api.libs.functional.syntax._
8 | import play.api.libs.json._
9 |
10 | object OkexStatus extends Enumeration {
11 | type OkexStatus = Value
12 | val filled = Value(2)
13 | val unfilled = Value(0)
14 |
15 | implicit val enumFormat = new Format[OkexStatus] {
16 | override def reads(json: JsValue): JsResult[OkexStatus] = json.validate[Int].map(OkexStatus(_))
17 | override def writes(enum: OkexStatus) = JsNumber(enum.id)
18 | }
19 | }
20 |
21 | object OkexParameters {
22 | val md5: MessageDigest = MessageDigest.getInstance("MD5")
23 |
24 | implicit val jsonFormat = Json.format[OkexParameters]
25 |
26 | object Implicits {
27 | implicit val writes = new Writes[OkexParameters] {
28 | def writes(r: OkexParameters): JsValue = Json.obj(
29 | "sign" -> r.sign,
30 | "api_key" -> r.api_key,
31 | "symbol" -> r.symbol,
32 | "order_id" -> r.order_id,
33 | "type" -> r.symbol,
34 | "price" -> r.price,
35 | "amount" -> r.amount,
36 | "status" -> r.status,
37 | "current_page" -> r.current_page,
38 | "page_length" -> r.page_length
39 | )
40 | }
41 |
42 | implicit val reads: Reads[OkexParameters] = (
43 | (JsPath \ "sign").readNullable[String] and
44 | (JsPath \ "api_key").read[String] and
45 | (JsPath \ "symbol").readNullable[String] and
46 | (JsPath \ "order_id").readNullable[String] and
47 | (JsPath \ "type").readNullable[Side] and
48 | (JsPath \ "price").readNullable[BigDecimal] and
49 | (JsPath \ "amount").readNullable[BigDecimal] and
50 | (JsPath \ "status").readNullable[OkexStatus] and
51 | (JsPath \ "current_page").readNullable[Int] and
52 | (JsPath \ "page_length").readNullable[Int]
53 | ) (OkexParameters.apply _)
54 | }
55 |
56 | }
57 |
58 | case class OkexParameters (
59 | sign : Option[String],
60 | api_key : String,
61 | symbol : Option[String],
62 | order_id : Option[String],
63 | `type` : Option[Side],
64 | price : Option[BigDecimal],
65 | amount: Option[BigDecimal],
66 | status : Option[OkexStatus] = None,
67 | current_page : Option[Int] = None,
68 | page_length : Option[Int] = None
69 |
70 |
71 | )
72 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/OpWsActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import akka.actor.{ActorRef, Props}
4 | import akka.dispatch.ExecutionContexts.global
5 | import me.mbcu.integrated.mmm.actors.OpWsActor._
6 | import me.mbcu.integrated.mmm.actors.WsActor._
7 | import me.mbcu.integrated.mmm.ops.Definitions.{ErrorIgnore, ErrorShutdown, Settings}
8 | import me.mbcu.integrated.mmm.ops.common.AbsWsParser.{GotSubscribe, LoggedIn, SendWs}
9 | import me.mbcu.integrated.mmm.ops.common._
10 | import me.mbcu.scala.MyLogging
11 |
12 | import scala.concurrent.ExecutionContextExecutor
13 | import scala.concurrent.duration._
14 | import scala.language.postfixOps
15 |
16 | object OpWsActor {
17 |
18 | case class QueueWs(sendWs: Seq[SendWs])
19 |
20 | }
21 |
22 | class OpWsActor(exchangeDef: AbsExchange, bots: Seq[Bot], fileActor: ActorRef) extends AbsOpActor(exchangeDef, bots, fileActor) with MyLogging {
23 | private var base: Option[ActorRef] = None
24 | private val wsEx: AbsWsExchange = exchangeDef.asInstanceOf[AbsWsExchange]
25 | private val parser: AbsWsParser = wsEx.getParser(self)
26 | private implicit val ec: ExecutionContextExecutor = global
27 | private var client: Option[ActorRef] = None
28 | private var botMap: Map[String, ActorRef] = Map.empty
29 |
30 | override def receive: Receive = {
31 |
32 | case "start" =>
33 | base = Some(sender)
34 | botMap ++= bots.map(bot => bot.pair -> context.actorOf(Props(new OrderWsActor(bot, exchangeDef, wsEx.getRequest)), name = s"${bot.pair}"))
35 | botMap.values.foreach(_ ! "start")
36 | client = Some(context.actorOf(Props(new WsActor()), name = "wsclient"))
37 | client.foreach(_ ! "start")
38 |
39 | case WsRequestClient =>
40 | botMap.values.foreach(_ ! WsRequestClient)
41 | context.system.scheduler.scheduleOnce(Settings.wsInitSeconds seconds, self, "reconnect websocket")
42 |
43 | case "reconnect websocket" => client.foreach(_ ! WsConnect(exchangeDef.endpoint))
44 |
45 | case WsConnected => send(wsEx.getRequest.login(bots.head.credentials))
46 |
47 | case WsGotText(text) => parser.parse(text, botMap)
48 |
49 | case a: LoggedIn => send(wsEx.getRequest.subscribe)
50 |
51 | case a: GotSubscribe => botMap.values.foreach(_ ! a)
52 |
53 | case QueueWs(sendWs) => sendWs foreach send
54 |
55 | case a: ErrorShutdown => base foreach (_ ! a)
56 |
57 | case a: ErrorIgnore => base foreach (_ ! a)
58 |
59 | }
60 |
61 | def send(s: SendWs): Unit = {
62 | info(s"${s.as} : ${s.jsValue.toString}")
63 | client foreach(_ ! SendJs(s.jsValue))
64 | }
65 |
66 |
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/OpRestActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import akka.actor.{ActorRef, Cancellable, Props}
4 | import akka.dispatch.ExecutionContexts.global
5 | import me.mbcu.integrated.mmm.ops.Definitions.{ErrorIgnore, ErrorRetryRest, ErrorShutdown}
6 | import me.mbcu.integrated.mmm.ops.common.AbsOrder.{CheckSafeForSeed, QueueRequest, SafeForSeed}
7 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
8 | import me.mbcu.integrated.mmm.ops.common.{AbsExchange, AbsOpActor, AbsOrder, Bot}
9 | import me.mbcu.scala.MyLogging
10 |
11 | import scala.concurrent.ExecutionContextExecutor
12 | import scala.concurrent.duration._
13 | import scala.language.postfixOps
14 |
15 | object OpRestActor extends MyLogging {
16 | }
17 |
18 | class OpRestActor(exchangeDef: AbsExchange, bots: Seq[Bot], fileActor: ActorRef) extends AbsOpActor(exchangeDef, bots, fileActor) with MyLogging {
19 | private implicit val ec: ExecutionContextExecutor = global
20 | private var base: Option[ActorRef] = None
21 | private var rest: Option[ActorRef] = None
22 | private var dqCancellable: Option[Cancellable] = None
23 | private val q = new scala.collection.mutable.Queue[SendRest]
24 | private val nos = scala.collection.mutable.Set[NewOrder]()
25 |
26 | override def receive: Receive = {
27 |
28 | case "start" =>
29 | base = Some(sender)
30 | rest = Some(context.actorOf(exchangeDef.getActorRefProps))
31 | rest foreach (_ ! StartRestActor)
32 | bots.foreach(bot => {
33 | val book = context.actorOf(Props(new OrderRestActor(bot, exchangeDef, fileActor)), name= s"${Bot.filePath(bot)}")
34 | book ! "start"
35 | })
36 | self ! "init dequeue scheduler"
37 |
38 | case "init dequeue scheduler" => dqCancellable = Some(context.system.scheduler.schedule(1 second, exchangeDef.intervalMillis milliseconds, self, "dequeue"))
39 |
40 | case "dequeue" =>
41 | if (q.nonEmpty) {
42 | val next = q.dequeue()
43 | next match {
44 | case order: NewOrder => nos += order
45 | case _ =>
46 | }
47 | rest foreach(_ ! next)
48 | }
49 |
50 | case QueueRequest(seq) => q ++= seq
51 |
52 | case a: GotNewOrderId => nos -= a.send
53 |
54 | case CheckSafeForSeed(ref, bot) => ref ! SafeForSeed(AbsOrder.isSafeForSeed(q, nos, bot))
55 |
56 | case ErrorRetryRest(sendRequest, code, msg, shouldEmail) =>
57 | q += sendRequest
58 | base foreach(_ ! ErrorRetryRest(sendRequest, code, msg, shouldEmail))
59 |
60 | case a : ErrorShutdown => base foreach(_ ! a)
61 |
62 | case a : ErrorIgnore => base foreach(_ ! a)
63 |
64 | }
65 |
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/SesActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import akka.actor.{Actor, ActorRef, Cancellable}
4 | import akka.dispatch.ExecutionContexts.global
5 | import com.amazonaws.regions.Regions
6 | import com.amazonaws.services.simpleemail.model.SendEmailResult
7 | import jp.co.bizreach.ses.SESClient
8 | import jp.co.bizreach.ses.models.{Address, Content, Email}
9 | import me.mbcu.integrated.mmm.actors.SesActor.{CacheMessages, MailSent}
10 | import me.mbcu.integrated.mmm.ops.Definitions
11 | import me.mbcu.scala.MyLogging
12 |
13 | import scala.collection.mutable.ListBuffer
14 | import scala.concurrent.ExecutionContextExecutor
15 | import scala.concurrent.duration._
16 | import scala.language.postfixOps
17 | import scala.util.Try
18 |
19 | object SesActor {
20 |
21 | case class CacheMessages(msg :String, shutdownCode : Option[Int])
22 |
23 | object MailTimer
24 |
25 | case class MailSent(t : Try[SendEmailResult], shutdownCode : Option[Int])
26 |
27 | }
28 |
29 | class SesActor(sesKey : Option[String], sesSecret : Option[String], emails : Option[Seq[String]]) extends Actor with MyLogging{
30 | private implicit val ec: ExecutionContextExecutor = global
31 | val title = "MMM Error"
32 | var tos : Seq[Address] = Seq.empty
33 | var cli : Option[SESClient] = None
34 | private implicit val region: Regions = Regions.US_EAST_1
35 | private var base : Option[ActorRef] = None
36 | private val cache = new ListBuffer[(String, Option[Int])]
37 | var sendCancellable : Option[Cancellable] = None
38 |
39 | override def receive: Receive = {
40 |
41 | case "start" =>
42 | base = Some(sender)
43 | (sesKey, sesSecret, emails) match {
44 | case (Some(k), Some(s), Some(m)) =>
45 | cli = Some(SESClient(k, s))
46 | this.tos ++= m map (Address(_))
47 | case _ =>
48 | }
49 |
50 | case CacheMessages(msg, shutdownCode) =>
51 | sendCancellable foreach (_.cancel())
52 | sendCancellable = Some(context.system.scheduler.scheduleOnce(Definitions.Settings.cachingEmailSeconds second, self, "execute send"))
53 | cache += ((msg, shutdownCode))
54 |
55 | case "execute send" =>
56 | val shutdownCode = cache flatMap(_._2) reduceOption(_ min _)
57 | val msg = cache map (_._1) mkString "\n\n"
58 | send(title, msg, shutdownCode)
59 | }
60 |
61 | def send(title: String, body: String, shutdownCode : Option[Int] = None): Unit = {
62 | (tos.headOption, cli) match {
63 | case (Some(address), Some(c)) =>
64 | val email = Email(Content(title), address, Some(Content(body)), None, tos)
65 | val future = c send email
66 | future.onComplete(t => base foreach(_ ! MailSent(t, shutdownCode)))
67 | case _ =>
68 | }
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/ops/okex/OkexRestActorTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.okex
2 |
3 | import me.mbcu.integrated.mmm.ops.common.Status
4 | import org.scalatest.FunSuite
5 | import play.api.libs.json.{JsObject, Json}
6 |
7 | class OkexRestActorTest extends FunSuite{
8 |
9 | test("parse filled") {
10 | val a =
11 | """
12 | |{
13 | | "result": true,
14 | | "total": 5,
15 | | "currency_page": 1,
16 | | "page_length": 200,
17 | | "orders": [
18 | | {
19 | | "amount": 1E+1,
20 | | "avg_price": 0.00008081,
21 | | "create_date": 1532873222000,
22 | | "deal_amount": 1E+1,
23 | | "order_id": 40611446,
24 | | "orders_id": 40611446,
25 | | "price": 0.00008081,
26 | | "status": 2,
27 | | "symbol": "trx_eth",
28 | | "type": "sell"
29 | | },
30 | | {
31 | | "amount": 1E+1,
32 | | "avg_price": 0,
33 | | "create_date": 1532873073000,
34 | | "deal_amount": 0,
35 | | "order_id": 40610842,
36 | | "orders_id": 40610842,
37 | | "price": 0.00008117,
38 | | "status": -1,
39 | | "symbol": "trx_eth",
40 | | "type": "sell"
41 | | },
42 | | {
43 | | "amount": 1E+1,
44 | | "avg_price": 0,
45 | | "create_date": 1532872788000,
46 | | "deal_amount": 0,
47 | | "order_id": 40609669,
48 | | "orders_id": 40609669,
49 | | "price": 0.00008118,
50 | | "status": -1,
51 | | "symbol": "trx_eth",
52 | | "type": "sell"
53 | | },
54 | | {
55 | | "amount": 1E+1,
56 | | "avg_price": 0.00008090,
57 | | "create_date": 1532867632000,
58 | | "deal_amount": 1E+1,
59 | | "order_id": 40586533,
60 | | "orders_id": 40586533,
61 | | "price": 0.0000809,
62 | | "status": 2,
63 | | "symbol": "trx_eth",
64 | | "type": "sell"
65 | | },
66 | | {
67 | | "amount": 1E+1,
68 | | "avg_price": 0.00008057,
69 | | "create_date": 1532865557000,
70 | | "deal_amount": 1E+1,
71 | | "order_id": 40575742,
72 | | "orders_id": 40575742,
73 | | "price": 0.00008057,
74 | | "status": 2,
75 | | "symbol": "trx_eth",
76 | | "type": "sell"
77 | | }
78 | | ]
79 | |}
80 | """.stripMargin
81 |
82 | val res = OkexRestActor.parseFilled(Json.parse(a), 40575742)
83 | assert(res._1.size === 2)
84 | assert(res._1.head.id.toLong < res._1(1).id.toLong)
85 |
86 |
87 | }
88 |
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/OpGIActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import akka.actor.{Actor, ActorRef, Cancellable, Props}
4 | import akka.dispatch.ExecutionContexts.global
5 | import me.mbcu.integrated.mmm.actors.OrderGIActor.{CheckSafeForGI, SafeForGI}
6 | import me.mbcu.integrated.mmm.ops.Definitions.{ErrorIgnore, ErrorRetryRest, ErrorShutdown}
7 | import me.mbcu.integrated.mmm.ops.common.AbsOrder.{CheckSafeForSeed, QueueRequest, SafeForSeed}
8 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
9 | import me.mbcu.integrated.mmm.ops.common.{AbsExchange, AbsOrder, Bot}
10 | import me.mbcu.scala.MyLogging
11 |
12 | import scala.collection.mutable
13 | import scala.concurrent.ExecutionContextExecutor
14 | import scala.concurrent.duration._
15 | import scala.language.postfixOps
16 |
17 | object OpGIActor {
18 | def isSafeForGI(q: mutable.Queue[SendRest], nos: mutable.Set[NewOrder], bot: Bot): Boolean =
19 | (q.filter(_.bot == bot) ++ nos.filter(_.bot == bot)).isEmpty
20 |
21 | }
22 |
23 | class OpGIActor(exchangeDef: AbsExchange, bots: Seq[Bot]) extends Actor with MyLogging {
24 | private implicit val ec: ExecutionContextExecutor = global
25 | private var base: Option[ActorRef] = None
26 | private var rest: Option[ActorRef] = None
27 | private var dqCancellable: Option[Cancellable] = None
28 | private val q = new scala.collection.mutable.Queue[SendRest]
29 | private val nos = scala.collection.mutable.Set[NewOrder]()
30 |
31 | override def receive: Receive = {
32 |
33 | case "start" =>
34 | base = Some(sender)
35 | rest = Some(context.actorOf(exchangeDef.getActorRefProps))
36 | rest foreach (_ ! StartRestActor)
37 | bots.foreach(bot => {
38 | val book = context.actorOf(Props(new OrderGIActor(bot, exchangeDef)), name = s"${Bot.filePath(bot)}")
39 | book ! "start"
40 | })
41 | self ! "init dequeue scheduler"
42 |
43 | case "init dequeue scheduler" => dqCancellable = Some(context.system.scheduler.schedule(1 second, exchangeDef.intervalMillis milliseconds, self, "dequeue"))
44 |
45 | case "dequeue" =>
46 | if (q.nonEmpty) {
47 | val next = q.dequeue()
48 | next match {
49 | case order: NewOrder => nos += order
50 | case _ =>
51 | }
52 | rest foreach (_ ! next)
53 | }
54 |
55 | case QueueRequest(seq) => q ++= seq
56 |
57 | case a: CheckSafeForGI => a.ref ! SafeForGI(OpGIActor.isSafeForGI(q, nos, a.bot))
58 |
59 | case a: CheckSafeForSeed => a.ref ! SafeForSeed(AbsOrder.isSafeForSeed(q, nos, a.bot))
60 |
61 | case a: GetOrderInfo => q += a
62 |
63 | case a: GotNewOrderId => nos -= a.send
64 |
65 | case ErrorRetryRest(sendRequest, code, msg, shouldEmail) =>
66 | base foreach (_ ! ErrorRetryRest(sendRequest, code, msg, shouldEmail))
67 | q += sendRequest
68 |
69 | case a: ErrorShutdown => base foreach (_ ! a)
70 |
71 | case a: ErrorIgnore => base foreach (_ ! a)
72 |
73 | }
74 |
75 |
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/yobit/YobitRequest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.yobit
2 |
3 | import me.mbcu.integrated.mmm.ops.common.Side.Side
4 | import me.mbcu.integrated.mmm.ops.common.{AbsRestRequest, Credentials}
5 |
6 | object YobitRequest extends AbsRestRequest {
7 |
8 | case class YobitParams(sign: String, params: String)
9 |
10 | def nonce : Long = System.currentTimeMillis() / 1000 - Yobit.nonceFactor
11 |
12 | def addNonce(params: Map[String, String]): Map[String, String] = params + ("nonce" -> nonce.toString)
13 |
14 | def body(params: Map[String, String]): String = params.toSeq.sortBy(_._1).map(v => s"${v._1}=${v._2}").mkString("&")
15 |
16 | def toYobitParams(params : Map[String, String], secret: String) : YobitParams = {
17 | val withNonce = body(addNonce(params))
18 | YobitParams(toHex(signHmacSHA512(secret, withNonce), isCapital = false), withNonce)
19 | }
20 |
21 | def ownTrades(credentials: Credentials, pair: String) : YobitParams = ownTrades( credentials.signature, pair )
22 |
23 | def ownTrades(secret:String, pair: String) : YobitParams = {
24 | val params = Map(
25 | "method" -> "TradeHistory", //default order is DESC
26 | "pair" -> pair,
27 | "count" -> 100.toString
28 | )
29 | toYobitParams(params, secret)
30 | }
31 |
32 | def infoOrder(credentials: Credentials, orderId: String) : YobitParams = infoOrder(credentials.signature, orderId: String)
33 |
34 | def infoOrder(secret: String, orderId: String) : YobitParams = {
35 | val params = Map(
36 | "method" -> "OrderInfo",
37 | "order_id" -> orderId
38 | )
39 | toYobitParams(params, secret)
40 | }
41 |
42 | def newOrder(credentials: Credentials, pair: String, `type`: Side, price: BigDecimal, amount: BigDecimal) : YobitParams =
43 | newOrder(credentials.signature, pair, `type`, price, amount)
44 |
45 | def newOrder(secret: String, pair: String, `type`: Side, price: BigDecimal, amount: BigDecimal) : YobitParams = {
46 | val params = Map(
47 | "method" -> "Trade",
48 | "pair" -> pair,
49 | "type" -> `type`.toString,
50 | "rate" -> price.bigDecimal.toPlainString,
51 | "amount" -> amount.bigDecimal.toPlainString
52 | )
53 | toYobitParams(params, secret)
54 | }
55 |
56 | def cancelOrder(credentials: Credentials, orderId : String) : YobitParams = cancelOrder(credentials.signature, orderId)
57 |
58 | def cancelOrder(secret : String, orderId : String) : YobitParams = {
59 | val params = Map(
60 | "method" -> "CancelOrder",
61 | "order_id" -> orderId
62 | )
63 | toYobitParams(params, secret)
64 | }
65 |
66 | def activeOrders(credentials: Credentials, pair: String) : YobitParams = activeOrders(credentials.signature, pair)
67 |
68 | def activeOrders(secret: String, pair: String) : YobitParams = {
69 | val params = Map(
70 | "method" -> "ActiveOrders",
71 | "pair" -> pair
72 | )
73 | toYobitParams(params, secret)
74 | }
75 |
76 | def getInfo(credentials: Credentials) : YobitParams = getInfo(credentials.signature)
77 |
78 | def getInfo(secret: String) : YobitParams = {
79 | val params = Map(
80 | "method" -> "getInfo"
81 | )
82 | toYobitParams(params, secret)
83 | }
84 |
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/FileActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import java.io._
4 | import java.nio.charset.StandardCharsets
5 | import java.nio.file.StandardWatchEventKinds._
6 | import java.nio.file.{Files, Paths}
7 |
8 | import akka.actor.{Actor, ActorRef, Props}
9 | import com.beachape.filemanagement.Messages._
10 | import com.beachape.filemanagement.MonitorActor
11 | import com.beachape.filemanagement.RegistryTypes._
12 | import me.mbcu.integrated.mmm.actors.BaseActor.ConfigReady
13 | import me.mbcu.integrated.mmm.actors.FileActor.millisFileName
14 | import me.mbcu.integrated.mmm.actors.OrderRestActor._
15 | import me.mbcu.integrated.mmm.ops.common.{Bot, BotCache, Config}
16 | import me.mbcu.scala.MyLogging
17 | import play.api.libs.json.Json
18 |
19 | import scala.util.{Failure, Success, Try}
20 |
21 | object FileActor extends MyLogging {
22 | def props(configPath: String, millisPath: String): Props = Props(new FileActor(configPath, millisPath))
23 |
24 | def readConfig(path: String): Option[Config] = {
25 | val source = scala.io.Source.fromFile(path)
26 | val rawJson = try source.mkString finally source.close()
27 | Try(Json.parse(rawJson).as[Config]) match {
28 | case Success(config) => Some(config)
29 | case Failure(e) =>
30 | error(e.getMessage)
31 | None
32 | }
33 | }
34 |
35 |
36 | def millisFileName(msPath: String, bot: Bot): String = msPath + Bot.millisFileFormat.format(bot.exchange.toString, Bot.filePath(bot))
37 |
38 | def readLastCounterId(path: String, bot: Bot): BotCache = {
39 | val fName = millisFileName(path, bot)
40 | if (!Files.exists(Paths.get(fName))) {
41 | val pw = new PrintWriter(new File(fName))
42 | pw.close()
43 | }
44 | val raw = readFile(fName)
45 | Try(Json.parse(raw).as[BotCache]) match {
46 | case Success(bc) => bc
47 | case Failure(e) =>
48 | e.getMessage match {
49 | case a if a contains "No content to map" => error("File is empty. Creating default.")
50 | case _ => error _
51 | }
52 | BotCache.default
53 | }
54 | }
55 |
56 | def readFile(path: String): String = {
57 | val source = scala.io.Source.fromFile(path)
58 | try source.mkString finally source.close()
59 | }
60 |
61 | def writeFile(path: String, content: String): Unit = Files.write(Paths.get(path), content.getBytes(StandardCharsets.UTF_8))
62 |
63 | }
64 |
65 | class FileActor(cfgPath: String, msPath: String) extends Actor {
66 | private var base: Option[ActorRef] = None
67 |
68 | override def receive: Receive = {
69 |
70 | case "start" =>
71 | import FileActor._
72 | base = Some(sender)
73 | sender ! ConfigReady(readConfig(cfgPath))
74 |
75 | case "listen" =>
76 | val fileMonitorActor = context.actorOf(MonitorActor(concurrency = 2))
77 | val modifyCallbackFile: Callback = { path => println(s"Something was modified in a file: $path") }
78 | val file = Paths get cfgPath
79 | /*
80 | This will receive callbacks for just the one file
81 | */
82 | fileMonitorActor ! RegisterCallback(
83 | event = ENTRY_MODIFY,
84 | path = file,
85 | callback = modifyCallbackFile
86 | )
87 |
88 | case GetLastCounter(book, bot, as) => book ! GotLastCounter(FileActor.readLastCounterId(msPath, bot), as)
89 |
90 | case WriteLastCounter(book, bot, m) => FileActor.writeFile(millisFileName(msPath, bot), Json.toJson(m).toString())
91 |
92 | }
93 |
94 |
95 | }
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/btcalpha/BtcalphaRequest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.btcalpha
2 |
3 | import me.mbcu.integrated.mmm.ops.btcalpha.BtcalphaRequest.BtcalphaStatus.BtcalphaStatus
4 | import me.mbcu.integrated.mmm.ops.common.Side.Side
5 | import me.mbcu.integrated.mmm.ops.common.{AbsRestRequest, Credentials}
6 | import me.mbcu.scala.MyLogging
7 | import play.api.libs.json._
8 |
9 | object BtcalphaRequest extends AbsRestRequest with MyLogging {
10 |
11 | def getNonce: String = System.currentTimeMillis().toString
12 |
13 | object BtcalphaStatus extends Enumeration {
14 | type BtcalphaStatus = Value
15 | val active: BtcalphaRequest.BtcalphaStatus.Value = Value(1)
16 | val cancelled: BtcalphaRequest.BtcalphaStatus.Value = Value(2)
17 | val done: BtcalphaRequest.BtcalphaStatus.Value = Value(3)
18 |
19 | implicit val enumFormat: Format[BtcalphaStatus] = new Format[BtcalphaStatus] {
20 | override def reads(json: JsValue): JsResult[BtcalphaStatus] = json.validate[Int].map(BtcalphaStatus(_))
21 | override def writes(enum: BtcalphaStatus) = JsNumber(enum.id)
22 | }
23 | }
24 |
25 | def sanitizeSecret(s: String): String = StringContext treatEscapes s
26 |
27 | case class BtcalphaParams(sign: String, nonce: String, url: String = "", params: String)
28 |
29 | def getTickers(pair: String): BtcalphaParams = BtcalphaParams("unused", "unused", Btcalpha.endpoint.format(s"charts/$pair/1/chart/?format=json&limit=1"), "unused")
30 |
31 | def getOrders(credentials: Credentials, pair: String, status: BtcalphaStatus): BtcalphaParams = getOrders(credentials.pKey, credentials.signature, pair, status)
32 |
33 | def getOrders(key:String, secret: String, pair: String, status: BtcalphaStatus): BtcalphaParams = {
34 | val params = Map(
35 | "pair" -> pair,
36 | "status" -> status.id.toString
37 | )
38 | val sorted = sortToForm(params)
39 | BtcalphaParams(sign(key, secret), getNonce, Btcalpha.endpoint.format(s"v1/orders/own/?$sorted"), sorted)
40 | }
41 |
42 | def newOrder(credentials: Credentials, pair: String, side: Side, price: BigDecimal, amount: BigDecimal) : BtcalphaParams =
43 | newOrder(credentials.pKey, credentials.signature, pair, side, price, amount)
44 |
45 | def newOrder(key: String, secret: String, pair: String, side: Side, price: BigDecimal, amount: BigDecimal) : BtcalphaParams = {
46 | //67a6cfe5-c6fc-46fd-8e4d-b7f87533fdc7amount=1000&pair=NOAH_ETH&price=0.000001&type=buy
47 | val params = Map(
48 | "pair" -> pair,
49 | "type" -> side.toString,
50 | "price" -> price.bigDecimal.toPlainString,
51 | "amount" -> amount.bigDecimal.toPlainString
52 | )
53 |
54 | val sorted = sortToForm(params)
55 | val signed = sign(arrangePost(key, sorted), secret)
56 | BtcalphaParams(signed, getNonce, Btcalpha.endpoint.format("v1/order/"), sorted)
57 | }
58 |
59 | def cancelOrder(credentials: Credentials, id: String) : BtcalphaParams = cancelOrder(credentials.pKey, credentials.signature, id)
60 |
61 | def cancelOrder(key: String, secret: String, id: String) : BtcalphaParams = {
62 | val params = s"order=$id"
63 | val signed = sign(arrangePost(key, params), secret)
64 | BtcalphaParams(signed, getNonce, Btcalpha.endpoint.format("v1/order-cancel/"), params)
65 | }
66 |
67 | def getOrderInfo(credentials: Credentials, id: String) : BtcalphaParams = getOrderInfo(credentials.pKey, credentials.signature, id)
68 |
69 | def getOrderInfo(key: String, secret: String, id: String) : BtcalphaParams = {
70 | val signed = sign(key, secret)
71 | BtcalphaParams(signed, getNonce, Btcalpha.endpoint.format(s"v1/order/$id/"), id)
72 | }
73 |
74 | def arrangePost(key: String, sorted: String): String = s"$key$sorted"
75 |
76 | def sign(payload: String, secret: String): String = toHex(signHmacSHA256(sanitizeSecret(secret), payload), isCapital = false)
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/BaseActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import akka.actor.{Actor, ActorRef, Props}
4 | import akka.dispatch.ExecutionContexts.global
5 | import me.mbcu.integrated.mmm.actors.BaseActor.{ConfigReady, Shutdown}
6 | import me.mbcu.integrated.mmm.actors.SesActor.{CacheMessages, MailSent}
7 | import me.mbcu.integrated.mmm.ops.Definitions
8 | import me.mbcu.integrated.mmm.ops.Definitions._
9 | import me.mbcu.integrated.mmm.ops.common.{Bot, Config}
10 | import me.mbcu.scala.MyLogging
11 |
12 | import scala.concurrent.duration._
13 | import scala.concurrent.{ExecutionContext, ExecutionContextExecutor}
14 | import scala.language.postfixOps
15 | import scala.util.{Failure, Success}
16 |
17 | object BaseActor {
18 |
19 | case class ConfigReady(config: Option[Config])
20 |
21 | case class HandleRPCError(shutdowncode: Option[Int], errorCode: Int, msg: String)
22 |
23 | case class Shutdown(code: Option[Int])
24 |
25 | case class HandleError(msg: String, code: Option[Int] = None)
26 |
27 | case class ConfigUpdate(bot:Bot, lastGetFilledMs: Long )
28 |
29 | }
30 |
31 | class BaseActor(configPath: String, msPath: String) extends Actor with MyLogging {
32 | private var config: Option[Config] = None
33 | private var ses: Option[ActorRef] = None
34 | private implicit val ec: ExecutionContextExecutor = global
35 | val botName = "bot-%s"
36 | private var fileActor: Option[ActorRef] = None
37 |
38 | def receive: Receive = {
39 |
40 | case "start" =>
41 | fileActor = Some(context.actorOf(Props(new FileActor(configPath, msPath)), name ="fio"))
42 | fileActor foreach(_ ! "start")
43 |
44 | case ConfigReady(tryCfg) => tryCfg match {
45 |
46 | case Some(c) =>
47 | config = Some(c)
48 |
49 |
50 | ses = Some(context.actorOf(Props(new SesActor(c.env.sesKey, c.env.sesSecret, c.env.emails)), name = "ses"))
51 | ses foreach (_ ! "start")
52 | self ! "init bot"
53 |
54 | case _ => self ! Shutdown(Some(ShutdownCode.fatal.id))
55 | }
56 |
57 | case "init bot" =>
58 | (config, fileActor) match {
59 | case (Some(c), Some(f)) =>
60 | Config.groupBots(c.bots).zipWithIndex.foreach {
61 | case (b, i) =>
62 | val excDef = Definitions.exchangeMap(b._1)
63 | val props = excDef.protocol match {
64 | case Op.rest => Props(new OpRestActor(excDef, b._2, f))
65 | case Op.ddex => Props(new OpDdexActor(excDef, b._2, f))
66 | case Op.ws => Props(new OpWsActor(excDef, b._2, f))
67 | case Op.restgi => Props(new OpGIActor(excDef, b._2))
68 | }
69 | val opActor = context.actorOf(props, botName.format(i))
70 | opActor ! "start"
71 | }
72 | case _ =>
73 | }
74 |
75 | case ErrorShutdown(shutdown, code, msg) => ses foreach(_ ! CacheMessages(msg, Some(shutdown.id)))
76 |
77 | case ErrorIgnore(code, msg, shouldEmail) => if(shouldEmail) ses foreach(_ ! CacheMessages(msg, None))
78 |
79 | case ErrorRetryRest(sendRequest, code, msg, shouldEmail) => if(shouldEmail) ses foreach(_ ! CacheMessages(msg, None))
80 |
81 | case MailSent(t, shutdownCode) =>
82 | t match {
83 | case Success(_) => info("Email Sent")
84 | case Failure(c) => info(
85 | s"""Failed sending email
86 | |${c.getMessage}
87 | """.stripMargin)
88 | }
89 | self ! Shutdown(shutdownCode)
90 |
91 | case Shutdown(code) =>
92 | code foreach (_ => {
93 | info(s"Stopping application, shutdown code $code")
94 | implicit val executionContext: ExecutionContext = context.system.dispatcher
95 | context.system.scheduler.scheduleOnce(Duration.Zero)(System.exit(_))
96 | })
97 |
98 | }
99 |
100 | }
101 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/fcoin/FcoinRequest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.fcoin
2 |
3 | import java.util.Base64
4 |
5 | import me.mbcu.integrated.mmm.ops.common.Side.Side
6 | import me.mbcu.integrated.mmm.ops.common.{AbsRestRequest, Credentials}
7 | import me.mbcu.integrated.mmm.ops.fcoin.FcoinMethod.FcoinMethod
8 | import me.mbcu.integrated.mmm.ops.fcoin.FcoinState.FcoinState
9 | import play.api.libs.json.{Json, Reads, Writes}
10 |
11 | object FcoinState extends Enumeration {
12 | type FcoinState = Value
13 | val submitted, partial_filled, partial_canceled, filled, canceled, pending_cancel = Value
14 |
15 | implicit val read = Reads.enumNameReads(FcoinState)
16 | implicit val write = Writes.enumNameWrites
17 | }
18 |
19 | object FcoinMethod extends Enumeration {
20 | type FcoinMethod = Value
21 | val POST, GET = Value
22 | }
23 |
24 | object FcoinRestRequest extends AbsRestRequest {
25 |
26 | case class FcoinParams(sign: String, params: String, ts: Long, url: String = "", js: String)
27 |
28 | def getOrders(secret: String, pair: String, state: FcoinState, after: Int) : FcoinParams = {
29 | val params = Map(
30 | "symbol" -> pair,
31 | "states" -> state.toString,
32 | "after" -> after.toString,
33 | "limit" -> 100.toString
34 | )
35 | sign(params, secret, FcoinMethod.GET, Fcoin.endpoint.format("orders"))
36 | }
37 |
38 |
39 | def getTickers(pair: String): FcoinParams = FcoinParams("unused", pair, System.currentTimeMillis(), Fcoin.endpoint.format(s"market/ticker/$pair"), "unused")
40 |
41 | def getOrders(credentials: Credentials, pair: String, state: FcoinState, after: Int) : FcoinParams = getOrders(credentials.signature, pair, state, after)
42 |
43 | def newOrder(credentials: Credentials, pair: String, side: Side, price: BigDecimal, amount: BigDecimal) : FcoinParams
44 | = newOrder(credentials.signature, pair, side, price, amount)
45 |
46 | def newOrder(secret: String, pair: String, side: Side, price: BigDecimal, amount: BigDecimal) : FcoinParams = {
47 | val params = Map(
48 | "symbol" -> pair,
49 | "side" -> side.toString,
50 | "type" -> "limit",
51 | "price" -> price.bigDecimal.toPlainString,
52 | "amount" -> amount.bigDecimal.toPlainString
53 | )
54 | sign(params, secret, FcoinMethod.POST, Fcoin.endpoint.format("orders"))
55 | }
56 |
57 | def cancelOrder(secret : String, orderId : String): FcoinParams = {
58 | val params = Map(
59 | "order_id" -> orderId
60 | )
61 |
62 | val url = Fcoin.endpoint.format(s"orders/$orderId/submit-cancel")
63 | sign(params, secret, FcoinMethod.POST, url)
64 | }
65 |
66 | def cancelOrder(credentials: Credentials, orderId: String): FcoinParams = cancelOrder(credentials.signature, orderId)
67 |
68 | def getOrderInfo(secret: String, orderId: String): FcoinParams = {
69 | val params = Map(
70 | "order_id" -> orderId
71 | )
72 | val url = Fcoin.endpoint.format(s"orders/$orderId")
73 | sign(params, secret, FcoinMethod.GET, url)
74 | }
75 |
76 | def getOrderInfo(credentials: Credentials, orderId: String): FcoinParams = getOrderInfo(credentials.signature, orderId)
77 |
78 | def sign(params: Map[String, String], secret: String, method: FcoinMethod, baseUrl: String) : FcoinParams = {
79 | val ts = System.currentTimeMillis()
80 | val jsParams = Json.toJson(params).toString()
81 | val formParams = params.toSeq.sortBy(_._1).map(c => s"${c._1}=${c._2}").mkString("&")
82 | val template = "%s%s%s%s"
83 | val res = if (method.toString == "GET") {
84 | val getUrl = "%s%s%s".format(baseUrl, "?", formParams)
85 | val formatted = template.format(method, getUrl, ts, "")
86 | (formatted, getUrl)
87 | } else {
88 | val formatted = template.format(method, baseUrl, ts, formParams)
89 | (formatted, baseUrl)
90 | }
91 | val first = Base64.getEncoder.encodeToString(res._1.getBytes)
92 | val second = signHmacSHA1(secret, first)
93 | val signed = Base64.getEncoder.encodeToString(second)
94 | FcoinParams(signed, formParams, ts, res._2, jsParams)
95 | }
96 |
97 | }
98 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/livecoin/LivecoinRequest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.livecoin
2 |
3 | import java.net.URLEncoder
4 | import java.nio.charset.StandardCharsets
5 |
6 | import me.mbcu.integrated.mmm.ops.common.Side.Side
7 | import me.mbcu.integrated.mmm.ops.common.{AbsRestRequest, Credentials, Side}
8 | import me.mbcu.integrated.mmm.ops.livecoin.LivecoinRequest.LivecoinState.LivecoinState
9 | import play.api.libs.json.{Json, Reads, Writes}
10 |
11 | object LivecoinRequest extends AbsRestRequest {
12 |
13 | object LivecoinState extends Enumeration {
14 | type LivecoinState = Value
15 | val ALL, OPEN, CLOSED, CANCELLED, EXECUTED, NOT_CANCELLED, PARTIALLY, PARTIALLY_FILLED, PARTIALLY_FILLED_AND_CANCELLED = Value
16 |
17 | implicit val read = Reads.enumNameReads(LivecoinState)
18 | implicit val write = Writes.enumNameWrites
19 | }
20 |
21 | case class LivecoinParams(sign: String, url: String = "", params: String)
22 |
23 | def getTicker(pair: String): LivecoinParams = LivecoinParams("unused", Livecoin.endpoint.format(s"exchange/ticker?currencyPair=${urlEncode(pair)}"), "")
24 |
25 | def getOwnTrades(credentials: Credentials, pair: String, state: LivecoinState, after: Long): LivecoinParams =
26 | getOwnTrades(credentials.signature, pair, state, after)
27 |
28 | def getOwnTrades(secret: String, pair: String, state: LivecoinState, after: Long): LivecoinParams = {
29 | var params = Map(
30 | "currencyPair" -> pair,
31 | "openClosed" -> state.toString,
32 | "issuedFrom" -> after.toString
33 | )
34 | val sorted = sortToForm(params)
35 | LivecoinParams(sign(sorted, secret), Livecoin.endpoint.format(s"exchange/client_orders?$sorted"), sorted)
36 | }
37 |
38 | def getActiveOrders(credentials: Credentials, pair: String, state: LivecoinState, startRow: Int): LivecoinParams =
39 | getActiveOrders(credentials.signature, pair, state, startRow)
40 |
41 | def getActiveOrders(secret: String, pair: String, state: LivecoinState, startRow: Int): LivecoinParams = {
42 | var params = Map(
43 | "currencyPair" -> pair,
44 | "openClosed" -> state.toString,
45 | "startRow" -> startRow.toString
46 | )
47 | val sorted = sortToForm(params)
48 | LivecoinParams(sign(sorted, secret), Livecoin.endpoint.format(s"exchange/client_orders?$sorted"), sorted)
49 | }
50 |
51 |
52 | def newOrder(credentials: Credentials, pair: String, side: Side, price: BigDecimal, amount: BigDecimal): LivecoinParams = newOrder(credentials.signature, pair, side, price, amount)
53 |
54 | def newOrder(secret: String, pair:String, side: Side, price: BigDecimal, amount: BigDecimal): LivecoinParams = {
55 | val params = Map(
56 | "currencyPair" -> pair,
57 | "price" -> price.bigDecimal.toPlainString,
58 | "quantity" -> amount.bigDecimal.toPlainString
59 | )
60 | val sorted = sortToForm(params)
61 | val path = if (side == Side.buy) s"exchange/buylimit" else s"exchange/selllimit"
62 | LivecoinParams(sign(sorted, secret), Livecoin.endpoint.format(path), sorted)
63 | }
64 |
65 | def cancelOrder(credentials: Credentials, pair: String, orderId: String): LivecoinParams = cancelOrder(credentials.signature, pair, orderId)
66 |
67 | def cancelOrder(secret: String, pair: String, orderId: String): LivecoinParams = {
68 | val params = Map(
69 | "currencyPair" -> pair,
70 | "orderId" -> orderId
71 | )
72 | val sorted = sortToForm(params)
73 | LivecoinParams(sign(sorted, secret), Livecoin.endpoint.format("exchange/cancellimit"), sorted)
74 | }
75 |
76 | def getOrderInfo(credentials: Credentials, orderId: String): LivecoinParams = getOrderInfo(credentials.signature, orderId)
77 |
78 | def getOrderInfo(secret: String, orderId: String): LivecoinParams = {
79 | val params = Map(
80 | "orderId" -> orderId
81 | )
82 | val sorted = sortToForm(params)
83 | LivecoinParams(sign(sorted, secret), Livecoin.endpoint.format(s"/exchange/order?$sorted"), sorted)
84 | }
85 |
86 |
87 |
88 | def sign(sorted:String, secret: String): String = toHex(signHmacSHA256(secret, sorted))
89 |
90 | }
91 |
92 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/Offer.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import me.mbcu.integrated.mmm.ops.common.Side.Side
4 | import me.mbcu.integrated.mmm.ops.common.Status.Status
5 | import play.api.libs.functional.syntax._
6 | import play.api.libs.json._
7 |
8 | object Side extends Enumeration {
9 | type Side = Value
10 | val buy, sell = Value
11 |
12 | implicit val read = Reads.enumNameReads(Side)
13 | implicit val write = Writes.enumNameWrites
14 |
15 | def reverse(a: Side): Side = if (a == Side.buy) sell else buy
16 | def withNameOpt(s: String): Option[Value] = values.find(_.toString == s)
17 | }
18 |
19 | object Status extends Enumeration {
20 | type Status = Value
21 | val active, filled, partiallyFilled, cancelled, expired, debug = Value
22 |
23 | implicit val read = Reads.enumNameReads(Status)
24 | implicit val write = Writes.enumNameWrites
25 | }
26 |
27 | object Offer {
28 | implicit val jsonFormat = Json.format[Offer]
29 | val sortTimeDesc: Seq[Offer] => Seq[Offer] = in => in.sortWith(_.createdAt > _.createdAt)
30 | val sortTimeAsc: Seq[Offer] => Seq[Offer] = in => in.sortWith(_.createdAt < _.createdAt)
31 |
32 | def newOffer(symbol: String, side: Side, price: BigDecimal, quantity: BigDecimal): Offer = Offer("unused", symbol, side, Status.debug, -1L, None, quantity, price, None)
33 |
34 | def splitToBuysSels(in: Seq[Offer]): (Seq[Offer], Seq[Offer]) = {
35 | val t = in.partition(_.side == Side.buy)
36 | (sortBuys(t._1), sortSels(t._2))
37 | }
38 |
39 | def sortBuys(buys: Seq[Offer]): scala.collection.immutable.Seq[Offer] =
40 | collection.immutable.Seq(buys.sortWith(_.price > _.price): _*)
41 |
42 | def sortSels(sels: Seq[Offer]): scala.collection.immutable.Seq[Offer] =
43 | collection.immutable.Seq(sels.sortWith(_.price < _.price): _*)
44 |
45 | def dump(bot: Bot, sortedBuys: Seq[Offer], sortedSels: Seq[Offer]): String = {
46 | val builder = StringBuilder.newBuilder
47 | builder.append(System.getProperty("line.separator"))
48 | builder.append(s"Open Orders ${bot.exchange}: ${bot.pair}")
49 | builder.append(System.getProperty("line.separator"))
50 | builder.append(s"sells : ${sortedSels.size}")
51 | builder.append(System.getProperty("line.separator"))
52 | sortedSels.sortWith(_.price > _.price).foreach(s => {
53 | builder.append(s"id:${s.id} quantity:${s.quantity.bigDecimal.toPlainString} price:${s.price.bigDecimal.toPlainString} filled:${s.cumQuantity.getOrElse(BigDecimal("0").bigDecimal.toPlainString)}")
54 | builder.append(System.getProperty("line.separator"))
55 | })
56 | builder.append(s"buys : ${sortedBuys.size}")
57 | builder.append(System.getProperty("line.separator"))
58 | sortedBuys.foreach(b => {
59 | builder.append(s"id:${b.id} quantity:${b.quantity.bigDecimal.toPlainString} price:${b.price.bigDecimal.toPlainString} filled:${b.cumQuantity.getOrElse(BigDecimal("0").bigDecimal.toPlainString)}")
60 | builder.append(System.getProperty("line.separator"))
61 | })
62 |
63 | builder.toString()
64 | }
65 |
66 | object Implicits {
67 | implicit val writes: Writes[Offer] = (o: Offer) => Json.obj(
68 | "id" -> o.id,
69 | "symbol" -> o.symbol,
70 | "side" -> o.side,
71 | "status" -> o.status,
72 | "createdAt" -> o.createdAt,
73 | "updatedAt" -> o.updatedAt,
74 | "quantity" -> o.quantity,
75 | "price" -> o.price,
76 | "cumQuantity" -> o.cumQuantity
77 | )
78 |
79 | implicit val reads: Reads[Offer] = (
80 | (JsPath \ "id").read[String] and
81 | (JsPath \ "symbol").read[String] and
82 | (JsPath \ "side").read[Side] and
83 | (JsPath \ "status").read[Status] and
84 | (JsPath \ "createdAt").read[Long] and
85 | (JsPath \ "updatedAt").readNullable[Long] and
86 | (JsPath \ "quantity").read[BigDecimal] and
87 | (JsPath \ "price").read[BigDecimal] and
88 | (JsPath \ "cumQuantity").readNullable[BigDecimal]
89 | ) (Offer.apply _)
90 | }
91 |
92 | }
93 |
94 | case class Offer(
95 | id: String,
96 | symbol: String,
97 | side: Side,
98 | status: Status,
99 | createdAt: Long,
100 | updatedAt: Option[Long],
101 | quantity: BigDecimal,
102 | price: BigDecimal,
103 | cumQuantity: Option[BigDecimal]
104 | )
105 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/AbsRestActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import akka.actor.{Actor, ActorRef}
4 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode.ShutdownCode
5 | import me.mbcu.integrated.mmm.ops.Definitions.{ErrorIgnore, ErrorRetryRest, ErrorShutdown}
6 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As.As
7 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
8 |
9 | object AbsRestActor {
10 |
11 | object As extends Enumeration {
12 | type As = Value
13 | val Seed, Trim, Counter, ClearOpenOrders, KillDupes, RoutineCheck, Init = Value
14 | }
15 |
16 | trait SendRest{
17 | def as:As
18 | def bot: Bot
19 | def book: ActorRef
20 | }
21 |
22 | trait GotRest{
23 | def arriveMs : Long
24 | def send : SendRest
25 | }
26 |
27 | case class GetActiveOrders(lastMs: Long, cache: Seq[Offer], page: Int, override val as: As)(implicit val bot:Bot, implicit val book:ActorRef) extends SendRest
28 |
29 | case class GetFilledOrders(lastCounterId: String, override val as: As)(implicit val bot:Bot, implicit val book:ActorRef) extends SendRest
30 |
31 | case class CancelOrder(offer: Offer, as: As)(implicit val bot:Bot, implicit val book:ActorRef) extends SendRest
32 |
33 | case class NewOrder(offer: Offer, as: As)(implicit val bot:Bot, implicit val book:ActorRef) extends SendRest
34 |
35 | case class GetTickerStartPrice(override val as:As)(implicit val bot:Bot, implicit val book:ActorRef) extends SendRest
36 |
37 | case class GetOrderInfo(id: String, as: As)(implicit val bot:Bot, implicit val book:ActorRef) extends SendRest
38 |
39 | case class GetOwnPastTrades(override val as:As)(implicit val bot:Bot, implicit val book:ActorRef) extends SendRest
40 |
41 | case class GotNewOrderId(id: String, as: As, override val arriveMs: Long, override val send: NewOrder) extends GotRest
42 |
43 | case class GotTickerStartPrice(price: Option[BigDecimal], override val arriveMs: Long, override val send: GetTickerStartPrice) extends GotRest
44 |
45 | case class GotLastOwnStartPrice(price: Option[BigDecimal], override val arriveMs: Long, override val send: SendRest) extends GotRest
46 |
47 | case class GotOrderInfo(offer: Offer, override val arriveMs: Long, override val send: GetOrderInfo) extends GotRest
48 |
49 | case class GotOrderCancelled(as: As, override val arriveMs: Long, override val send: CancelOrder) extends GotRest
50 |
51 | case class GotActiveOrders(offers: Seq[Offer], currentPage: Int, nextPage: Boolean, override val arriveMs: Long, override val send: GetActiveOrders) extends GotRest
52 |
53 | case class GotUncounteredOrders(offers: Seq[Offer], latestCounterId: Option[String], isSortedFromOldest: Boolean = true, override val arriveMs: Long, override val send: GetFilledOrders) extends GotRest
54 |
55 | case class GotStartPrice(price: Option[BigDecimal], override val arriveMs: Long, override val send: SendRest) extends GotRest
56 |
57 | case class GotProvisionalOffer(id: String, provisionalTs: Long, offer: Offer) // this comes after NewOrder with id and provisionalTs (used to correctly remove duplicates)
58 |
59 | object StartRestActor
60 |
61 | }
62 |
63 | abstract class AbsRestActor() extends Actor {
64 | var op : Option[ActorRef] = None
65 |
66 | def sendRequest(r: SendRest)
67 |
68 | def setOp(op: Option[ActorRef]) : Unit = this.op = op
69 |
70 | def start()
71 |
72 | def errorRetry(sendRequest: SendRest, code: Int, msg: String, shouldEmail: Boolean = true): Unit = op foreach (_ ! ErrorRetryRest(sendRequest, code, msg, shouldEmail))
73 |
74 | def errorShutdown(shutdownCode: ShutdownCode, code: Int, msg: String): Unit = op foreach (_ ! ErrorShutdown(shutdownCode, code, msg))
75 |
76 | def errorIgnore(code: Int, msg: String, shouldEmail: Boolean = false): Unit = op foreach (_ ! ErrorIgnore(code, msg, shouldEmail))
77 |
78 | override def receive: Receive = {
79 |
80 | case StartRestActor => start()
81 |
82 | case a: SendRest => sendRequest(a)
83 |
84 | }
85 |
86 | def stringifyXWWWForm(params: Map[String, String]): String = params.map(r => s"${r._1}=${r._2}").mkString("&")
87 |
88 | def logResponse(a: SendRest, raw: String): String = {
89 |
90 | def bound(r: String, max: Int): String = if (raw.length > max) s"${r.substring(0, max)}..." + "..." else r
91 |
92 | val response = a match {
93 | case t: GetFilledOrders => bound(raw, max = 500)
94 |
95 | case t: GetActiveOrders => bound(raw, max = 500)
96 |
97 | case _ => raw
98 | }
99 | s"""
100 | |Request: As: $a, ${a.bot.exchange} : ${a.bot.pair}
101 | |Response:
102 | |$response
103 | """.stripMargin
104 | }
105 |
106 | val requestTimeoutSec = 10
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/hitbtc/HitbtcRequest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.hitbtc
2 |
3 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As
4 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As.As
5 | import me.mbcu.integrated.mmm.ops.common.AbsWsParser.SendWs
6 | import me.mbcu.integrated.mmm.ops.common.Side.Side
7 | import me.mbcu.integrated.mmm.ops.common.{AbsWsRequest, Credentials, Offer, Side}
8 | import play.api.libs.json._
9 |
10 | object HitbtcStatus extends Enumeration {
11 | type HitbtcStatus = Value
12 | val `new`, suspended, partiallyFilled, filled, canceled, expired = Value
13 |
14 | implicit val read = Reads.enumNameReads(HitbtcStatus)
15 | implicit val write = Writes.enumNameWrites
16 | }
17 |
18 | object HitbtcRequest extends AbsWsRequest{
19 |
20 | def sha256Hash(text: String) : String = String.format("%064x", new java.math.BigInteger(1, java.security.MessageDigest.getInstance("SHA-256").digest(text.getBytes("UTF-8"))))
21 |
22 | def requestId(pair: String, method: String) : String = s"$method.$pair" // requestId has no length limit
23 |
24 | def orderId(offer: Offer): String = { // has to be at most 32 chars
25 | val prefix = s"${offer.symbol}.${offer.side}."
26 | val random = scala.util.Random.alphanumeric.take(15).mkString
27 | val hash = sha256Hash(random + System.currentTimeMillis()).substring(0, 32 - prefix.length)
28 | s"$prefix$hash"
29 | }
30 |
31 | def pairFromId(id :String) : String = id.split("[.]")(1)
32 |
33 | def clientOrderIdFrom(id: String): String = {
34 | val p = id.split("[.]")
35 | s"${p(1)}.${p(2)}.${p(3)}"
36 | }
37 |
38 | def sideFromId(id : String) : Side = id match {
39 | case a if a contains ".sell." => Side.sell
40 | case _ => Side.buy
41 | }
42 |
43 | override def login(credentials: Credentials): SendWs = {
44 | val method = "login"
45 | val requestId = "login"
46 | SendWs(
47 | requestId,
48 | Json.parse(
49 | s"""
50 | |{
51 | | "params": {
52 | | "algo": "HS256",
53 | | "pKey": "${credentials.pKey}",
54 | | "nonce": "${credentials.nonce}",
55 | | "signature": "${credentials.signature}"
56 | | },
57 | | "method": "$method",
58 | | "id": "$requestId"
59 | |}
60 | """.stripMargin),
61 | As.Init
62 |
63 | )
64 | }
65 |
66 | override val subscribe: SendWs = {
67 | val method = "subscribeReports"
68 | val requestId = "subscribeReports"
69 | SendWs(
70 | requestId,
71 | Json.parse(
72 | s"""
73 | |{
74 | | "method": "$method",
75 | | "id": "$requestId",
76 | | "params": {}
77 | |}
78 | """.stripMargin),
79 | As.Init
80 | )
81 | }
82 |
83 | override def cancelOrder(orderId: String, as: As): SendWs = {
84 | val method = "cancelOrder"
85 | val requestId = HitbtcRequest.requestId(orderId, method)
86 | SendWs(requestId,
87 | Json.parse (
88 | s"""
89 | |{
90 | | "method": "$method",
91 | | "params": {
92 | | "clientOrderId": "$orderId"
93 | | },
94 | | "id": "$requestId"
95 | |}
96 | """.stripMargin
97 | ),
98 | as
99 | )
100 | }
101 |
102 | override def newOrder(offer: Offer, as: As): SendWs = {
103 | val method = "newOrder"
104 | val requestId = HitbtcRequest.requestId(offer.id, method)
105 |
106 | SendWs(requestId,
107 | Json.parse(
108 | s"""
109 | |{
110 | | "method": "$method",
111 | | "params": {
112 | | "clientOrderId": "${offer.id}",
113 | | "symbol": "${offer.symbol}",
114 | | "side": "${offer.side.toString}",
115 | | "price": "${offer.price.bigDecimal.toPlainString}",
116 | | "quantity": "${offer.quantity.bigDecimal.toPlainString}"
117 | | },
118 | | "id": "$requestId"
119 | |}
120 | """.stripMargin),
121 | as
122 | )
123 | }
124 |
125 | val marketSubs: String =
126 | """
127 | |{
128 | | "method": "%s",
129 | | "id": "%s",
130 | | "params": {
131 | | "symbol": "%s"
132 | | }
133 | |}
134 | """.stripMargin
135 |
136 | override def subsTicker(pair: String): SendWs = {
137 | val method = "subscribeTicker"
138 | val requestId = HitbtcRequest.requestId(pair, method)
139 | SendWs(requestId, Json.parse(marketSubs.format(method, requestId, pair)), As.Init)
140 | }
141 |
142 |
143 | override def unsubsTicker(pair: String): SendWs = {
144 | val method = "unsubscribeTicker"
145 | val requestId = HitbtcRequest.requestId(pair, method)
146 | SendWs(requestId, Json.parse(marketSubs.format(method, requestId, pair)), As.Init)
147 | }
148 |
149 | }
150 |
151 |
152 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/Config.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import me.mbcu.integrated.mmm.ops.Definitions.Exchange.Exchange
4 | import me.mbcu.integrated.mmm.ops.Definitions.Strategies.Strategies
5 | import play.api.libs.functional.syntax._
6 | import play.api.libs.json._
7 |
8 |
9 | case class Credentials(pKey: String, nonce: String, signature: String)
10 |
11 | object Credentials {
12 | implicit val jsonFormat: OFormat[Credentials] = Json.format[Credentials]
13 | }
14 |
15 | case class Env(emails: Option[Seq[String]], sesKey: Option[String], sesSecret: Option[String], logSeconds: Int)
16 |
17 | object Env {
18 | implicit val jsonFormat: OFormat[Env] = Json.format[Env]
19 |
20 | object Implicits {
21 | implicit val envWrites: Writes[Env] {
22 | def writes(env: Env): JsValue
23 | } = new Writes[Env] {
24 | def writes(env: Env): JsValue = Json.obj(
25 | "emails" -> env.emails,
26 | "sesKey" -> env.sesKey,
27 | "sesSecret" -> env.sesSecret,
28 | "logSeconds" -> env.logSeconds
29 | )
30 | }
31 |
32 | implicit val envReads: Reads[Env] = (
33 | (JsPath \ "emails").readNullable[Seq[String]] and
34 | (JsPath \ "sesKey").readNullable[String] and
35 | (JsPath \ "sesSecret").readNullable[String] and
36 | (JsPath \ "logSeconds").read[Int]
37 | ) (Env.apply _)
38 | }
39 |
40 | }
41 |
42 | object StartMethods extends Enumeration {
43 | type StartMethods = Value
44 | val lastTicker, lastOwn, cont = Value
45 | }
46 |
47 | case class Bot(
48 | exchange: Exchange,
49 | credentials: Credentials,
50 | pair: String,
51 | seed : String,
52 | gridSpace: BigDecimal,
53 | buyGridLevels: Int,
54 | sellGridLevels: Int,
55 | buyOrderQuantity: BigDecimal,
56 | sellOrderQuantity: BigDecimal,
57 | quantityPower: Int,
58 | maxPrice: Option[BigDecimal],
59 | minPrice: Option[BigDecimal],
60 | counterScale: Int,
61 | baseScale: Int,
62 | isStrictLevels: Boolean,
63 | isNoQtyCutoff: Boolean,
64 | strategy: Strategies
65 | )
66 |
67 | object Bot {
68 | val millisFileFormat = "%s_%s"
69 |
70 | def filePath(bot: Bot):String = bot.pair.replaceAll("/", "_").toLowerCase()
71 |
72 | implicit val jsonFormat: OFormat[Bot] = Json.format[Bot]
73 |
74 | object Implicits {
75 | implicit val botWrites: Writes[Bot] {
76 | def writes(bot: Bot): JsValue
77 | } = new Writes[Bot] {
78 | def writes(bot: Bot): JsValue = Json.obj(
79 | "exchange" -> bot.exchange,
80 | "credentials" -> bot.credentials,
81 | "pair" -> bot.pair,
82 | "seed" -> bot.seed,
83 | "gridSpace" -> bot.gridSpace,
84 | "buyGridLevels" -> bot.buyGridLevels,
85 | "sellGridLevels" -> bot.sellGridLevels,
86 | "buyOrderQuantity" -> bot.buyOrderQuantity,
87 | "sellOrderQuantity" -> bot.sellOrderQuantity,
88 | "quantityPower" -> bot.quantityPower,
89 | "maxPrice" -> bot.maxPrice,
90 | "minPrice" -> bot.minPrice,
91 | "counterScale" -> bot.counterScale,
92 | "baseScale" -> bot.baseScale,
93 | "isStrictLevels" -> bot.isStrictLevels,
94 | "isNoQtyCutoff" -> bot.isNoQtyCutoff,
95 | "strategy" -> bot.strategy
96 | )
97 | }
98 |
99 | implicit val botReads: Reads[Bot] = (
100 | (JsPath \ "ops").read[Exchange] and
101 | (JsPath \ "credentials").read[Credentials] and
102 | (JsPath \ "pair").read[String] and
103 | (JsPath \ "seed").read[String] and
104 | (JsPath \ "gridSpace").read[BigDecimal] and
105 | (JsPath \ "buyGridLevels").read[Int] and
106 | (JsPath \ "sellGridLevels").read[Int] and
107 | (JsPath \ "buyOrderQuantity").read[BigDecimal] and
108 | (JsPath \ "sellOrderQuantity").read[BigDecimal] and
109 | (JsPath \ "quantityPower").read[Int] and
110 | (JsPath \ "maxPrice").readNullable[BigDecimal] and
111 | (JsPath \ "minPrice").readNullable[BigDecimal] and
112 | (JsPath \ "counterScale").read[Int] and
113 | (JsPath \ "baseScale").read[Int] and
114 | (JsPath \ "isStrictLevels").read[Boolean] and
115 | (JsPath \ "isNoQtyCutoff").read[Boolean] and
116 | (JsPath \ "strategy").read[Strategies]
117 | ) (Bot.apply _)
118 | }
119 |
120 | }
121 |
122 | case class Config(env: Env, bots: List[Bot])
123 |
124 | object Config {
125 | implicit val jsonFormat: OFormat[Config] = Json.format[Config]
126 |
127 | def groupBots(bots: Seq[Bot]) : Map[Exchange , Seq[Bot]] = {
128 | bots.map(p => (p.exchange, p.pair) -> p)
129 | .groupBy(_._1)
130 | .mapValues(_.map(_._2).head)
131 | .groupBy(_._1._1)
132 | .mapValues(_.values.toList)
133 | }
134 |
135 | }
136 |
137 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/ops/fcoin/FcoinActorTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.fcoin
2 |
3 | import me.mbcu.integrated.mmm.ops.common.Status
4 | import org.scalatest.FunSuite
5 | import play.api.libs.json.{JsValue, Json}
6 |
7 | class FcoinActorTest extends FunSuite{
8 |
9 | test("parse response to offer (filled") {
10 | val a =
11 | """
12 | |{
13 | | "status": 0,
14 | | "data": [
15 | | {
16 | | "id": "h_tbQC5CJIbBeCgtIq3f3KUP_3ODRt6mlw4W9m7MwVg=",
17 | | "symbol": "xrpusdt",
18 | | "amount": "3.040000000000000000",
19 | | "price": "0.440000000000000000",
20 | | "created_at": 1532066585373,
21 | | "type": "limit",
22 | | "side": "buy",
23 | | "filled_amount": "3.040000000000000000",
24 | | "executed_value": "1.337600000000000000",
25 | | "fill_fees": "0.003040000000000000",
26 | | "source": "api",
27 | | "state": "filled"
28 | | },
29 | | {
30 | | "id": "IUDiFCdkTPatOu0Lpu5TD3fRVMD_nYokzgrY2ggiRZY=",
31 | | "symbol": "xrpusdt",
32 | | "amount": "3.020000000000000000",
33 | | "price": "0.450000000000000000",
34 | | "created_at": 1532066584567,
35 | | "type": "limit",
36 | | "side": "buy",
37 | | "filled_amount": "3.020000000000000000",
38 | | "executed_value": "1.359000000000000000",
39 | | "fill_fees": "0.003020000000000000",
40 | | "source": "api",
41 | | "state": "filled"
42 | | },
43 | | {
44 | | "id": "UfqKwFqGPaIQax6xMml0G-oUdY19AobGxmJNKXfk4Z8=",
45 | | "symbol": "xrpusdt",
46 | | "amount": "14.920000000000000000",
47 | | "price": "0.478400000000000000",
48 | | "created_at": 1531844657648,
49 | | "type": "limit",
50 | | "side": "sell",
51 | | "filled_amount": "14.920000000000000000",
52 | | "executed_value": "7.137728000000000000",
53 | | "fill_fees": "0.007137728000000000",
54 | | "source": "api",
55 | | "state": "filled"
56 | | },
57 | | {
58 | | "id": "NO5qUaGqzppONSEmlor3iYiUPhTYNgko3zsLCVdjFAk=",
59 | | "symbol": "xrpusdt",
60 | | "amount": "15.080000000000000000",
61 | | "price": "0.473600000000000000",
62 | | "created_at": 1531844368429,
63 | | "type": "limit",
64 | | "side": "buy",
65 | | "filled_amount": "15.080000000000000000",
66 | | "executed_value": "7.141888000000000000",
67 | | "fill_fees": "0.015080000000000000",
68 | | "source": "api",
69 | | "state": "filled"
70 | | },
71 | | {
72 | | "id": "RcHxUSl3u3jCdBcnH196hzUgYpEc2xV5i-_XYXxgfCk=",
73 | | "symbol": "xrpusdt",
74 | | "amount": "300.000000000000000000",
75 | | "price": "0.463200000000000000",
76 | | "created_at": 1531751435077,
77 | | "type": "limit",
78 | | "side": "sell",
79 | | "filled_amount": "300.000000000000000000",
80 | | "executed_value": "138.960000000000000000",
81 | | "fill_fees": "0.138960000000000000",
82 | | "source": "web",
83 | | "state": "filled"
84 | | }
85 | | ]
86 | |}
87 | """.stripMargin
88 |
89 |
90 | val data = Json.parse(a) \ "data"
91 | val res1 = FcoinActor.parseFilled(data, "NO5qUaGqzppONSEmlor3iYiUPhTYNgko3zsLCVdjFAk=")
92 | assert(res1._1.size === 3)
93 | assert(res1._2 === Some("h_tbQC5CJIbBeCgtIq3f3KUP_3ODRt6mlw4W9m7MwVg="))
94 |
95 | val res2 = FcoinActor.parseFilled(data, "id not exist") // all offers are uncountered
96 | assert(res2._1.size === 5)
97 | }
98 |
99 | test( "parse response to offer (submitted)") {
100 | val a =
101 | """
102 | |{
103 | | "status": 0,
104 | | "data": {
105 | | "id": "YnkBPuViqnLSJMh5p2t3wAqJ042mZOt93A2TVUCSqIs=",
106 | | "symbol": "ethusdt",
107 | | "amount": "0.001900000000000000",
108 | | "price": "477.570000000000000000",
109 | | "created_at": 1531803578106,
110 | | "type": "limit",
111 | | "side": "sell",
112 | | "filled_amount": "0.000000000000000000",
113 | | "executed_value": "0.000000000000000000",
114 | | "fill_fees": "0.000000000000000000",
115 | | "source": "api",
116 | | "state": "submitted"
117 | | }
118 | |}
119 | """.stripMargin
120 |
121 | val js = Json.parse(a)
122 | val data = (js \ "data").as[JsValue]
123 | val offer = FcoinActor.toOffer(data)
124 | assert(offer.status === Status.active)
125 |
126 | }
127 |
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/OpDdexActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import java.math.BigInteger
4 |
5 | import akka.actor.{Actor, ActorRef, Cancellable}
6 | import akka.dispatch.ExecutionContexts.global
7 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.{NewOrder, SendRest}
8 | import me.mbcu.integrated.mmm.ops.common.{AbsExchange, AbsOpActor, Bot}
9 | import me.mbcu.scala.MyLogging
10 | import org.bouncycastle.util.encoders.Hex
11 | import org.web3j.crypto.{Credentials, ECKeyPair, Sign}
12 |
13 | import scala.concurrent.ExecutionContextExecutor
14 |
15 | class OpDdexActor(exchangeDef: AbsExchange, bots: Seq[Bot], fileActor: ActorRef) extends AbsOpActor(exchangeDef, bots, fileActor) with MyLogging{
16 | private implicit val ec: ExecutionContextExecutor = global
17 | private var base: Option[ActorRef] = None
18 | private var rest: Option[ActorRef] = None
19 | private var dqCancellable: Option[Cancellable] = None
20 | private val q = new scala.collection.mutable.Queue[SendRest]
21 | private val nos = scala.collection.mutable.Set[NewOrder]()
22 |
23 | override def receive: Receive = {
24 |
25 | case "start" =>
26 | base = Some(sender())
27 | self ! "test"
28 |
29 | //7196933fe363871920c59be78aa5c478bf6e6271532db5d0ce3b090518f91f03
30 | case "test" =>
31 | // val privateKey = ""
32 | // import org.web3j.crypto.Hash
33 | // import org.web3j.utils.Numeric
34 | // val test = "HYDRO-AUTHENTICATION@" + "aa"
35 | // val hash = Hash.sha3(Numeric.toHexStringNoPrefix(test.getBytes))
36 | // val message = "\\u0019Ethereum Signed Message:\n" + hash.length + hash
37 | // val message2 = "\\\\x19Ethereum Signed Message:\n" + hash.length + hash
38 | //
39 | // println(hash)
40 | //
41 | // println(s"Message: $message")
42 | //
43 | // val cred = Credentials.create(privateKey)
44 | // println(s"Address:${cred.getAddress}")
45 | // val signatureData = Sign.signMessage(message.getBytes(), cred.getEcKeyPair)
46 | // import org.web3j.utils.Numeric
47 | // val r = Numeric.toHexString(signatureData.getR)
48 | // val s = Numeric.toHexString(signatureData.getS)
49 | // val v = "%02X".format(signatureData.getV)
50 | //
51 | // val signed = s"${Numeric.toHexString(signatureData.getR)}${Numeric.toHexString(signatureData.getS).substring(2)}${"%02X".format(signatureData.getV).toLowerCase()}"
52 | // println(signed)
53 |
54 | import org.web3j.crypto.Credentials
55 | import org.web3j.crypto.Hash
56 | import org.web3j.crypto.Sign
57 | import org.web3j.utils.Numeric
58 | val privateKey1 = "fba4137335dc20dc23ad3dcd9f4ad728370b09131a6a14abf6c839748700780d"
59 | val credentials = Credentials.create(privateKey1)
60 | System.out.println("Address: " + credentials.getAddress)
61 |
62 | // val hash = Hash.sha3(Numeric.toHexStringNoPrefix("TEST".getBytes))
63 | // System.out.println("Hash" + hash)
64 | //
65 | // val message = " val message = "\u0019Ethereum Signed Message:\n32" + hash.length + hash
66 | // println(s"Message: $message")" + hash.length + hash
67 | // println(s"Message: $message")
68 | //
69 | // val data = message.getBytes
70 | //
71 | // val signature = Sign.signMessage(data, credentials.getEcKeyPair)
72 | //
73 | // System.out.println("R: " + Numeric.toHexString(signature.getR))
74 | // System.out.println("S: " + Numeric.toHexString(signature.getS))
75 | // System.out.println("V: " + Integer.toString(signature.getV))
76 | // val signed = s"${Numeric.toHexString(signature.getR)}${Numeric.toHexString(signature.getS).substring(2)}${"%02X".format(signature.getV).toLowerCase()}"
77 | // println(signed)
78 | //
79 | //
80 | // val pub = Sign.signedMessageToKey(message.getBytes, signature)
81 | // println(s"Pub key: $pub")
82 | // println(credentials.getEcKeyPair.getPublicKey)
83 |
84 | // sign(keccak256("\x19Ethereum Signed Message:\n" + len(message) + message)))
85 | val test = "TEST"
86 | val message = "\u0019Ethereum Signed Message:\n32" + test.length + test
87 | val hash = Hash.sha3(message)
88 | val signature = Sign.signMessage(hash.getBytes, credentials.getEcKeyPair)
89 | val r = Numeric.toHexString(signature.getR)
90 | val s = Numeric.toHexString(signature.getS)
91 | val v = "%02X".format(signature.getV)
92 | val signed = s"${Numeric.toHexString(signature.getR)}${Numeric.toHexString(signature.getS).substring(2)}${"%02X".format(signature.getV).toLowerCase()}"
93 | println(signed)
94 | //0x5d46da45a1af583990eeb142199e593480d0b1e5f88903e287ff67209140dcc646384d275b500ded85ca16dea5ff369532a27fc92721189cef68d652ab18580a1c
95 |
96 | // val sData = new org.web3j.crypto.Sign.SignatureData(
97 | //
98 | // new BigInteger(v, 16).toByteArray,
99 | // Hex.decode(r),
100 | // Hex.decode(s))
101 | // val extractedPublicKey = org.web3j.crypto.Sign.signedMessageToKey(signed.getBytes, sData).toString(16)
102 | // println(extractedPublicKey)
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # MMM: Integrated Crypto Market Making Bot
2 |
3 | An automated grid/ping-pong market-making bot supporting various exchanges.
4 |
5 | In general, market making works by placing orders on both buy and sell sides. When an order is filled, the bot counters it by placing a new order on the opposing side.
6 | The bot also maintains the number of order levels by seeding (placing new orders) or trimming (cancelling extra orders) as defined in config.
7 | In this bot only filled orders are countered. It doesn't counter partially-filled orders.
8 |
9 | As having multiple orders on the same price and quantity is undesirable the bot will also remove such duplicates starting from the newest one.
10 |
11 | One bot (defined in config's `bots`) should operate on one symbol (pair) only.
12 |
13 | Supported exchanges:
14 | - HitBtc
15 | - Okex
16 | - Yobit
17 | - Fcoin
18 | - Livecoin
19 | - Btcalpha
20 |
21 | [Exchanges](./EXCHANGES.md)
22 |
23 | [Operation](./OPERATION.md)
24 |
25 | [Version notes](./VERSIONS.md)
26 |
27 | ## Running
28 | Use [config-template](./config-template) as config reference.
29 |
30 | One bot in [bots] will market-make on one pair.
31 |
32 | Clone the repo and run this to build and package it into a fat jar.
33 |
34 | ```
35 | sbt assembly
36 | ```
37 |
38 | (result jar is in /target)
39 |
40 | To run
41 |
42 | ```
43 | java -jar program.jar
44 | ```
45 |
46 | ## Config
47 |
48 | #### exchange
49 |
50 | Type: `string`
51 |
52 | Name of the exchange. See [Exchanges](./EXCHANGES.md) for supported exchanges.
53 |
54 | #### credentials
55 |
56 | **pKey**
57 |
58 | Type: `string`
59 |
60 | Api Key
61 |
62 | **nonce**
63 |
64 | Type: `string`
65 |
66 | Nonce (used for Hitbtc). Set empty string if it's not needed.
67 |
68 | **signature**
69 |
70 | Type: `string`
71 |
72 | Secret key or signed nonce (Hitbtc).
73 |
74 | #### pair
75 |
76 | Type: `string`
77 |
78 | Pair or symbol in format accepted by the exchange.
79 |
80 | #### seed
81 |
82 | Type: `string`
83 | Enum: `lastTicker`, `cont`, or any valid number as starting price.
84 |
85 | Seed determines how the bot starts. Refer to Supported Exchanges for allowed methods.
86 |
87 | **lastTicker**, **any valid number**: cancels all active orders, seeds new orders from config. Any uncountered orders during the time the bot is not running will not be countered.
88 | This method is used when starting fresh or when config was changed.
89 |
90 | **lastOwn** : uses last traded price as new seed price. As the amount information is lost, amount is taken from config.
91 |
92 | **cont**: continues from last session. Any uncountered orders during the time the bot is not running will be countered.
93 | To correctly run this method, the account **needs to have some active orders** and the bot should not be inactive longer than exchange's trade history retention. Some exchanges keep trade history for 2 days so if the bot is restarted later, it may not be able to retrieve all filled orders.
94 |
95 | #### gridSpace
96 |
97 | Type: `string`
98 |
99 | Price level between orders, behavior determined by strategy.
100 |
101 | #### buyGridLevels, sellGridLevels
102 |
103 | Type: `int`
104 |
105 | Number of orders to be maintained in orderbook.
106 |
107 | #### buyOrderQuantity, sellOrderQuantity
108 |
109 | Type: `string`
110 |
111 | Amount of order.
112 |
113 | #### quantityPower
114 |
115 | Type: `int`
116 |
117 | Only used in **ppt**. Rate of quantity appreciation / depreciation which is denoted as (sqrt(gridSpace))^quantityPower.
118 |
119 | #### counterScale, baseScale
120 |
121 | Type: `int`
122 |
123 | Scale of minimum amount of counter / base currency accepted by the exchange.
124 |
125 | Examples:
126 | - If minimum quantity 1 -> scale = 0
127 | - If minimum quantity 0.001 -> scale = 3
128 | - If minimum quantity 1000 -> scale = -3
129 |
130 | #### isHardReset (deprecated)
131 |
132 | Type: `boolean`
133 |
134 | If true then when the bot starts it will remove all orders and seed new orders with recalculated middle price. This method will clear any holes (missing levels) but lose all ping/pong information from the old orders.
135 |
136 | If false then the bot will only fill the hole closest to market middle price. This will preserve the ping/pong info of each order but not fill all possible holes.
137 |
138 |
139 | #### isStrictLevels
140 |
141 | Type: `boolean`
142 |
143 | If true then number of orders is kept according to buyGridLevels pr sellGridLevels by removing tail orders. WARNING : this may cause holes in orderbook.
144 |
145 | #### isNoQtyCutoff
146 |
147 | Type: `boolean`
148 |
149 | If true then order's quantity will never become zero. Instead it will be replaced with the currency's minimum amount.
150 |
151 |
152 | #### maxPrice, minPrice (Optional)
153 |
154 | Type: `string`
155 |
156 | Maximum and minimum price the bot will operate on
157 |
158 | #### strategy
159 |
160 | Type: `string`
161 |
162 | Strategy to be used. Refer to strategy section for valid names.
163 |
164 |
165 | ## Strategies
166 |
167 | ### Proportional
168 |
169 | code: `ppt`
170 |
171 | Both base quantity and unit price are spaced by percentage point of the previous offerCreate level.
172 |
173 | For sell direction p1 = (1 + gridSpace / 100) * p0 and q1 = q0 / ((1 + gridSpace / 100)^0.5)^quantityPower
174 |
175 | For buy direction p1 = p0 / (1 + gridSpace / 100) and q1 = q0 * ((1 + gridSpace / 100)^0.5)^quantityPower
176 |
177 | Pay attention minimum quantity. Ideally minimum quantity should be smaller than (gridSpace / 100 * quantity)
178 |
179 | ### Full-fixed
180 |
181 | Code: `fullfixed`
182 |
183 | The new unit price is spaced rigidly by gridSpace, e.g. if a buy order with price X is consumed then a new sell order selling the same quantity with price X + gridSpace will be created.
184 |
185 | If a sell order with price Y is consumed then a buy order with the same quantity and price Y - gridSpace will be created.
186 |
187 |
188 |
189 |
--------------------------------------------------------------------------------
/VERSIONS.md:
--------------------------------------------------------------------------------
1 | 1.1.1
2 | - added error log on FCoin undetermined response
3 |
4 | 1.1.0
5 | - fixed OrderRestActor, initseed happened to fast before ticker price arrived
6 |
7 | 1.0.9
8 | - replaced local MyLogging
9 |
10 | 1.0.8
11 | - Added timeout mechanism in websocket
12 | - undo onDisconnect method
13 |
14 | 1.0.7
15 | - WsActor now doesn't wait for onDisconnect to reconnect
16 |
17 | 1.0.6
18 | - added Livecoin error condition
19 | - added rest timeout handler (Livecoin suffers from super long timeout)
20 |
21 | 1.0.5
22 | - fixed orderGI: counter the order cache which has the original order info not the API response
23 |
24 | 1.0.4
25 | - fixed orderGI: order info should update offer not replace it
26 |
27 | 1.0.2
28 | - minor fix Livecoin filled amount
29 |
30 | 1.0.1
31 | - escaped secret for Btcalpha
32 |
33 | 1.0.0
34 | - added Hitbtc
35 | - automatic websocket reconnect and cache drain
36 |
37 | 0.8.3
38 | - fixed Btcalpha pair name and request
39 |
40 | 0.8.2
41 | - Livecoin added errorRetry "Cannot get a connection"
42 | - first version of untested integrated HitBtc
43 |
44 | 0.8.1
45 | - changed yobit endpoint to yobit.io
46 | - changed yobit rate to 1500ms
47 |
48 | 0.8.0
49 | - added bitcoinalpha
50 |
51 | 0.7.2
52 | - refactored Livecoin getOrder
53 | - removed Livecoin pagination (doesn't work)
54 |
55 | 0.7.1
56 | - added Livecoin status
57 |
58 | 0.7.0
59 | - added Livecoin
60 | - Bitcalpha in progress
61 |
62 | 0.6.1
63 | - fixed Yobit status 3 from filled to partially-filled
64 |
65 | 0.6.0
66 | - brought back GI (GetOrderInfo) method: checking order status by calling GetOrderInfo
67 | - avoided waiting for GetOrderInfo by using provisional offer: offer whose values come from request and id from newOrder
68 | - fixed find duplicates: returns more than one if more than one dupe
69 | - seed starts only when theres' no NewOrder and CancelOrder
70 | - ProvisionalOffer is set with serverTs to allow duplicates removal
71 | - trims and remove duplicates requests are now merged
72 |
73 | 0.5.4
74 | - added KillDupes enum
75 | - more readme
76 |
77 | 0.5.3
78 | - made getActive arrive before getFilled: seed happens before counter
79 | - added delete duplicates
80 |
81 | 0.5.2
82 | - fixed bug in grow
83 |
84 | 0.5.1
85 | - fixed bug on unfunded newOrder : needs to clear the set in op for each successful request
86 | - fixed wiring
87 |
88 | 0.5.0
89 | - revamped
90 | - input last order sent/ts
91 | - active orders cannot queue when new orders are in q
92 | - active orders need to be partitioned and sorted
93 | - active orders : seed and trim, filled orders : counter. no dependency relation between them
94 | - counter mechanism: store last countered id on bot cache, read it before getFilled is called, and use it as lower bound of all uncountered orders
95 | - different exchange has different indicator
96 |
97 | 0.4.3
98 | - fixed small bug in dump
99 |
100 | 0.4.2
101 | - counter needs to cancel all seeds
102 |
103 | 0.4.1
104 | - fixed bug in reseed
105 |
106 | 0.4.0
107 | - Introduced a new variable in Exchange `seedIfEmpty` to seed only if the side has no open orders.
108 | - because during GetOrderInfo sequence multiple orders may get filled, seed while half-empty cannot work well
109 | - Yobit status 3 and _ is now cancelled
110 |
111 | 0.3.7
112 | - solution 0.3.6 was not perfect. After receiving a new order id, this id must be queried again with GetInfo. During this duration, there was a hole where `CheckInQueue` cannot find any pending orders. This caused multiple to be pushed.
113 | - OpRest now holds a set to hold pending NewOrders or successful new orders that are still waiting for their info. `CheckInQueue` will also check any pending orders here.
114 |
115 | 0.3.6
116 | - doubles were caused by multiple GetOrderInfo (with same id) being queued too many times (order check just keeps pushing new requests every interval). If multiple GetOrderInfo check a same filled order then the bot will create more than one counter.
117 | - fixed it by removing inbound GetOrderInfo requests which have the same order Id as the ones in queue.
118 | - Grow is put in the same place as counter. Removed grow from balancer scheduler
119 | - GotOrderCancel is not a place to put grow. This is used to clear orderbook the first time.
120 |
121 | 0.3.5
122 | - doubles were caused by GotOrderInfo's starting refresh before orderbook init was complete (startPrice not yet available)
123 | - fixed by not launching refresh if the cancellable is None.
124 | - changed isInQueue
125 |
126 | 0.3.4
127 | - fixed isInQueue
128 | - enumerized As
129 | - all request should have As. Refresh orders will first check As in queue along with GetOrderInfo which sets off after NewOrder
130 |
131 | 0.3.3
132 | - do check on request sequence whenever "refresh order" is called. This should prevent double orders, possibly caused by balancer ("reseed") called when the queue still has reseed orders.
133 | - if not ok, check the Strategy for reseed
134 | NOTE: even more secure way is not to pop the request from the queue but remove it once the request has been executed and returned response.
135 |
136 | 0.2.3
137 | - make errorRetry's send email optional
138 |
139 | 0.2.2
140 | - added exchange and pair in log orderbook
141 |
142 | 0.2.1
143 | - cleaned up codes
144 | - add docs and template
145 |
146 | 0.2.0
147 | - grouped all bot of same exchanges with one sender sequence
148 | - created ConfigTest for groupBots
149 | - refactored OpActor, OrderbookActor. Orderbook now sends orders to Op's queue but gets the results directly from RestActor
150 |
151 | 0.1.1
152 | - added Fcoin
153 |
154 | 0.1.0
155 | - Refactor OpRestActor, now uses queue for unified request delay
156 | - Strategy requires ceil and floor on price with its scaling factor
157 |
158 | 0.0.6
159 | - Yobit new offer needed to avoid scientific notation
160 |
161 | 0.0.5
162 | - specified Application in build.sbt
163 |
164 | 0.0.4
165 | - added trim
166 | - refactored orderbook
167 |
168 | 0.0.3
169 | - removed isHardReset
170 | - changes startPrice to seed
171 |
172 | 0.0.2
173 | - Yobit
174 |
175 | 0.0.1
176 | - Refactoring from Okex MMM
177 | - Supports okexRest
178 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/ops/common/AbsOrderTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import org.scalatest.FunSuite
4 | import org.scalatest.mockito.MockitoSugar
5 |
6 | class AbsOrderTest extends FunSuite with MockitoSugar{
7 |
8 | test ("get duplicates from list with duplicates") {
9 |
10 | val symbol = "noah_rur"
11 | val active = Status.active
12 | val side = Side.sell // symbol, side, BigDecimal("0.07742766"),
13 | val offers = Seq(
14 | Offer("2073861098400280", symbol, side, active, 1L, None, BigDecimal(1972), BigDecimal(".07742766"), None),
15 | Offer("2073861098400222", symbol, side, active, 2L, None, BigDecimal(1986), BigDecimal(".07688943"), None),
16 | Offer("2073861098403259", symbol, side, active, 3L, None, BigDecimal(2000), BigDecimal(".07635494"), None),
17 | Offer("2073861098402160", symbol, side, active, 4L, None, BigDecimal(2000), BigDecimal(".07635494"), None),
18 | Offer("2073861098403224", symbol, side, active, 9L, None, BigDecimal(2014), BigDecimal(".07582417"), None),
19 | Offer("2073861098403220", symbol, side, active, 5L, None, BigDecimal(2014), BigDecimal(".07582417"), None),
20 | Offer("2073861098402116", symbol, side, active, 6L, None, BigDecimal(2014), BigDecimal(".07582417"), None),
21 | Offer("2073861098403227", symbol, side, active, 7L, None, BigDecimal(2028), BigDecimal(".07582400"), None),
22 | Offer("2073861098402119", symbol, side, active, 8L, None, BigDecimal(2014), BigDecimal(".07582400"), None)
23 | )
24 | scala.util.Random.shuffle(offers)
25 | val res = AbsOrder.getDuplicates(offers)
26 |
27 | assert(res.size === 3)
28 | assert(res.head.price === BigDecimal(".07582417"))
29 | assert(res(1).price === BigDecimal(".07582417"))
30 | assert(res.last.price === BigDecimal(".07635494"))
31 |
32 | val noDup = Seq(
33 | Offer("2073861098400280", symbol, side, active, 1L, None, BigDecimal(1972), BigDecimal(".07742766"), None),
34 | Offer("2073861098400222", symbol, side, active, 2L, None, BigDecimal(1986), BigDecimal(".07688943"), None),
35 | Offer("2073861098402160", symbol, side, active, 4L, None, BigDecimal(2000), BigDecimal(".07635494"), None),
36 | Offer("2073861098403220", symbol, side, active, 5L, None, BigDecimal(2014), BigDecimal(".07582417"), None),
37 | Offer("2073861098403227", symbol, side, active, 7L, None, BigDecimal(2028), BigDecimal(".07582400"), None),
38 | Offer("2073861098402119", symbol, side, active, 8L, None, BigDecimal(2014), BigDecimal(".07582400"), None)
39 | )
40 | scala.util.Random.shuffle(noDup)
41 | val res2 = AbsOrder.getDuplicates(noDup)
42 | assert(res2.size === 0)
43 |
44 | val one = Seq(
45 | // id:1073861122230849 quantity:2000 price:0.06267738 filled:0
46 | Offer("2073861098400280", symbol, side, active, 1L, None, BigDecimal(2000), BigDecimal(".06267738"), None)
47 |
48 | )
49 | val res3 = AbsOrder.getDuplicates(one)
50 | assert(res3.size === 0)
51 |
52 | val singles = Seq(
53 | Offer("2073861098400280", symbol, side, active, 1L, None, BigDecimal(1972), BigDecimal(".07742766"), None),
54 | Offer("2073861098400222", symbol, side, active, 2L, None, BigDecimal(1986), BigDecimal(".07688943"), None)
55 | )
56 | val res4 = AbsOrder.getDuplicates(singles)
57 | assert(res4.size === 0)
58 | }
59 |
60 | test("merge trims and cancel dupes") {
61 | val symbol = "noah_rur"
62 | val active = Status.active
63 | val side = Side.sell // symbol, side, BigDecimal("0.07742766"),
64 |
65 | val trims1 = Seq(
66 | Offer("2073861098400280", symbol, side, active, 1L, None, BigDecimal(1972), BigDecimal(".07742766"), None),
67 | Offer("2073861098400222", symbol, side, active, 2L, None, BigDecimal(1986), BigDecimal(".07688943"), None),
68 | Offer("2073861098402160", symbol, side, active, 4L, None, BigDecimal(2000), BigDecimal(".07635494"), None),
69 | Offer("2073861098403220", symbol, side, active, 5L, None, BigDecimal(2014), BigDecimal(".07582417"), None)
70 | )
71 |
72 | val dupes1 = Seq(
73 | Offer("2073861098400280", symbol, side, active, 1L, None, BigDecimal(1972), BigDecimal(".07742766"), None),
74 | Offer("2073861098400222", symbol, side, active, 2L, None, BigDecimal(1986), BigDecimal(".07688943"), None),
75 | Offer("2073861098403227", symbol, side, active, 7L, None, BigDecimal(2028), BigDecimal(".07582400"), None),
76 | Offer("2073861098403228", symbol, side, active, 7L, None, BigDecimal(2028), BigDecimal(".07582400"), None),
77 | Offer("2073861098403229", symbol, side, active, 7L, None, BigDecimal(2028), BigDecimal(".07582400"), None),
78 | Offer("2073861098403230", symbol, side, active, 7L, None, BigDecimal(2028), BigDecimal(".07582400"), None)
79 | )
80 |
81 | val res1 = AbsOrder.margeTrimAndDupes(trims1, dupes1)
82 | assert(res1._1.size === 2)
83 | assert(res1._2.size === 6)
84 |
85 |
86 | val trims2 = Seq(
87 | Offer("2073861098400280", symbol, side, active, 1L, None, BigDecimal(1972), BigDecimal(".07742766"), None),
88 | Offer("2073861098400222", symbol, side, active, 2L, None, BigDecimal(1986), BigDecimal(".07688943"), None),
89 | Offer("2073861098402160", symbol, side, active, 4L, None, BigDecimal(2000), BigDecimal(".07635494"), None),
90 | Offer("2073861098403220", symbol, side, active, 5L, None, BigDecimal(2014), BigDecimal(".07582417"), None)
91 | )
92 |
93 | val dupes2 = Seq()
94 |
95 | val res2 = AbsOrder.margeTrimAndDupes(trims2, dupes2)
96 | assert(res2._1.size === 4)
97 | assert(res2._2.size === 0)
98 |
99 | val trims3 = Seq()
100 |
101 | val dupes3 = Seq(
102 | Offer("2073861098400280", symbol, side, active, 1L, None, BigDecimal(1972), BigDecimal(".07742766"), None),
103 | Offer("2073861098400222", symbol, side, active, 2L, None, BigDecimal(1986), BigDecimal(".07688943"), None),
104 | Offer("2073861098402160", symbol, side, active, 4L, None, BigDecimal(2000), BigDecimal(".07635494"), None),
105 | Offer("2073861098403220", symbol, side, active, 5L, None, BigDecimal(2014), BigDecimal(".07582417"), None)
106 | )
107 |
108 | val res3 = AbsOrder.margeTrimAndDupes(trims3, dupes3)
109 | assert(res3._1.size === 0)
110 | assert(res3._2.size === 4)
111 | }
112 |
113 | }
114 |
--------------------------------------------------------------------------------
/src/test/scala/me/mbcu/integrated/mmm/ops/livecoin/LivecoinActorTest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.livecoin
2 |
3 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode
4 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
5 | import me.mbcu.integrated.mmm.ops.common.{AbsRestActor, Offer, Status}
6 | import org.scalatest.FunSuite
7 | import org.scalatest.mockito.MockitoSugar
8 | import play.api.libs.json.{JsLookupResult, JsValue, Json}
9 |
10 | import scala.util.{Failure, Success, Try}
11 |
12 |
13 | class LivecoinActorTest extends FunSuite with MockitoSugar{
14 |
15 | test ("Livecoin to offer") {
16 | val a =
17 | """
18 | |{
19 | | "totalRows": 3,
20 | | "startRow": 0,
21 | | "endRow": 2,
22 | | "data": [
23 | | {
24 | | "id": 17444431201,
25 | | "currencyPair": "ETH/BTC",
26 | | "goodUntilTime": 0,
27 | | "type": "LIMIT_SELL",
28 | | "orderStatus": "CANCELLED",
29 | | "issueTime": 1535380653251,
30 | | "price": 0.05,
31 | | "quantity": 0.01,
32 | | "remainingQuantity": 0.01,
33 | | "commissionByTrade": 0,
34 | | "bonusByTrade": 0,
35 | | "bonusRate": 0,
36 | | "commissionRate": 0.0018,
37 | | "lastModificationTime": 1535517866196
38 | | },
39 | | {
40 | | "id": 17597454851,
41 | | "currencyPair": "ETH/BTC",
42 | | "goodUntilTime": 0,
43 | | "type": "LIMIT_SELL",
44 | | "orderStatus": "EXECUTED",
45 | | "issueTime": 1535512555998,
46 | | "price": 0.01,
47 | | "quantity": 0.01,
48 | | "remainingQuantity": 0,
49 | | "commissionByTrade": 7.5e-7,
50 | | "bonusByTrade": 0,
51 | | "bonusRate": 0,
52 | | "commissionRate": 0.0018,
53 | | "lastModificationTime": 1535512555998
54 | | },
55 | | {
56 | | "id": 17592585201,
57 | | "currencyPair": "ETH/BTC",
58 | | "goodUntilTime": 0,
59 | | "type": "LIMIT_SELL",
60 | | "orderStatus": "EXECUTED",
61 | | "issueTime": 1535508494524,
62 | | "price": 0.04175,
63 | | "quantity": 0.01,
64 | | "remainingQuantity": 0,
65 | | "commissionByTrade": 7.6e-7,
66 | | "bonusByTrade": 0,
67 | | "bonusRate": 0,
68 | | "commissionRate": 0.0018,
69 | | "lastModificationTime": 1535508501398
70 | | }
71 | | ]
72 | |}
73 | """.stripMargin
74 |
75 | val js = Json.parse(a)
76 | val data = (js \ "data").as[List[JsValue]]
77 | val res = data.map(LivecoinActor.toOffer)
78 | assert(res.size === 3)
79 | assert(res.head.status === Status.cancelled)
80 | }
81 |
82 |
83 | test("Livecoin getUncounteredOffers") {
84 |
85 | val a =
86 | """
87 | |{
88 | | "totalRows": 4,
89 | | "startRow": 0,
90 | | "endRow": 3,
91 | | "data": [
92 | | {
93 | | "id": 17551577701,
94 | | "currencyPair": "ETH/BTC",
95 | | "goodUntilTime": 0,
96 | | "type": "LIMIT_SELL",
97 | | "orderStatus": "CANCELLED",
98 | | "issueTime": 1535474938522,
99 | | "price": 0.05,
100 | | "quantity": 0.01,
101 | | "remainingQuantity": 0.01,
102 | | "commissionByTrade": 0,
103 | | "bonusByTrade": 0,
104 | | "bonusRate": 0,
105 | | "commissionRate": 0.0018,
106 | | "lastModificationTime": 1535518250557
107 | | },
108 | | {
109 | | "id": 17444431201,
110 | | "currencyPair": "ETH/BTC",
111 | | "goodUntilTime": 0,
112 | | "type": "LIMIT_SELL",
113 | | "orderStatus": "CANCELLED",
114 | | "issueTime": 1535380653251,
115 | | "price": 0.05,
116 | | "quantity": 0.01,
117 | | "remainingQuantity": 0.01,
118 | | "commissionByTrade": 0,
119 | | "bonusByTrade": 0,
120 | | "bonusRate": 0,
121 | | "commissionRate": 0.0018,
122 | | "lastModificationTime": 1535517866196
123 | | },
124 | | {
125 | | "id": 17597454851,
126 | | "currencyPair": "ETH/BTC",
127 | | "goodUntilTime": 0,
128 | | "type": "LIMIT_SELL",
129 | | "orderStatus": "EXECUTED",
130 | | "issueTime": 1535512555998,
131 | | "price": 0.01,
132 | | "quantity": 0.01,
133 | | "remainingQuantity": 0,
134 | | "commissionByTrade": 7.5e-7,
135 | | "bonusByTrade": 0,
136 | | "bonusRate": 0,
137 | | "commissionRate": 0.0018,
138 | | "lastModificationTime": 1535512555998
139 | | },
140 | | {
141 | | "id": 17592585201,
142 | | "currencyPair": "ETH/BTC",
143 | | "goodUntilTime": 0,
144 | | "type": "LIMIT_SELL",
145 | | "orderStatus": "EXECUTED",
146 | | "issueTime": 1535508494524,
147 | | "price": 0.04175,
148 | | "quantity": 0.01,
149 | | "remainingQuantity": 0,
150 | | "commissionByTrade": 7.6e-7,
151 | | "bonusByTrade": 0,
152 | | "bonusRate": 0,
153 | | "commissionRate": 0.0018,
154 | | "lastModificationTime": 1535508501398
155 | | }
156 | | ]
157 | |}
158 | """.stripMargin
159 | val js = Json.parse(a)
160 | val res = LivecoinActor.getUncounteredOrders(( js \ "data").as[List[JsValue]])
161 | assert(res.size === 2)
162 | assert(res.head.updatedAt.get < res(1).updatedAt.get)
163 | }
164 |
165 |
166 | }
167 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/sequences/Strategy.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.sequences
2 |
3 | import java.math.MathContext
4 |
5 | import me.mbcu.integrated.mmm.ops.Definitions.Strategies
6 | import me.mbcu.integrated.mmm.ops.Definitions.Strategies.Strategies
7 | import me.mbcu.integrated.mmm.ops.common.Side.Side
8 | import me.mbcu.integrated.mmm.ops.common.{Offer, Side}
9 | import me.mbcu.integrated.mmm.sequences.Strategy.Movement.Movement
10 | import me.mbcu.integrated.mmm.sequences.Strategy.PingPong.PingPong
11 |
12 | import scala.math.BigDecimal.RoundingMode
13 |
14 | object Strategy {
15 |
16 | val mc : MathContext = MathContext.DECIMAL64
17 | val ZERO = BigDecimal("0")
18 | val ONE = BigDecimal("1")
19 | val CENT = BigDecimal("100")
20 | val INFINITY = BigDecimal("1e60")
21 |
22 | object Movement extends Enumeration {
23 | type Movement = Value
24 | val UP, DOWN = Value
25 | }
26 |
27 | object PingPong extends Enumeration {
28 | type PingPong = Value
29 | val ping, pong = Value
30 |
31 | def reverse (a : PingPong) : PingPong = if (a == ping) pong else ping
32 | }
33 |
34 | def seed (qty0 : BigDecimal, unitPrice0 : BigDecimal, amtPwr : Int,
35 | ctrScale : Int, basScale : Int, symbol : String, levels : Int, gridSpace : BigDecimal,
36 | side: Side, act : PingPong,
37 | isPulledFromOtherSide : Boolean, strategy : Strategies, isNoQtyCutoff : Boolean,
38 | maxPrice : Option[BigDecimal] = None, minPrice : Option[BigDecimal] = None) : Seq[Offer] = {
39 | val range = strategy match {
40 | case Strategies.ppt => if(isPulledFromOtherSide) 2 else 1
41 | case Strategies.fullfixed => if(isPulledFromOtherSide) 2 else 1
42 | case _ => 0
43 | }
44 | val zero = PriceQuantity(unitPrice0, qty0)
45 |
46 | (range until (levels + range))
47 | .map(n => {
48 | val movement = if (side == Side.buy) Movement.DOWN else Movement.UP
49 | strategy match {
50 | case Strategies.ppt =>
51 | val mtp = ONE + gridSpace(mc) / CENT
52 | List.fill(n)(1)
53 | .foldLeft(zero)((z, i) => ppt(z.price, z.quantity, amtPwr, mtp, ctrScale, basScale, movement))
54 |
55 | case Strategies.fullfixed =>
56 | List.fill(n)(1)
57 | .foldLeft(zero)((z, i) => step(z.price, z.quantity, gridSpace, ctrScale, movement))
58 |
59 | case _ => PriceQuantity(ZERO, ZERO)
60 | }
61 | }
62 | )
63 | .filter(_.price > 0)
64 | .map(p1q1 => if (p1q1.quantity <= 0 && isNoQtyCutoff) PriceQuantity(p1q1.price, calcMinAmount(ctrScale)) else p1q1)
65 | .filter(_.quantity > 0)
66 | .filter(minPrice match {
67 | case Some(mp) => _.price >= mp
68 | case _ => _.price > ZERO
69 | })
70 | .filter(maxPrice match {
71 | case Some(mp) => _.price <= mp
72 | case None => _.price < INFINITY
73 | })
74 | .map(p1q1 => Offer.newOffer(symbol, side, p1q1.price, p1q1.quantity.setScale(ctrScale, RoundingMode.HALF_EVEN)))
75 | }
76 |
77 | case class PriceQuantity(price : BigDecimal, quantity: BigDecimal)
78 |
79 |
80 | def counter(qty0 : BigDecimal, unitPrice0 : BigDecimal, amtPwr : Int, ctrScale : Int, basScale : Int, symbol : String, gridSpace : BigDecimal, side : Side,
81 | oldAct : PingPong, strategy : Strategies, isNoQtyCutoff : Boolean,
82 | maxPrice : Option[BigDecimal] = None, minPrice : Option[BigDecimal] = None): Offer = {
83 | val newSide = Side.reverse(side)
84 | val newAct = PingPong.reverse(oldAct)
85 | seed(qty0, unitPrice0, amtPwr, ctrScale, basScale, symbol, 1, gridSpace, newSide, newAct, isPulledFromOtherSide = false, strategy, isNoQtyCutoff, maxPrice, minPrice).head
86 | }
87 |
88 | def ppt(unitPrice0 : BigDecimal, qty0 : BigDecimal, amtPower : Int, rate : BigDecimal, ctrScale : Int, basScale: Int, movement: Movement): PriceQuantity ={
89 | val unitPrice1 = if (movement == Movement.DOWN) roundFloor(unitPrice0(mc) / rate, basScale) else roundCeil(unitPrice0 * rate, basScale)
90 | val mtpBoost = sqrt(rate) pow amtPower
91 | val qty1 = if (movement == Movement.DOWN) roundCeil(qty0 * mtpBoost, ctrScale) else roundFloor(qty0(mc) / mtpBoost, ctrScale)
92 | PriceQuantity(unitPrice1, qty1)
93 | }
94 |
95 | def step(unitPrice0 : BigDecimal, qty0 : BigDecimal, rate : BigDecimal, ctrScale : Int, movement: Movement ): PriceQuantity ={
96 | val unitPrice1 = if (movement == Movement.DOWN) unitPrice0(mc) - rate else unitPrice0 + rate
97 | val qty1 = qty0
98 | PriceQuantity(unitPrice1, qty1)
99 | }
100 |
101 | def calcMinAmount(ctrScale : Int) : BigDecimal = {
102 | ONE.setScale(ctrScale, BigDecimal.RoundingMode.CEILING)
103 | }
104 |
105 | def calcMid(unitPrice0 : BigDecimal, qty0 : BigDecimal, amtPower : Int, gridSpace : BigDecimal, ctrScale : Int, basScale:Int, side: Side, marketMidPrice : BigDecimal, strategy : Strategies)
106 | : (BigDecimal, BigDecimal, Int) = {
107 | var base = PriceQuantity(unitPrice0, qty0)
108 | var levels = 0
109 | val mtp = ONE + gridSpace(mc) / CENT
110 |
111 | side match {
112 | case Side.buy =>
113 | while (base.price < marketMidPrice) {
114 | levels = levels + 1
115 | strategy match {
116 | case Strategies.ppt => base = ppt(base.price, base.quantity, amtPower, mtp, ctrScale, basScale, Movement.UP)
117 | case _ => base = step(base.price, base.quantity, gridSpace, ctrScale, Movement.UP)
118 | }
119 | }
120 | case _ =>
121 | while (base.price > marketMidPrice) {
122 | levels = levels + 1
123 | strategy match {
124 | case Strategies.ppt => base = ppt(base.price, base.quantity, amtPower, mtp, ctrScale, basScale, Movement.DOWN)
125 | case _ => base = step(base.price, base.quantity, gridSpace, ctrScale, Movement.DOWN)
126 | }
127 | }
128 | }
129 | (base.price, base.quantity, levels)
130 | }
131 |
132 | private def sqrt(a: BigDecimal, scale: Int = 16): BigDecimal = {
133 | val mc = MathContext.DECIMAL64
134 | var x = BigDecimal( Math.sqrt(a.doubleValue()), mc )
135 |
136 | if (scale < 17) {
137 | return x
138 | }
139 |
140 | var tempScale = 16
141 | while(tempScale < scale){
142 | x = x - (x * x - a)(mc) / (2 * x)
143 | tempScale *= 2
144 | }
145 | x
146 | }
147 |
148 | private def roundCeil(a : BigDecimal, scale : Int): BigDecimal =a.setScale(scale, RoundingMode.CEILING)
149 |
150 | private def roundFloor(a : BigDecimal, scale : Int): BigDecimal =a.setScale(scale, RoundingMode.FLOOR)
151 |
152 | private def roundHalfDown(a : BigDecimal, scale : Int) : BigDecimal = a.setScale(scale, RoundingMode.HALF_DOWN)
153 |
154 | }
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/OrderRestActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import akka.actor.{ActorRef, Cancellable}
4 | import akka.dispatch.ExecutionContexts.global
5 | import me.mbcu.integrated.mmm.actors.OrderRestActor._
6 | import me.mbcu.integrated.mmm.ops.Definitions.{ErrorShutdown, Settings, ShutdownCode}
7 | import me.mbcu.integrated.mmm.ops.common.AbsOrder.{CheckSafeForSeed, QueueRequest, SafeForSeed}
8 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As.As
9 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
10 | import me.mbcu.integrated.mmm.ops.common._
11 | import me.mbcu.scala.MyLogging
12 |
13 | import scala.concurrent.ExecutionContextExecutor
14 | import scala.concurrent.duration._
15 | import scala.language.postfixOps
16 |
17 | object OrderRestActor {
18 |
19 | case class GetLastCounter(book: ActorRef, bot: Bot, as: As)
20 |
21 | case class GotLastCounter(bc: BotCache, as: As)
22 |
23 | case class WriteLastCounter(book: ActorRef, bot: Bot, m: BotCache)
24 |
25 | case class LogActives(arriveMs: Long, buys: Seq[Offer], sels: Seq[Offer])
26 | }
27 |
28 | class OrderRestActor(bot: Bot, exchange: AbsExchange, fileActor: ActorRef) extends AbsOrder(bot) with MyLogging {
29 | private implicit val ec: ExecutionContextExecutor = global
30 | private var op: Option[ActorRef] = None
31 | private implicit val book: ActorRef = self
32 | private implicit val imBot: Bot = bot
33 | private var getFilledCancellable: Option[Cancellable] = None
34 | private var getActiveCancellable: Option[Cancellable] = None
35 |
36 | def receive: Receive = {
37 |
38 | case "start" =>
39 | op = Some(sender())
40 | fileActor ! GetLastCounter(self, bot, As.Init)
41 |
42 | case GotLastCounter(botCache, as) => qFilledOrders(Seq.empty[Offer], botCache.lastCounteredId, as) //must start from past filled orders. Starting bout qFilledOrders and qActiverOrders will mess up init orders
43 |
44 | case GotActiveOrders(offers, currentPage, nextPage, arriveMs, send) =>
45 | if (nextPage) {
46 | qActiveOrders(offers ++ send.cache, send.lastMs, currentPage + 1, send.as)
47 | }
48 | else {
49 | val activeOrders = send.cache ++ offers
50 | val (sortedBuys,sortedSels) = Offer.splitToBuysSels(activeOrders)
51 | send.as match {
52 | case As.Init =>
53 | bot.seed match {
54 | case a if a.equals(StartMethods.cont.toString) => // wont arrive here
55 |
56 | case a if a.equals(StartMethods.lastTicker.toString) =>
57 | qClearOrders(activeOrders, As.Init)
58 | queue1(GetTickerStartPrice(As.Init))
59 |
60 | case a if a.equals(StartMethods.lastOwn.toString) => op foreach(_ ! ErrorShutdown(ShutdownCode.fatal, -1, s"$a is not supported"))
61 |
62 | case _ =>
63 | qClearOrders(activeOrders, As.Init)
64 | qSeed(initialSeed(Seq.empty[Offer], Seq.empty[Offer], BigDecimal(bot.seed)))
65 | scheduleGetFilled(Settings.getFilledSeconds)
66 |
67 | }
68 |
69 | case As.RoutineCheck =>
70 | val dupes = AbsOrder.getDuplicates(sortedBuys) ++ AbsOrder.getDuplicates(sortedSels)
71 | val trims = if (bot.isStrictLevels) trim(sortedBuys, sortedSels, Side.buy) ++ trim(sortedBuys, sortedSels, Side.sell) else Seq.empty[Offer]
72 | val cancels = AbsOrder.margeTrimAndDupes(trims, dupes)
73 | qClearOrders(cancels._1, As.Trim)
74 | qClearOrders(cancels._2, As.KillDupes)
75 | qSeed(grow(sortedBuys, sortedSels, Side.buy) ++ grow(sortedBuys, sortedSels, Side.sell))
76 |
77 | case _ => // not handled
78 | }
79 | self ! LogActives(arriveMs, sortedBuys, sortedSels)
80 | }
81 |
82 | case GotUncounteredOrders(uncountereds, latestCounterId, isSortedFromOldest, arriveMs, send) =>
83 | fileActor ! WriteLastCounter(self, bot, BotCache(latestCounterId.getOrElse(send.lastCounterId)))
84 | def pipe(): Unit = {
85 | if (uncountereds.isEmpty) scheduleGetActive(1) else qCounter(uncountereds)
86 | scheduleGetFilled(Settings.getFilledSeconds) // should it be moved to GotActiveOrders$As.RoutineCheck ?
87 | }
88 | send.as match {
89 | case As.Init =>
90 | bot.seed match {
91 | case a if a.equals(StartMethods.cont.toString) => pipe()
92 |
93 | case _ => qActiveOrders(Seq.empty[Offer], System.currentTimeMillis(), page = 1, As.Init) // lastTicker or custom = config change, so past filled orders are not countered
94 | }
95 |
96 | case As.RoutineCheck => pipe()
97 |
98 | case _ => // not handled
99 | }
100 |
101 |
102 | case GotTickerStartPrice(price, arriveMs, send) => // start ownTicker
103 | price match {
104 | case Some(p) =>
105 | qSeed(initialSeed(Seq.empty[Offer], Seq.empty[Offer], p))
106 | scheduleGetFilled(Settings.getFilledSeconds)
107 | case _ => // no ticker price found
108 | }
109 |
110 | case SafeForSeed(yes) =>
111 | if (yes) {
112 | qActiveOrders(Seq.empty[Offer], System.currentTimeMillis(), page = 1, As.RoutineCheck)
113 | } else{
114 | scheduleGetActive(1)
115 | }
116 |
117 | case "get routine active orders" => op foreach(_! CheckSafeForSeed(self, bot))
118 |
119 | case "get filled orders" => fileActor ! GetLastCounter(self, bot, As.RoutineCheck)
120 |
121 | case LogActives(arriveMs, buys, sels) =>
122 | (arriveMs / 1000L) % 30 match {
123 | case x if x < 33 | x > 27 => info(Offer dump(bot, buys, sels))
124 | case _ => // ignore log
125 | }
126 | }
127 |
128 | def scheduleGetActive(s: Int) : Unit = {
129 | getActiveCancellable foreach(_.cancel())
130 | getActiveCancellable = Some(context.system.scheduler.scheduleOnce(s seconds, self, message="get routine active orders"))
131 | }
132 |
133 | def scheduleGetFilled(s: Int): Unit = {
134 | getFilledCancellable foreach(_.cancel())
135 | getFilledCancellable = Some(context.system.scheduler.scheduleOnce(s seconds, self, message="get filled orders"))
136 | }
137 |
138 | def qCounter(seq: Seq[Offer]): Unit = {
139 | val counters = Offer.sortTimeDesc(seq).map(counter).map(NewOrder(_, As.Counter))
140 | queue(counters)
141 | }
142 |
143 | def qSeed(offers: Seq[Offer]): Unit = queue(offers.map(NewOrder(_, As.Seed)))
144 |
145 | def qClearOrders(offers: Seq[Offer], as: As): Unit = queue(offers.map(CancelOrder(_,as)))
146 |
147 | def qActiveOrders(cache: Seq[Offer], lastMs: Long, page:Int, as: As) : Unit = queue1(GetActiveOrders(lastMs, cache, page, as))
148 |
149 | def qFilledOrders(cache: Seq[Offer], lastCounterId:String, as: As) : Unit = queue1(GetFilledOrders(lastCounterId, as))
150 |
151 | def queue(reqs: Seq[SendRest]): Unit = op foreach(_ ! QueueRequest(reqs))
152 |
153 | def queue1(req: SendRest): Unit = queue(Seq(req))
154 |
155 | }
156 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/hitbtc/HitbtcParser.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.hitbtc
2 |
3 | import akka.actor.ActorRef
4 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode
5 | import me.mbcu.integrated.mmm.ops.common.AbsWsParser._
6 | import me.mbcu.integrated.mmm.ops.common.Side.Side
7 | import me.mbcu.integrated.mmm.ops.common.{AbsWsParser, Offer, Status}
8 | import me.mbcu.integrated.mmm.ops.hitbtc.HitbtcStatus.HitbtcStatus
9 | import org.joda.time.DateTime
10 | import play.api.libs.json.{JsValue, Json}
11 |
12 | import scala.util.{Failure, Success, Try}
13 |
14 | object HitbtcParser {
15 |
16 | def toOffer(js: JsValue): Offer = {
17 | Offer(
18 | (js \ "clientOrderId").as[String],
19 | (js \ "symbol").as[String],
20 | (js \ "side").as[Side],
21 | (js \ "status").as[HitbtcStatus] match {
22 | case HitbtcStatus.`new` => Status.active
23 | case HitbtcStatus.canceled => Status.cancelled
24 | case HitbtcStatus.expired => Status.cancelled
25 | case HitbtcStatus.filled => Status.filled
26 | case HitbtcStatus.partiallyFilled => Status.partiallyFilled
27 | case _ => Status.cancelled
28 | },
29 | DateTime.parse((js \ "createdAt").as[String]).getMillis,
30 | Some(DateTime.parse((js \ "updatedAt").as[String]).getMillis),
31 | (js \ "quantity").as[BigDecimal],
32 | (js \ "price").as[BigDecimal],
33 | Some((js \ "cumQuantity").as[BigDecimal])
34 | )
35 |
36 | /*
37 | "id": "4345613661",
38 | "clientOrderId": "57d5525562c945448e3cbd559bd068c3",
39 | "symbol": "BCCBTC",
40 | "side": "sell",
41 | "status": "new",
42 | "type": "limit",
43 | "timeInForce": "GTC",
44 | "quantity": "0.013",
45 | "price": "0.100000",
46 | "cumQuantity": "0.000",
47 | "createdAt": "2017-10-20T12:17:12.245Z",
48 | "updatedAt": "2017-10-20T12:17:12.245Z",
49 | "reportType": "status"
50 | */
51 | }
52 |
53 | }
54 |
55 | class HitbtcParser(op: ActorRef) extends AbsWsParser(op){
56 |
57 | override def parse(raw: String, botMap: Map[String, ActorRef]): Unit = {
58 | info(
59 | s"""Raw response
60 | |$raw
61 | """.stripMargin)
62 |
63 | val x = Try(Json parse raw)
64 | x match {
65 | case Success(js) =>
66 | if ((js \ "error").isDefined){
67 | val error = (js \ "error").as[RPCError]
68 | val requestId = (js \ "id").as[String]
69 | val book = botMap(HitbtcRequest.pairFromId(requestId))
70 | error.code match {
71 | case 2011 | 10001 | 20001 | 20002 => book ! RemoveOfferWs(isRetry = false, HitbtcRequest.clientOrderIdFrom(requestId), HitbtcRequest.sideFromId(requestId), requestId)
72 | case 20008 | 20004 =>
73 | val isRetry = if(requestId.contains("newOrder")) true else false
74 | book ! RemoveOfferWs(isRetry, HitbtcRequest.clientOrderIdFrom(requestId), HitbtcRequest.sideFromId(requestId), requestId) // duplicate clientOrderId
75 | case 2001 | 2002 => errorShutdown(ShutdownCode.fatal, error.code, error.message.getOrElse("Currency / symbol not found"))
76 | case 1001 | 1002 | 1003 | 1004 | 403 => errorShutdown(ShutdownCode.fatal, error.code, error.message.getOrElse("Authentication failed"))
77 | case 429 | 500 | 503 | 504 => book ! RetryPendingWs(requestId)
78 | case _ => // ErrorNonAffecting(error, id)
79 | }
80 | /*
81 | 403 401 Action is forbidden for account
82 | 429 429 Too many requests Action is being rate limited for account
83 | 500 500 Internal Server Error
84 | 503 503 Service Unavailable Try it again later
85 | 504 504 Gateway Timeout Check the result of your request later
86 | 1001 401 Authorisation required
87 | 1002 401 Authorisation failed
88 | 1003 403 Action is forbidden for this API key Check permissions for API key
89 | 1004 401 Unsupported authorisation method Use Basic authentication
90 | 2001 400 Symbol not found
91 | 2002 400 Currency not found
92 | 20001 400 Insufficient funds Insufficient funds for creating order or any account operation
93 | 20002 400 Order not found Attempt to get active order that not existing, filled, canceled or expired. Attempt to cancel not existing order. Attempt to cancel already filled or expired order.
94 | 20003 400 Limit exceeded Withdrawal limit exceeded
95 | 20004 400 Transaction not found Requested transaction not found
96 | 20005 400 Payout not found
97 | 20006 400 Payout already committed
98 | 20007 400 Payout already rolled back
99 | 20008 {"jsonrpc":"2.0","error":{"code":20008,"message":"Duplicate clientOrderId","description":"ClientOrderId must be unique during trading session"},"id":"cancelOrder.NOAHBTC.buy.559071f116b6b5fcadde"}
100 | code":2011,"message":"Quant ity too low
101 | {"code":10001,"message":"\"price\" must be a positive number
102 | code":2011,"message":"Quantity too low","description":"Minimum quantity 1"
103 | */
104 | }
105 | else {
106 | if((js \ "method").isDefined && (js \ "params").isDefined){ // stream report comes here
107 | val method = (js \ "method").as[String]
108 | val params = js \ "params"
109 | method match {
110 |
111 | case "activeOrders" =>
112 | val mapped = params.as[List[JsValue]].map(HitbtcParser.toOffer).groupBy(_.symbol)
113 | botMap.map(p => (p._2, mapped.getOrElse(p._1, Seq.empty[Offer]))).foreach(p => p._1 ! GotActiveOrdersWs(p._2, "noId"))
114 |
115 | case "report" =>
116 | val offer = HitbtcParser.toOffer(params.as[JsValue])
117 | botMap(offer.symbol) ! GotOfferWs(offer, offer.id)
118 |
119 | case "ticker" =>
120 | val pair = (params \ "symbol").as[String]
121 | val last = (params \ "last").as[BigDecimal]
122 | botMap(pair) ! GotTickerPriceWs(Some(last), "subscribeReports")
123 |
124 | case _ =>
125 | }
126 |
127 | }
128 | else if ((js \ "id").isDefined){ // RPC response
129 | val id = (js \ "id").as[String]
130 | id match {
131 | case a if a equals "login" => op ! LoggedIn(a) // {"jsonrpc":"2.0","result":true,"id":"login"}
132 | case a if a equals "subscribeReports" => op ! GotSubscribe(a)
133 | case _ => botMap(HitbtcRequest.pairFromId(id)) ! RemovePendingWs(id)
134 | }
135 | }
136 | }
137 |
138 | case Failure(e) => errorIgnore(-1, s"Cannot parse into Json: $raw", shouldEmail = true)
139 |
140 | }
141 |
142 | }
143 |
144 | }
145 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/fcoin/FcoinActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.fcoin
2 |
3 | import akka.dispatch.ExecutionContexts.global
4 | import akka.stream.ActorMaterializer
5 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode
6 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
7 | import me.mbcu.integrated.mmm.ops.common.Side.Side
8 | import me.mbcu.integrated.mmm.ops.common.{AbsRestActor, Offer, Status}
9 | import me.mbcu.integrated.mmm.ops.fcoin.FcoinRestRequest.FcoinParams
10 | import me.mbcu.integrated.mmm.ops.fcoin.FcoinState.FcoinState
11 | import me.mbcu.scala.MyLogging
12 | import play.api.libs.json.{JsLookupResult, JsValue, Json}
13 | import play.api.libs.ws.ahc.StandaloneAhcWSClient
14 |
15 | import scala.concurrent.ExecutionContextExecutor
16 | import scala.language.postfixOps
17 | import scala.util.{Failure, Success, Try}
18 |
19 | object FcoinActor {
20 |
21 | def toOffer(p: JsValue): Offer = {
22 | val state = (p \ "state").as[FcoinState]
23 | val status = state match {
24 | case FcoinState.filled => Status.filled
25 | case FcoinState.submitted => Status.active
26 | case FcoinState.partial_filled => Status.partiallyFilled
27 | case _ => Status.cancelled
28 | }
29 | new Offer(
30 | (p \ "id").as[String],
31 | (p \ "symbol").as[String],
32 | (p \ "side").as[Side],
33 | status,
34 | (p \ "created_at").as[Long],
35 | None,
36 | (p \ "amount").as[BigDecimal],
37 | (p \ "price").as[BigDecimal],
38 | Some((p \ "filled_amount").as[BigDecimal])
39 | )
40 | /*
41 | {
42 | "id" : "string" ,
43 | "symbol" : "string" ,
44 | "type" : "limit" ,
45 | "side" : "buy" ,
46 | "price" : "string" ,
47 | "amount" : "string" ,
48 | "state" : "submitted" ,
49 | "executed_value" : "string" ,
50 | "fill_fees" : "string" ,
51 | "filled_amount" : "string" ,
52 | "created_at" : 0 ,
53 | "source" : "web"
54 | }
55 | ]
56 | */
57 | }
58 |
59 | def parseFilled(data: JsLookupResult, lastCounterId: String): (Seq[Offer], Option[String]) = {
60 | val dexed = data.as[Seq[JsValue]].map(toOffer).filter(_.status == Status.filled).zipWithIndex
61 |
62 | if (dexed.isEmpty){
63 | (Seq.empty[Offer], None)
64 | } else {
65 | val latestCounterId = dexed.head._1.id
66 | val idPos = dexed.find(a => a._1.id == lastCounterId) match {
67 | case Some(pos) => pos._2
68 | case _ => Int.MaxValue
69 | }
70 | val uncountereds = dexed.filter(_._2 < idPos).sortWith(_._2 < _._2).map(_._1)
71 | (uncountereds, Some(latestCounterId))
72 | }
73 | }
74 | }
75 |
76 | class FcoinActor() extends AbsRestActor() with MyLogging {
77 | import play.api.libs.ws.DefaultBodyReadables._
78 | import play.api.libs.ws.DefaultBodyWritables._
79 |
80 | import scala.concurrent.duration._
81 |
82 | private implicit val materializer = ActorMaterializer()
83 | private implicit val ec: ExecutionContextExecutor = global
84 | private var ws = StandaloneAhcWSClient()
85 |
86 | override def start(): Unit = setOp(Some(sender()))
87 |
88 | override def sendRequest(r: AbsRestActor.SendRest): Unit = {
89 |
90 | r match {
91 | case a: GetTickerStartPrice => httpGet(a, FcoinRestRequest.getTickers(a.bot.pair))
92 |
93 | case a: GetActiveOrders => httpGet(a, FcoinRestRequest.getOrders(a.bot.credentials, a.bot.pair, FcoinState.submitted, a.page))
94 |
95 | case a: GetFilledOrders => httpGet(a, FcoinRestRequest.getOrders(a.bot.credentials, a.bot.pair, FcoinState.filled, 1))
96 |
97 | case a: NewOrder => httpPost(a, FcoinRestRequest.newOrder(a.bot.credentials, a.bot.pair, a.offer.side, a.offer.price, a.offer.quantity))
98 |
99 | case a: CancelOrder => httpPost(a, FcoinRestRequest.cancelOrder(a.bot.credentials, a.offer.id))
100 |
101 | }
102 |
103 | def httpGet(a: SendRest, f: FcoinParams): Unit = {
104 | ws.url(f.url)
105 | .addHttpHeaders("FC-ACCESS-KEY" -> a.bot.credentials.pKey)
106 | .addHttpHeaders("FC-ACCESS-SIGNATURE" -> f.sign)
107 | .addHttpHeaders("FC-ACCESS-TIMESTAMP" -> f.ts.toString)
108 | .withRequestTimeout(requestTimeoutSec seconds)
109 | .get()
110 | .map(response => parse(a, f.url, response.body[String]))
111 | .recover{
112 | case e: Exception => errorRetry(a, 0, e.getMessage, shouldEmail = false)
113 | }
114 | }
115 |
116 | def httpPost(a: SendRest, r: FcoinParams): Unit = {
117 | ws.url(r.url)
118 | .addHttpHeaders("Content-Type" -> "application/json;charset=UTF-8")
119 | .addHttpHeaders("FC-ACCESS-KEY" -> a.bot.credentials.pKey)
120 | .addHttpHeaders("FC-ACCESS-SIGNATURE" -> r.sign)
121 | .addHttpHeaders("FC-ACCESS-TIMESTAMP" -> r.ts.toString)
122 | .withRequestTimeout(requestTimeoutSec seconds)
123 | .post(r.js)
124 | .map(response => parse(a, r.params, response.body[String]))
125 | .recover{
126 | case e: Exception => errorRetry(a, 0, e.getMessage, shouldEmail = false)
127 | }
128 | }
129 | }
130 |
131 | def parse(a: AbsRestActor.SendRest, request: String, raw: String): Unit = {
132 | val arriveMs = System.currentTimeMillis()
133 |
134 | a match {
135 | case p: NewOrder => op foreach (_ ! GotNewOrderId("unused", p.as, arriveMs, p))
136 | case _ =>
137 | }
138 |
139 | val x = Try(Json parse raw)
140 | x match {
141 | case Success(js) =>
142 | val rootStatus = (js \ "status").as[Int]
143 | if (rootStatus != 0) {
144 | val msg = (js \ "msg").as[String]
145 | msg match {
146 | case m if m.contains("api key check fail") => errorShutdown(ShutdownCode.fatal, 0, s"$request $m")
147 | case _ =>
148 | error(raw)
149 | errorIgnore(0, msg)
150 | }
151 | }
152 | else {
153 | val data = js \ "data"
154 | a match {
155 | case a: GetTickerStartPrice =>
156 | val lastPrice = (data \ "ticker").head.as[BigDecimal]
157 | a.book ! GotTickerStartPrice(Some(lastPrice), arriveMs, a)
158 |
159 | case a: GetFilledOrders =>
160 | val res = FcoinActor.parseFilled(data, a.lastCounterId)
161 | a.book ! GotUncounteredOrders(res._1, res._2, isSortedFromOldest = true, arriveMs, a)
162 |
163 | case a: GetActiveOrders =>
164 | val res = data.as[Seq[JsValue]].map(FcoinActor.toOffer)
165 | a.book ! GotActiveOrders(res, a.page, if (res.size == 100) true else false, arriveMs, a)
166 |
167 | case a: CancelOrder => // not handled
168 |
169 | case a: NewOrder => // not handled, serverTime not available
170 |
171 | }
172 | }
173 |
174 | case Failure(e) =>
175 | raw match {
176 | case u: String if u.contains("") => errorRetry(a, 0, raw)
177 | case _ => errorIgnore(0, s"Unknown FcoinActor#parse : $raw")
178 | }
179 | }
180 |
181 | }
182 |
183 | }
184 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/okex/OkexRestActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.okex
2 |
3 | import akka.dispatch.ExecutionContexts.global
4 | import akka.stream.ActorMaterializer
5 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode
6 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
7 | import me.mbcu.integrated.mmm.ops.common.Side.Side
8 | import me.mbcu.integrated.mmm.ops.common.{AbsRestActor, Offer, Status}
9 | import me.mbcu.scala.MyLogging
10 | import play.api.libs.json.{JsValue, Json}
11 | import play.api.libs.ws.ahc.StandaloneAhcWSClient
12 |
13 | import scala.concurrent.ExecutionContextExecutor
14 | import scala.concurrent.duration._
15 | import scala.language.postfixOps
16 | import scala.util.{Failure, Success, Try}
17 |
18 | object OkexRestActor {
19 | def parseForId(js: JsValue): String = (js \ "order_id").as[Long].toString
20 |
21 | val toOffer: JsValue => Offer = (data: JsValue) => {
22 | val status = (data \ "status").as[Int] match {
23 | case -1 => Status.cancelled
24 | case 0 => Status.active
25 | case 1 => Status.partiallyFilled
26 | case 2 => Status.filled
27 | case 3 => Status.cancelled
28 | case _ => Status.cancelled
29 | }
30 | new Offer(
31 | (data \ "order_id").as[Long].toString,
32 | (data \ "symbol").as[String],
33 | (data \ "type").as[Side],
34 | status,
35 | (data \ "create_date").as[Long],
36 | None,
37 | (data \ "amount").as[BigDecimal],
38 | (data \ "price").as[BigDecimal],
39 | (data \ "deal_amount").asOpt[BigDecimal]
40 | )
41 | }
42 |
43 | def parseFilled(js: JsValue, lastCounterId: Long): (Seq[Offer], Option[String]) = {
44 | val filleds = (js \ "orders").as[List[JsValue]]
45 | .filter(a => (a \ "status").as[Int] == OkexStatus.filled.id) // assuming partially filled orders eventually have filled status
46 | .filter(a => (a \ "order_id").as[Long] > lastCounterId)
47 | .map(toOffer)
48 | if (filleds.isEmpty){
49 | (Seq.empty[Offer], None)
50 | } else {
51 | val latestCounterId = filleds.head.id
52 | val sorted = filleds.sortWith(_.createdAt < _.createdAt)
53 | (sorted, Some(latestCounterId))
54 | }
55 | }
56 | }
57 |
58 | class OkexRestActor() extends AbsRestActor() with MyLogging {
59 | import play.api.libs.ws.DefaultBodyReadables._
60 | import play.api.libs.ws.DefaultBodyWritables._
61 |
62 | private implicit val materializer = ActorMaterializer()
63 | private implicit val ec: ExecutionContextExecutor = global
64 | val OKEX_ERRORS: Map[Int, String] = OkexRest.OKEX_ERRORS
65 | val url: String = OkexRest.endpoint
66 |
67 | private var ws = StandaloneAhcWSClient()
68 |
69 | override def start(): Unit = setOp(Some(sender()))
70 |
71 | override def sendRequest(r: SendRest): Unit = {
72 |
73 | r match {
74 | case a: NewOrder => httpPost(a,s"$url/trade.do", OkexRestRequest.restNewOrder(a.bot.credentials, a.bot.pair, a.offer.side, a.offer.price, a.offer.quantity))
75 |
76 | case a: CancelOrder => httpPost(a,s"$url/cancel_order.do", OkexRestRequest.restCancelOrder(a.bot.credentials, a.bot.pair, a.offer.id))
77 |
78 | case a: GetActiveOrders => httpPost(a,s"$url/order_history.do", OkexRestRequest.restOwnTrades(a.bot.credentials, a.bot.pair, OkexStatus.unfilled, a.page) )
79 |
80 | case a: GetFilledOrders => httpPost(a,s"$url/order_history.do", OkexRestRequest.restOwnTrades(a.bot.credentials, a.bot.pair, OkexStatus.filled, 1))
81 |
82 | case a: GetTickerStartPrice => httpGet(a,s"$url/ticker.do", OkexRestRequest.restTicker(a.bot.pair))
83 |
84 | }
85 |
86 | def httpPost(a: SendRest, url: String, params: Map[String, String]): Unit =
87 | ws.url(url)
88 | .addHttpHeaders("Content-Type" -> "application/x-www-form-urlencoded")
89 | .withRequestTimeout(requestTimeoutSec seconds)
90 | .post(stringifyXWWWForm(params))
91 | .map(response => parse(a, response.body[String]))
92 | .recover{
93 | case e: Exception => errorRetry(a, 0, e.getMessage, shouldEmail = false)
94 | }
95 |
96 | def httpGet(a: SendRest, url: String, params: Map[String, String]): Unit =
97 | ws.url(url)
98 | .addQueryStringParameters(params.toSeq: _*)
99 | .withRequestTimeout(requestTimeoutSec seconds)
100 | .get()
101 | .map(response => parse(a, response.body[String]))
102 | .recover{
103 | case e: Exception => errorRetry(a, 0, e.getMessage, shouldEmail = false)
104 | }
105 |
106 | }
107 |
108 | def parse(a: SendRest, raw: String): Unit = {
109 | info(logResponse(a, raw))
110 | val arriveMs = System.currentTimeMillis()
111 |
112 | a match {
113 | case p: NewOrder => op foreach (_ ! GotNewOrderId("unused", p.as, arriveMs, p))
114 | case _ =>
115 | }
116 |
117 | val x = Try(Json parse raw)
118 | x match {
119 | case Success(js) =>
120 | if ((js \ "success").isDefined) {
121 | val code = (js \ "error_code").as[Int]
122 | val msg = OKEX_ERRORS.getOrElse(code, " code list : https://github.com/okcoin-okex/API-docs-OKEx.com/blob/master/API-For-Spot-EN/Error%20Code%20For%20Spot.md")
123 | pipeErrors(code, msg, a)
124 | }
125 | else {
126 | val book = a.book
127 | a match {
128 | case t: GetTickerStartPrice =>
129 | val lastTrade = (js \ "ticker" \ "last").as[BigDecimal]
130 | book ! GotTickerStartPrice(Some(lastTrade), arriveMs, t)
131 |
132 | case t: GetFilledOrders =>
133 | val res = OkexRestActor.parseFilled(js, t.lastCounterId.toLong)
134 | book ! GotUncounteredOrders(res._1, res._2, isSortedFromOldest = true, arriveMs, t)
135 |
136 | case t: GetActiveOrders =>
137 | val res = (js \ "orders").as[List[JsValue]].map(OkexRestActor.toOffer)
138 | val currentPage = (js \ "currency_page").as[Int]
139 | val nextPage = if ((js \ "page_length").as[Int] > 200) true else false
140 | book ! GotActiveOrders(res, currentPage, nextPage, arriveMs, t)
141 |
142 | case t: CancelOrder => // not handled
143 |
144 | case t: NewOrder => // not handled, serverTime not available
145 |
146 | case _ => error(s"Unknown OkexRestActor#parseRest : $raw")
147 | }
148 | }
149 |
150 | case Failure(e) => raw match {
151 | case u: String if u.contains("") =>
152 | val code = -1000
153 | val msg =
154 | s"""${OKEX_ERRORS(code)}
155 | |$raw
156 | """.stripMargin
157 | pipeErrors(code, msg, a)
158 | case _ => error(s"Unknown OkexRestActor#parseRest : $raw")
159 | }
160 | }
161 | }
162 |
163 | private def pipeErrors(code: Int, msg: String, sendRequest: SendRest): Unit = {
164 | code match {
165 | case 10009 | 10010 | 10011 | 10014 | 10016 | 10024 | 1002 => errorIgnore(code, msg)
166 | case 20100 | 10007 | 10005 | -1000 => errorRetry(sendRequest, code, msg)
167 | case 10008 | 10100 => errorShutdown(ShutdownCode.fatal, code, msg)
168 | case _ => errorRetry(sendRequest, code, msg)
169 | }
170 | }
171 |
172 | }
173 |
--------------------------------------------------------------------------------
/OPERATION.md:
--------------------------------------------------------------------------------
1 | # Operation
2 |
3 | How the bot operates depends on the exchange's type. Here is a list the bot operates on
4 |
5 | ## Exchange Types
6 |
7 | ### REST Exchanges
8 |
9 | REST Exchanges supports only REST methods for trades. This is the most common type of exchange.
10 |
11 | Characteristics:
12 |
13 | - Order cannot be pre-set with custom id. A raw offer is sent then returned with a response that contains order id.
14 | - To get the status of an order, we do another request gerOrderInfo.
15 |
16 | Example: Okex, Binance, Huobi
17 |
18 | #### Operations
19 |
20 | There are two ways a bot manages orders for REST exchange. By fetching Trade History and Active Orders or by checking orders with GetOrderInfo
21 |
22 | #### Trade History Method
23 |
24 | The bot basically fetches two pieces of information to operate:
25 | - Trade History or history of filled orders for the bot to counter
26 | - Active Orders to see if the bot needs to seed empty side or trim extra orders.
27 |
28 | As the bot simply counters whatever orders that come out in Trade History the only problem is to mark the last offer the bot left out.
29 | For this purpose the bot saves the last countered order on a text file, in the form of order id or timestamp.
30 | The next time it fetches Trade History, it will start countering from this id.
31 | If the boundary doesn't exist in the response, the bot will consider all orders there to be uncountered and counter them all
32 |
33 | Active Order on the other hand are needed for seeding or trimming.
34 |
35 | Both Trade History and Active Orders are fetched cyclically.
36 |
37 | This is the simplest and the best possible method for REST exchange as it doesn't require many calls to the server.
38 |
39 | For this method to work, an exchange must have Trade History method that returns one among other things the status (filled, partially-filled, cancelled)
40 |
41 | All requests are pooled in a queue and popped by the rate allowed by the exchange.
42 |
43 | #### GetOrderInfo Method
44 |
45 | Some exchanges have Trade History that doesn't return trade status. We cannot know if it's filled or partially filled.
46 |
47 | As the bot only counters one order with one order, countering an order with a lot of partially-filled fractured orders is not desirable.
48 |
49 | In this method, the bot maintains in-memory open orders. Every interval, the bot sends getOrderInfo request to check the status of an order.
50 |
51 | The original request is cached in memory. Subsequent order info will only update it (to preserve the original information).
52 |
53 | If the status is filled the bot will counter it and remove the order from the memory.
54 |
55 | The bot also checks regularly if it needs to reseed or trim a side.
56 |
57 | This method requires a lot of requests to be sent to server and may not work well if there are too many orders.
58 |
59 | ### Websocket Exchanges
60 |
61 | Websocket exchanges supports Websocket methods for trades
62 |
63 | Characteristics:
64 |
65 | - Offer can be pre-set with a custom id. This is critical since in websocket a request doesn't correspond to a response.
66 | - Successful offer request returns its status (filled, partially-filled, etc)
67 | - Has event stream. Any trade event is broadcast into a channel.
68 |
69 | Example: HitBTC
70 |
71 | #### Operations
72 |
73 | It is similar to GetOrderInfo method except that the bot doesn't need to send getOrderInfo request. The bot simply listens to a subscribed channel for trade events.
74 |
75 | As such the bot can act very fast to counter a filled order.
76 |
77 | Websocket may get disconnected. To handle such scenario, the bot caches all outbound orders. Successful request will remove the corresponding order from the cache.
78 | If websocket is down, re-send all cached requests.
79 |
80 | ### DEX (Distributed Exchange)
81 |
82 | Trades are done on or off blockchain directly from wallet. There are many DEX platforms such as Ripple or Stellar we are working on DDEX API which implements 0x.
83 |
84 | Characteristics
85 | - Mix of websocket and REST methods
86 | - Many operations depend on blockhain events (counter immediately upon trade executed event vs wait until it is finalized on ledger)
87 |
88 | #### Operations
89 |
90 | This is similar to Websocket. Trades are done with REST but updates come from websocket stream.
91 |
92 | ### Stupid Websocket Exchanges
93 |
94 | Websocket that just wraps REST methods
95 |
96 | Characteristics:
97 | - Basically REST methods are done through websocket
98 |
99 | Example: Okex
100 |
101 | In REST it is possible to map request and response. In websocket it is not possible.
102 | That's why client uses response-challenge on a param like request id to match it with incoming response.
103 | ~Having one websocket for one bot(one symbol) can make things easier, since we can eliminate the possibilities of getting responses not related to that bot.~ API quota !
104 | Although it is very likely such server isn't designed to handle HFT (since it just wraps REST) it is still worthwhile to do websocket method if only to save some latency from Http POST/GET.
105 |
106 | ## Duplicates
107 |
108 | Duplicates are orders with the same price and amount. This is the no.1 undesirable. Duplicates are always the result of seeding at the wrong time.
109 |
110 | To prevent duplicates, seeding should initiate only when there's no pending trades in the request queue. The goal is that seeding should be based on stable open orders.
111 |
112 | The bot also has remove duplicates function that fires off regularly. However the use should not be exploited as sending order reduces the API quota.
113 |
114 | ## Extension
115 |
116 | This app is built with Akka framework that uses Actors. The core ones are:
117 | - BaseActor: reads config
118 | - OpActor : contains FIFO sequence that caches requests and sends them one-by-one with rate allowed by the exchange
119 | - OrderActor: manages order operation for a pair
120 | - counter : happens immediately after getFilledOrders returns uncounteres orders
121 | - cancel duplicates : happens if getActiveOrders returns duplicates
122 | - seed : happens if getActiveOrders returns no duplicates and has any side that is shorter than config's gridLevels
123 | - trim : happens if `bot.isStrictLevel = true` and getActiveOrders returns any side longer than config's gridLevels
124 | - ExchangeActor: handles API's request, response specific for an exchange
125 |
126 | To include a new exchange, these items need to be checked:
127 | - the type of exchange
128 | - if it's REST check if TradeHistory returns
129 | - execution timestamp (or any sortable id) to know the order the trades are executed
130 | - status (filled, partially-filled) and see if ever returns a `filled` after an order is partially-filled many times
131 | - figure out how to sign a request
132 | - implement the necessary API
133 |
134 | ### For `rest`
135 | - own past trades API must have filled, partially filled, closed status and have sortable field
136 | - the API command has sort parameter or is sorted naturally
137 | - sort parameter or sortable field (e.g updatedTime or tradeId) will be lastCounterId
138 | - if it has sort parameter, check inclusion/exclusion (ts=a returns data exclusively after a or not)
139 | - if these requirements are not met use `restgi`: check all orders one-by-one with GetOrderInfo API
140 |
141 |
142 |
143 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/common/AbsOrder.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.common
2 |
3 | import akka.actor.{Actor, ActorRef}
4 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.{As, CancelOrder, NewOrder, SendRest}
5 | import me.mbcu.integrated.mmm.ops.common.Side.Side
6 | import me.mbcu.integrated.mmm.sequences.Strategy
7 | import me.mbcu.integrated.mmm.sequences.Strategy.PingPong
8 |
9 | import scala.collection.mutable
10 |
11 | object AbsOrder {
12 | // find duplicates: same price, same quantity, returns the newests (for cancellation)
13 | def getDuplicates(offers: Seq[Offer]): Seq[Offer] =
14 | offers.groupBy(_.price).collect { case (x, ys) if ys.lengthCompare(1) > 0 =>
15 | ys.groupBy(_.quantity).collect { case (r, s) if s.lengthCompare(1) > 0 => s.sortWith(_.createdAt < _.createdAt) }
16 | }.flatten.flatMap(_.drop(1)).toSeq
17 |
18 | def isSafeForSeed(q: mutable.Queue[SendRest], nos: mutable.Set[NewOrder], bot: Bot): Boolean =
19 | (q.filter(_.bot == bot) ++ nos.filter(_.bot == bot).toSeq)
20 | .collect {
21 | case a: NewOrder => a
22 | case a: CancelOrder => a
23 | }
24 | .isEmpty
25 |
26 | /**
27 | * merge trims and killDupes while preserving As
28 | * @param trims
29 | * @param dupes
30 | * @return (trims, killDupes)
31 | */
32 | def margeTrimAndDupes(trims: Seq[Offer], dupes: Seq[Offer]): (Seq[Offer], Seq[Offer]) = {
33 | val res = (trims.map(p => (p, As.Trim)) ++ dupes.map(p => (p, As.KillDupes)))
34 | .map(p => p._1.id -> p).toMap
35 | .values
36 | .groupBy(p => p._2)
37 | .toSeq.sortWith(_._1.toString > _._1.toString)
38 | .map(p => (p._1, p._2.toSeq.unzip._1))
39 | .partition(_._1 == As.Trim)
40 | (res._1.unzip._2.flatten, res._2.unzip._2.flatten)
41 | }
42 |
43 | case class CheckSafeForSeed(ref: ActorRef, bot: Bot)
44 |
45 | case class SafeForSeed(yes: Boolean)
46 |
47 | case class QueueRequest(a: Seq[SendRest])
48 |
49 |
50 | }
51 |
52 | abstract class AbsOrder(bot: Bot) extends Actor {
53 |
54 | def initialSeed(sortedBuys: Seq[Offer], sortedSels: Seq[Offer], midPrice: BigDecimal): Seq[Offer] = {
55 | var res: Seq[Offer] = Seq.empty[Offer]
56 | var buyQty = BigDecimal(0)
57 | var selQty = BigDecimal(0)
58 | var calcMidPrice = midPrice
59 | var buyLevels = 0
60 | var selLevels = 0
61 |
62 | def set(up: BigDecimal, bq: BigDecimal, sq: BigDecimal, bl: Int, sl: Int): Unit = {
63 | buyQty = bq
64 | selQty = sq
65 | buyLevels = bl
66 | selLevels = sl
67 | calcMidPrice = up
68 | }
69 |
70 | (sortedBuys.size, sortedSels.size) match {
71 | case (a, s) if a == 0 && s == 0 => set(midPrice, bot.buyOrderQuantity, bot.sellOrderQuantity, bot.buyGridLevels, bot.sellGridLevels)
72 |
73 | case (a, s) if a != 0 && s == 0 =>
74 | val anyBuy = sortedBuys.head
75 | val calcMid = Strategy.calcMid(anyBuy.price, anyBuy.quantity, bot.quantityPower, bot.gridSpace, bot.counterScale, bot.baseScale, Side.buy, midPrice, bot.strategy)
76 | val bl = calcMid._3 - 1
77 | set(calcMid._1, calcMid._2, calcMid._2, bl, bot.sellGridLevels)
78 |
79 | case (a, s) if a == 0 && s != 0 =>
80 | val anySel = sortedSels.head
81 | val calcMid = Strategy.calcMid(anySel.price, anySel.quantity, bot.quantityPower, bot.gridSpace, bot.counterScale, bot.baseScale, Side.sell, midPrice, bot.strategy)
82 | val sl = calcMid._3 - 1
83 | set(calcMid._1, calcMid._2, calcMid._2, bot.buyGridLevels, sl)
84 |
85 | case (a, s) if a != 0 && s != 0 =>
86 | val anySel = sortedSels.head
87 | val calcMidSel = Strategy.calcMid(anySel.price, anySel.quantity, bot.quantityPower, bot.gridSpace, bot.counterScale, bot.baseScale, Side.sell, midPrice, bot.strategy)
88 | val anyBuy = sortedBuys.head
89 | val calcMidBuy = Strategy.calcMid(anyBuy.price, anyBuy.quantity, bot.quantityPower, bot.gridSpace, bot.counterScale, bot.baseScale, Side.buy, calcMidSel._1, bot.strategy)
90 | val bl = calcMidBuy._3 - 1
91 | val sl = calcMidSel._3 - 1
92 | set(calcMidSel._1, calcMidBuy._2, calcMidSel._2, bl, sl)
93 | }
94 |
95 | res ++= Strategy.seed(buyQty, calcMidPrice, bot.quantityPower, bot.counterScale, bot.baseScale, bot.pair, levels = buyLevels, gridSpace = bot.gridSpace, side = Side.buy, act = PingPong.ping, isPulledFromOtherSide = false, strategy = bot.strategy, isNoQtyCutoff = bot.isNoQtyCutoff, maxPrice = bot.maxPrice, minPrice = bot.minPrice)
96 | res ++= Strategy.seed(selQty, calcMidPrice, bot.quantityPower, bot.counterScale, bot.baseScale, bot.pair, selLevels, bot.gridSpace, Side.sell, PingPong.ping, isPulledFromOtherSide = false, bot.strategy, bot.isNoQtyCutoff, bot.maxPrice, bot.minPrice)
97 | res
98 | }
99 |
100 | def counter(order: Offer): Offer = Strategy.counter(order.quantity, order.price, bot.quantityPower, bot.counterScale, bot.baseScale, bot.pair, bot.gridSpace, order.side, PingPong.pong, bot.strategy, bot.isNoQtyCutoff, bot.maxPrice, bot.minPrice)
101 |
102 | def grow(sortedBuys: Seq[Offer], sortedSels: Seq[Offer], side: Side): Seq[Offer] = {
103 | def matcher(side: Side): Seq[Offer] = {
104 | val preSeed = getRuntimeSeedStart(sortedBuys, sortedSels, side)
105 | Strategy.seed(preSeed._2, preSeed._3, bot.quantityPower, bot.counterScale, bot.baseScale, bot.pair, preSeed._1, bot.gridSpace, side, PingPong.ping, preSeed._4, bot.strategy, bot.isNoQtyCutoff, bot.maxPrice, bot.minPrice)
106 | }
107 |
108 | side match {
109 | case Side.buy => matcher(Side.buy)
110 | case Side.sell => matcher(Side.sell)
111 | case _ => Seq.empty[Offer] // unsupported operation at runtime
112 | }
113 | }
114 |
115 | def trim(sortedBuys: Seq[Offer], sortedSels: Seq[Offer], side : Side): Seq[Offer] = {
116 | val (orders, limit) = if (side == Side.buy) (sortedBuys, bot.buyGridLevels) else (sortedSels, bot.sellGridLevels)
117 | orders.slice(limit, orders.size)
118 | }
119 |
120 | def getRuntimeSeedStart(sortedBuys: Seq[Offer], sortedSels: Seq[Offer], side: Side): (Int, BigDecimal, BigDecimal, Boolean) = {
121 | var isPulledFromOtherSide: Boolean = false
122 | var levels: Int = 0
123 |
124 | val q0p0 = side match {
125 | case Side.buy =>
126 | sortedBuys.size match {
127 | case 0 =>
128 | val topSel = sortedSels.head
129 | levels = bot.buyGridLevels
130 | isPulledFromOtherSide = true
131 | (topSel.quantity, topSel.price)
132 | case _ =>
133 | val lowBuy = sortedBuys.last
134 | levels = bot.buyGridLevels - sortedBuys.size
135 | (lowBuy.quantity, lowBuy.price)
136 | }
137 | case Side.sell =>
138 | sortedSels.size match {
139 | case 0 =>
140 | val topBuy = sortedBuys.head
141 | levels = bot.sellGridLevels
142 | isPulledFromOtherSide = true
143 | (topBuy.quantity, topBuy.price)
144 | case _ =>
145 | val lowSel = sortedSels.last
146 | levels = bot.sellGridLevels - sortedSels.size
147 | (lowSel.quantity, lowSel.price)
148 | }
149 | case _ => (BigDecimal("0"), BigDecimal("0"))
150 | }
151 | (levels, q0p0._1, q0p0._2, isPulledFromOtherSide)
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/okex/OkexRestRequest.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.okex
2 |
3 | import me.mbcu.integrated.mmm.ops.common.Side.Side
4 | import me.mbcu.integrated.mmm.ops.common.{AbsRestRequest, Credentials, Offer}
5 | import me.mbcu.integrated.mmm.ops.okex.OkexChannels.OkexChannels
6 | import me.mbcu.integrated.mmm.ops.okex.OkexEvents.OkexEvents
7 | import me.mbcu.integrated.mmm.ops.okex.OkexStatus.OkexStatus
8 | import play.api.libs.functional.syntax._
9 | import play.api.libs.json._
10 |
11 |
12 | object OkexEvents extends Enumeration {
13 | type OkexEvents = Value
14 | val addChannel, removeChannel, login, ping, pong = Value
15 |
16 | implicit val read = Reads.enumNameReads(OkexEvents)
17 | implicit val write = Writes.enumNameWrites
18 |
19 | def withNameOpt(s: String): Option[Value] = values.find(_.toString == s)
20 | }
21 |
22 | object OkexChannels extends Enumeration {
23 | type OkexChannels = Value
24 | val ok_spot_orderinfo, ok_spot_order, ok_spot_cancel_order, login = Value
25 |
26 | implicit val read = Reads.enumNameReads(OkexChannels)
27 | implicit val write = Writes.enumNameWrites
28 |
29 | def withNameOpt(s: String): Option[Value] = values.find(_.toString == s)
30 | }
31 |
32 | object OkexRestRequest extends AbsRestRequest{
33 |
34 | implicit val jsonFormat = Json.format[OkexRestRequest]
35 |
36 | object Implicits {
37 | implicit val writes = new Writes[OkexRestRequest] {
38 | def writes(r: OkexRestRequest): JsValue = Json.obj(
39 | "event" -> r.event,
40 | "channel" -> r.channel,
41 | "parameters" -> r.parameters
42 | )
43 | }
44 |
45 | implicit val reads: Reads[OkexRestRequest] = (
46 | (JsPath \ "event").read[OkexEvents] and
47 | (JsPath \ "channel").readNullable[OkexChannels] and
48 | (JsPath \ "parameters").readNullable[OkexParameters]
49 | ) (OkexRestRequest.apply _)
50 |
51 | }
52 |
53 | def login(apiKey : String, secret : String) : OkexRestRequest = {
54 | val params = OkexParameters(None, apiKey, None, None, None, None, None)
55 | val signed = sign(secret, params)
56 | val p = OkexParameters(Some(signed), apiKey, None, None, None, None, None)
57 | OkexRestRequest(OkexEvents.login, None, Some(p))
58 | }
59 |
60 | def newOrder(credentials: Credentials, offer: Offer): OkexRestRequest = newOrder(credentials.pKey, credentials.signature, offer.symbol, offer.side, offer.quantity, offer.price)
61 |
62 |
63 | def newOrder(apiKey : String, secret : String, symbol : String, `type` : Side, amount : BigDecimal, price : BigDecimal): OkexRestRequest = {
64 | val params = OkexParameters(None, apiKey, Some(symbol), None, Some(`type`), Some(price), Some(amount))
65 | val signed = sign(secret, params)
66 | val p = OkexParameters(Some(signed), apiKey, Some(symbol), None, Some(`type`), Some(price), Some(amount))
67 | OkexRestRequest(OkexEvents.addChannel, Some(OkexChannels.ok_spot_order), Some(p))
68 | }
69 |
70 | def restCancelOrder(credentials: Credentials, symbol: String, orderId : String) : Map[String, String] =
71 | restCancelOrder(credentials.pKey, credentials.signature, symbol, orderId)
72 |
73 | def restCancelOrder(apiKey: String, secret : String, symbol: String, orderId : String) : Map[String, String] = {
74 | val params = OkexParameters(None, apiKey, Some(symbol), Some(orderId), None, None, None, None, None, None)
75 | val signed = sign(secret, params)
76 | val p = OkexParameters(Some(signed), apiKey, Some(symbol), Some(orderId), None, None, None, None, None, None )
77 | Json.toJson(p).as[JsObject].value.map(r => r._1 -> r._2.toString().replace("\"", "")).toMap
78 | }
79 |
80 | def cancelOrder(apiKey: String, secret : String, symbol : String, orderId : String) : OkexRestRequest = {
81 | val p = min3(apiKey, secret, symbol, orderId)
82 | OkexRestRequest(OkexEvents.addChannel, Some(OkexChannels.ok_spot_cancel_order), Some(p))
83 | }
84 |
85 | def infoOrder(apiKey : String, secret : String, symbol : String, orderId : String) : OkexRestRequest = {
86 | val p = min3(apiKey, secret, symbol, orderId)
87 | OkexRestRequest(OkexEvents.addChannel, Some(OkexChannels.ok_spot_orderinfo), Some(p))
88 | }
89 |
90 | def restNewOrder(credentials: Credentials, symbol: String, `type`: Side, price: BigDecimal, amount: BigDecimal) : Map[String, String] =
91 | restNewOrder(credentials.pKey, credentials.signature, symbol, `type`, price, amount)
92 |
93 | def restNewOrder(apiKey: String, secret: String, symbol: String, `type`: Side, price: BigDecimal, amount: BigDecimal) : Map[String, String] = {
94 | val params = OkexParameters(None, apiKey, Some(symbol), None, Some(`type`), Some(price), Some(amount), None, None, None)
95 | val signed = sign(secret, params)
96 | val p = OkexParameters(Some(signed), apiKey, Some(symbol), None, Some(`type`), Some(price), Some(amount), None, None, None)
97 | Json.toJson(p).as[JsObject].value.map(r => r._1 -> r._2.toString().replace("\"", "")).toMap
98 | }
99 |
100 | def restInfoOrder(credentials: Credentials, symbol: String, orderId: String) : Map[String, String] =
101 | restInfoOrder(credentials.pKey, credentials.signature, symbol : String, orderId: String)
102 |
103 | def restInfoOrder(apiKey : String, secret: String, symbol : String, orderId: String) : Map[String, String] = {
104 | val params = OkexParameters(None, apiKey, Some(symbol), Some(orderId), None, None, None, None, None, None)
105 | val signed = sign(secret, params)
106 | val p = OkexParameters(Some(signed), apiKey, Some(symbol), Some(orderId), None, None, None, None, None, None)
107 | Json.toJson(p).as[JsObject].value.map(r => r._1 -> r._2.toString().replace("\"", "")).toMap
108 | }
109 |
110 | def restOwnTrades(credentials: Credentials, symbol: String, status: OkexStatus, currentPage : Int) : Map[String, String] =
111 | restOwnTrades(credentials.pKey, credentials.signature, symbol, status, currentPage )
112 |
113 | def restOwnTrades(apiKey:String, secret:String, symbol:String, status: OkexStatus, currentPage : Int) : Map[String, String] = {
114 | val pageLength = 200
115 | val params = OkexParameters(None, apiKey, Some(symbol), None, None, None, None, Some(status), Some(currentPage), Some(pageLength))
116 | val signed = sign(secret, params)
117 | val p = OkexParameters(Some(signed), apiKey, Some(symbol), None, None, None, None, Some(status), Some(currentPage), Some(pageLength))
118 | Json.toJson(p).as[JsObject].value.map(r => r._1 -> r._2.toString().replace("\"", "")).toMap
119 | }
120 |
121 | def restTicker(symbol: String) : Map[String, String] = Map("symbol" -> symbol)
122 |
123 | def ping() : OkexRestRequest = OkexRestRequest(OkexEvents.ping, None, None)
124 |
125 |
126 | private def min3(apiKey: String, secret: String, symbol: String, orderId: String ) : OkexParameters = {
127 | val params = OkexParameters(None, apiKey, Some(symbol), Some(orderId), None, None, None)
128 | OkexParameters(Some(sign(secret, params)), apiKey, Some(symbol), Some(orderId), None, None, None)
129 | }
130 |
131 |
132 | val sign : (String, OkexParameters) => String = (secret, params) => {
133 | val a = Json.toJson(params).as[JsObject]
134 | val b = a.fields.sortBy(_._1).map(c => s"${c._1.toString}=${c._2}").reduce((l, r) => s"$l&$r")
135 | val d = s"""$b&secret_key=$secret""".replace("\"", "")
136 | md5(secret, d)
137 | }
138 | }
139 |
140 | case class OkexRestRequest(event : OkexEvents, channel : Option[OkexChannels], parameters : Option[OkexParameters])
141 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/btcalpha/BtcalphaActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.btcalpha
2 |
3 | import akka.dispatch.ExecutionContexts.global
4 | import akka.stream.ActorMaterializer
5 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode
6 | import me.mbcu.integrated.mmm.ops.btcalpha.BtcalphaRequest.BtcalphaStatus.BtcalphaStatus
7 | import me.mbcu.integrated.mmm.ops.btcalpha.BtcalphaRequest.{BtcalphaParams, BtcalphaStatus}
8 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
9 | import me.mbcu.integrated.mmm.ops.common.Side.Side
10 | import me.mbcu.integrated.mmm.ops.common.{AbsRestActor, Offer, Status}
11 | import me.mbcu.scala.MyLogging
12 | import play.api.libs.json.{JsValue, Json}
13 | import play.api.libs.ws.ahc.StandaloneAhcWSClient
14 |
15 | import scala.concurrent.ExecutionContextExecutor
16 | import scala.concurrent.duration._
17 | import scala.language.postfixOps
18 | import scala.util.{Failure, Success, Try}
19 |
20 | object BtcalphaActor {
21 |
22 | def parseTicker(js: JsValue): BigDecimal = BigDecimal((js.as[List[JsValue]].head \ "close").as[Double])
23 |
24 | def toOffer(js: JsValue): Offer =
25 | new Offer(
26 | (js \ "id").as[Long].toString,
27 | (js \ "pair").as[String],
28 | (js \ "type").as[Side],
29 | (js \ "status").as[BtcalphaStatus] match {
30 | case BtcalphaStatus.done => Status.filled
31 | case BtcalphaStatus.active => Status.active
32 | case BtcalphaStatus.cancelled => Status.cancelled
33 | case _ => Status.cancelled
34 | },
35 | createdAt = -1,
36 | None,
37 | (js \ "amount").as[BigDecimal], // this is the remaining amount not original amount
38 | (js \ "price").as[BigDecimal],
39 | None
40 | )
41 |
42 | /*
43 | [
44 | {
45 | "id": 28004855,
46 | "type": "buy",
47 | "pair": "NOAH_ETH",
48 | "price": "0.00000100",
49 | "amount": "1000.00000000",
50 | "status": 1
51 | }
52 | ]
53 | */
54 |
55 | }
56 |
57 | class BtcalphaActor extends AbsRestActor with MyLogging {
58 | import play.api.libs.ws.DefaultBodyReadables._
59 | import play.api.libs.ws.DefaultBodyWritables._
60 |
61 | private implicit val materializer = ActorMaterializer()
62 | private implicit val ec: ExecutionContextExecutor = global
63 | private var ws = StandaloneAhcWSClient()
64 |
65 | override def start(): Unit = setOp(Some(sender()))
66 |
67 | override def sendRequest(r: AbsRestActor.SendRest): Unit = {
68 | r match {
69 |
70 | case a: GetTickerStartPrice => httpGet(a, BtcalphaRequest.getTickers(a.bot.pair)) // https://btc-alpha.com/api/charts/BTC_USD/1/chart/?format=json&limit=1
71 |
72 | case a: NewOrder => httpPost(a, BtcalphaRequest.newOrder(a.bot.credentials, a.bot.pair, a.offer.side, a.offer.price, a.offer.quantity))
73 |
74 | case a: CancelOrder => httpPost(a, BtcalphaRequest.cancelOrder(a.bot.credentials, a.offer.id))
75 |
76 | case a: GetActiveOrders => httpGet(a, BtcalphaRequest.getOrders(a.bot.credentials, a.bot.pair, BtcalphaStatus.active))
77 |
78 | case a: GetOwnPastTrades => httpGet(a, BtcalphaRequest.getOrders(a.bot.credentials, a.bot.pair, BtcalphaStatus.done))
79 |
80 | case a: GetOrderInfo => httpGet(a, BtcalphaRequest.getOrderInfo(a.bot.credentials, a.id))
81 |
82 | }
83 |
84 | def httpPost(a: SendRest, r: BtcalphaParams): Unit = {
85 | ws.url(r.url)
86 | .addHttpHeaders("Content-Type" -> "application/x-www-form-urlencoded")
87 | .addHttpHeaders("X-KEY" -> a.bot.credentials.pKey)
88 | .addHttpHeaders("X-SIGN" -> r.sign)
89 | .addHttpHeaders("X-NONCE" -> r.nonce)
90 | .withRequestTimeout(requestTimeoutSec seconds)
91 | .post(r.params)
92 | .map(response => parse(a, r.params, response.body[String]))
93 | .recover{
94 | case e: Exception => errorRetry(a, 0, e.getMessage, shouldEmail = false)
95 | }
96 | }
97 |
98 | def httpGet(a: SendRest, r: BtcalphaParams): Unit = {
99 | ws.url(r.url)
100 | .addHttpHeaders("X-KEY" -> a.bot.credentials.pKey)
101 | .addHttpHeaders("X-SIGN" -> r.sign)
102 | .addHttpHeaders("X-NONCE" -> r.nonce)
103 | .withRequestTimeout(requestTimeoutSec seconds)
104 | .get()
105 | .map(response => parse(a, r.params, response.body[String]))
106 | .recover{
107 | case e: Exception => errorRetry(a, 0, e.getMessage, shouldEmail = false)
108 | }
109 | }
110 | }
111 |
112 | def parse(a: AbsRestActor.SendRest, request: String, raw: String): Unit = {
113 | info(logResponse(a, raw))
114 | val arriveMs = System.currentTimeMillis()
115 | val book = a.book
116 |
117 | a match {
118 | case p: NewOrder => op foreach (_ ! GotNewOrderId(s"prov-${System.currentTimeMillis()}", p.as, arriveMs, p))
119 | case _ =>
120 | }
121 |
122 | val x = Try(Json parse raw)
123 | x match {
124 | case Success(js) =>
125 |
126 | val detail = (js \ "detail").asOpt[String]
127 | if (detail.isDefined) {
128 | detail.get match {
129 | case m if m contains "Wrong Nonce" => errorRetry(a, 0, m, false)
130 | case m if m contains "Incorrect authentication credentials" => errorShutdown(ShutdownCode.fatal, 0, s"$request $m")
131 | case _ => errorIgnore(-1, raw)
132 | }
133 | }
134 | else if (raw contains "Order already done"){
135 | /*
136 | {
137 | "order": [
138 | "Order already done"
139 | ]
140 | }
141 | */
142 | book ! GotOrderCancelled(a.as, arriveMs,a.asInstanceOf[CancelOrder])
143 | errorIgnore(0, "order has gone")
144 | }
145 | else if (raw contains "Out of balance"){
146 | errorIgnore(0, s"Not enough balance ${a.bot.pair}")
147 | }
148 | else {
149 |
150 | a match {
151 |
152 | case a: GetTickerStartPrice => book ! GotStartPrice(Some(BtcalphaActor.parseTicker(js)), arriveMs, a)
153 |
154 | case a: GetOwnPastTrades =>
155 | val orders = js.as[List[JsValue]].map(BtcalphaActor.toOffer)
156 | val price = if (orders.nonEmpty) Some(orders.head.price) else None
157 | book ! GotStartPrice(price, arriveMs, a)
158 |
159 | case a: GetActiveOrders =>
160 | val activeOrders = js.as[List[JsValue]].map(BtcalphaActor.toOffer)
161 | book ! GotActiveOrders(activeOrders, a.page, nextPage = false, arriveMs, a)
162 |
163 | case a: GetOrderInfo => book ! GotOrderInfo(BtcalphaActor.toOffer(js), arriveMs, a)
164 |
165 | case a: CancelOrder => book ! GotOrderCancelled(a.as, arriveMs, a)
166 |
167 | case a: NewOrder =>
168 | val id = (js \ "oid").as[Long].toString
169 | val serverMs = ((js \ "date").as[Double] * 1000).toLong
170 | book ! GotProvisionalOffer(id, serverMs, a.offer)
171 | // op foreach (_ ! GotNewOrderId(id, a.as, arriveMs, a))
172 |
173 | case _ => error(s"Unknown BtcalphaActor#parse : $raw")
174 | }
175 | }
176 |
177 | case Failure(r) =>
178 | raw match {
179 | case m if m contains "html" => errorRetry(a, 0, raw)
180 | case m if m contains "Ddos" => errorRetry(a, 0, m, shouldEmail = false)
181 | case m if m.isEmpty => errorRetry(a, 0, m, shouldEmail = false)
182 | case _ => errorIgnore(0, s"Unknown BtcalphaActor#parse : $raw")
183 | }
184 | }
185 |
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/WsActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import java.util
4 |
5 | import akka.actor.{Actor, ActorRef, Cancellable, Props}
6 | import akka.dispatch.ExecutionContexts.global
7 | import com.neovisionaries.ws.client
8 | import com.neovisionaries.ws.client.{WebSocketFactory, WebSocketListener, WebSocketState}
9 | import me.mbcu.integrated.mmm.actors.WsActor._
10 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode
11 | import me.mbcu.scala.MyLogging
12 | import play.api.libs.json.{JsValue, Json}
13 |
14 | import scala.concurrent.ExecutionContextExecutor
15 | import scala.language.postfixOps
16 | import scala.util.{Failure, Success, Try}
17 |
18 | object WsActor {
19 | def props(): Props = Props(new WsActor())
20 |
21 | object WsConnected
22 |
23 | object WsDisconnected
24 |
25 | object WsRequestClient
26 |
27 | case class WsConnect(url: String)
28 |
29 | case class SendJs(jsValue: JsValue)
30 |
31 | case class WsGotText(text: String)
32 |
33 | case class WsError(msg: String)
34 |
35 | }
36 |
37 | class WsActor() extends Actor with MyLogging{
38 | private implicit val ec: ExecutionContextExecutor = global
39 | private var ws : Option[com.neovisionaries.ws.client.WebSocket] = None
40 | private var main: Option[ActorRef] = None
41 | private var disCan: Option[Cancellable] = None
42 | private val timeout: Int = 5000
43 |
44 | override def receive: Receive = {
45 |
46 | case "start" =>
47 | main = Some(sender)
48 | main foreach(_ ! WsRequestClient)
49 |
50 | case WsConnect(url) =>
51 | ws match {
52 | case None =>
53 | val factory = new WebSocketFactory
54 | factory.setConnectionTimeout(timeout)
55 | val websocket = factory.createSocket(url)
56 | websocket.addListener(ScalaWebSocketListener)
57 | ws = Some(websocket)
58 | Try(websocket.connect) match { // failure at connecting will result in exception. Listeners only sense errors when ws is alive
59 | case Success(w) => info("WsActor got connection. See onConnected for handler")
60 | case Failure(e) => // timeout error
61 | error(e.getMessage)
62 | self ! WsRequestClient
63 | }
64 |
65 | case _ => // don't init a client
66 |
67 | }
68 |
69 | case SendJs(jsValue) =>
70 | ws match {
71 | case Some(w) =>
72 | if (w.isOpen){
73 | ws.foreach(_.sendText(Json.stringify(jsValue)))
74 | } else {
75 | error("WsActor# websocket is not open")
76 | }
77 | case _ =>
78 | }
79 |
80 | case WsError(msg) =>
81 | error(msg)
82 | ws foreach(_.disconnect("server down")) // One client can only be disconnected once
83 |
84 | case WsDisconnected =>
85 | info("WsActor client disconnected")
86 | self ! WsRequestClient
87 |
88 | case WsRequestClient =>
89 | ws foreach(_.clearListeners())
90 | ws = None
91 | main foreach(_ ! WsRequestClient)
92 |
93 | }
94 |
95 | object ScalaWebSocketListener extends WebSocketListener {
96 | val fatal = Some(ShutdownCode.fatal.id)
97 | val recover = Some(ShutdownCode.recover.id)
98 |
99 | override def onConnected(websocket: client.WebSocket, headers: util.Map[String, util.List[String]]): Unit = main foreach (_ ! WsConnected)
100 |
101 | def onTextMessage(websocket: com.neovisionaries.ws.client.WebSocket, data: String) : Unit =
102 | main foreach (_ ! WsGotText(data))
103 |
104 | override def onStateChanged(websocket: client.WebSocket, newState: WebSocketState): Unit = {}
105 | override def handleCallbackError(x$1: com.neovisionaries.ws.client.WebSocket, t: Throwable): Unit =
106 | self ! WsError(s"WsActor#handleCallbackError: ${t.getMessage}")
107 |
108 | override def onBinaryFrame(x$1: com.neovisionaries.ws.client.WebSocket, f: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
109 | override def onBinaryMessage(x$1: com.neovisionaries.ws.client.WebSocket, d: Array[Byte]): Unit = {}
110 | override def onCloseFrame(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
111 | override def onConnectError(x$1: com.neovisionaries.ws.client.WebSocket, e: com.neovisionaries.ws.client.WebSocketException): Unit =
112 | self ! WsError(s"WsActor#onConnectError: ${e.getMessage}")
113 |
114 |
115 | override def onContinuationFrame(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
116 | override def onDisconnected(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame, x$3: com.neovisionaries.ws.client.WebSocketFrame, closedByServer: Boolean): Unit =
117 | self ! WsDisconnected
118 |
119 | override def onError(x$1: com.neovisionaries.ws.client.WebSocket, e: com.neovisionaries.ws.client.WebSocketException): Unit =
120 | self ! WsError(s"WsActor#onError: ${e.getMessage}")
121 |
122 |
123 | override def onFrame(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
124 | override def onFrameError(x$1: com.neovisionaries.ws.client.WebSocket, e: com.neovisionaries.ws.client.WebSocketException, x$3: com.neovisionaries.ws.client.WebSocketFrame): Unit =
125 | self ! WsError(s"WsActor#onFrameError: ${e.getMessage}")
126 |
127 | override def onFrameSent(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
128 | override def onFrameUnsent(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
129 | override def onMessageDecompressionError(x$1: com.neovisionaries.ws.client.WebSocket, e: com.neovisionaries.ws.client.WebSocketException, x$3: Array[Byte]): Unit =
130 | self ! WsError(s"WsActor#onMessageDecompressionError: ${e.getMessage}")
131 |
132 | override def onMessageError(x$1: com.neovisionaries.ws.client.WebSocket, e: com.neovisionaries.ws.client.WebSocketException, x$3: java.util.List[com.neovisionaries.ws.client.WebSocketFrame]): Unit =
133 | self ! WsError(s"WsActor#onMessageError: ${e.getMessage}")
134 |
135 | override def onPingFrame(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
136 | override def onPongFrame(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
137 | override def onSendError(x$1: com.neovisionaries.ws.client.WebSocket, e: com.neovisionaries.ws.client.WebSocketException, x$3: com.neovisionaries.ws.client.WebSocketFrame): Unit =
138 | self ! WsError(s"WsActor#onSendError: ${e.getMessage}")
139 |
140 | override def onSendingFrame(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
141 | override def onSendingHandshake(x$1: com.neovisionaries.ws.client.WebSocket, x$2: String, x$3: java.util.List[Array[String]]): Unit = {}
142 | override def onTextFrame(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.WebSocketFrame): Unit = {}
143 | override def onTextMessageError(x$1: com.neovisionaries.ws.client.WebSocket, e: com.neovisionaries.ws.client.WebSocketException, x$3: Array[Byte]): Unit =
144 | self ! WsError(s"WsActor#onTextMessageError: ${e.getMessage}")
145 |
146 | override def onThreadCreated(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.ThreadType, x$3: Thread): Unit = {}
147 | override def onThreadStarted(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.ThreadType, x$3: Thread): Unit = {}
148 | override def onThreadStopping(x$1: com.neovisionaries.ws.client.WebSocket, x$2: com.neovisionaries.ws.client.ThreadType, x$3: Thread): Unit = {}
149 | override def onUnexpectedError(x$1: com.neovisionaries.ws.client.WebSocket, e: com.neovisionaries.ws.client.WebSocketException): Unit =
150 | self ! WsError(s"WsActor#onUnexpectedError: ${e.getMessage}")
151 |
152 | }
153 |
154 |
155 |
156 | }
157 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/ops/livecoin/LivecoinActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.ops.livecoin
2 |
3 | import akka.dispatch.ExecutionContexts.global
4 | import akka.stream.ActorMaterializer
5 | import me.mbcu.integrated.mmm.ops.Definitions.ShutdownCode
6 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
7 | import me.mbcu.integrated.mmm.ops.common.{AbsRestActor, Offer, Side, Status}
8 | import me.mbcu.integrated.mmm.ops.livecoin.LivecoinRequest.LivecoinState.LivecoinState
9 | import me.mbcu.integrated.mmm.ops.livecoin.LivecoinRequest.{LivecoinParams, LivecoinState}
10 | import me.mbcu.scala.MyLogging
11 | import play.api.libs.json.{JsValue, Json}
12 | import play.api.libs.ws.ahc.StandaloneAhcWSClient
13 |
14 | import scala.concurrent.ExecutionContextExecutor
15 | import scala.concurrent.duration._
16 | import scala.language.postfixOps
17 | import scala.util.{Failure, Success, Try}
18 |
19 | object LivecoinActor {
20 |
21 | def toOffer(data: JsValue):Offer = {
22 | val state = (data \ "orderStatus").as[LivecoinState]
23 |
24 | val status = state match {
25 | case LivecoinState.CLOSED => Status.expired // returns EXECUTED and CANCELLED, only used in request
26 | case LivecoinState.EXECUTED => Status.filled
27 | case LivecoinState.OPEN => Status.active
28 | case LivecoinState.CANCELLED => Status.cancelled
29 | case LivecoinState.NOT_CANCELLED => Status.active
30 | case LivecoinState.PARTIALLY => Status.partiallyFilled // used in request
31 | case LivecoinState.PARTIALLY_FILLED => Status.partiallyFilled // used in request
32 | case LivecoinState.PARTIALLY_FILLED_AND_CANCELLED => Status.partiallyFilled
33 | case LivecoinState.ALL => Status.active
34 | case _ => Status.cancelled
35 | }
36 | val quantity = (data \ "quantity").as[BigDecimal]
37 | new Offer(
38 | (data \ "id").as[Long].toString,
39 | (data \ "currencyPair").as[String],
40 | if ((data \ "type").as[String] contains "LIMIT_BUY") Side.buy else Side.sell,
41 | status,
42 | (data \ "issueTime").as[Long],
43 | Some((data \ "lastModificationTime").as[Long]),
44 | quantity,
45 | (data \ "price").as[BigDecimal],
46 | Some(quantity - (data \ "remainingQuantity").as[BigDecimal])
47 | )
48 |
49 | /*
50 | "id": 17551577701,
51 | "currencyPair": "ETH/BTC",
52 | "goodUntilTime": 0,
53 | "type": "LIMIT_SELL",
54 | "orderStatus": "OPEN",
55 | "issueTime": 1535474938522,
56 | "price": 0.05,
57 | "quantity": 0.01,
58 | "remainingQuantity": 0.01,
59 | "commissionByTrade": 0,
60 | "bonusByTrade": 0,
61 | "bonusRate": 0,
62 | "commissionRate": 0.0018,
63 | "lastModificationTime": 1535474938522
64 | */
65 | }
66 |
67 | def getUncounteredOrders(list: List[JsValue]): Seq[Offer] =
68 | list.map(LivecoinActor.toOffer)
69 | .filter(_.status == Status.filled)
70 | .sortWith((p,q) => p.updatedAt.getOrElse(p.createdAt) < q.updatedAt.getOrElse(q.createdAt))
71 |
72 | }
73 |
74 | class LivecoinActor extends AbsRestActor with MyLogging{
75 | import play.api.libs.ws.DefaultBodyReadables._
76 | import play.api.libs.ws.DefaultBodyWritables._
77 |
78 | private implicit val materializer = ActorMaterializer()
79 | private implicit val ec: ExecutionContextExecutor = global
80 | private var ws = StandaloneAhcWSClient()
81 | override def start(): Unit = setOp(Some(sender()))
82 |
83 | override def sendRequest(r: AbsRestActor.SendRest): Unit = {
84 |
85 | r match {
86 |
87 | case a: GetTickerStartPrice => httpGet(a, LivecoinRequest.getTicker(a.bot.pair))
88 |
89 | case a: GetActiveOrders => httpGet(a, LivecoinRequest.getActiveOrders(a.bot.credentials, a.bot.pair, LivecoinState.OPEN, (a.page - 1) * 100 ))
90 |
91 | case a: GetFilledOrders => httpGet(a, LivecoinRequest.getOwnTrades(a.bot.credentials, a.bot.pair, LivecoinState.CLOSED, a.lastCounterId.toLong))
92 |
93 | case a: NewOrder => httpPost(a, LivecoinRequest.newOrder(a.bot.credentials, a.offer.symbol, a.offer.side, a.offer.price, a.offer.quantity))
94 |
95 | case a: CancelOrder => httpPost(a, LivecoinRequest.cancelOrder(a.bot.credentials, a.offer.symbol, a.offer.id))
96 | }
97 |
98 | def httpPost(a: SendRest, r: LivecoinParams): Unit = {
99 | ws.url(r.url)
100 | .addHttpHeaders("Content-Type" -> "application/x-www-form-urlencoded")
101 | .addHttpHeaders("Sign" -> r.sign)
102 | .addHttpHeaders("API-key" -> a.bot.credentials.pKey)
103 | .withRequestTimeout(requestTimeoutSec seconds)
104 | .post(r.params)
105 | .map(response => parse(a, r.params, response.body[String]))
106 | .recover{
107 | case e: Exception => errorRetry(a, 0, e.getMessage, shouldEmail = false)
108 | }
109 | }
110 |
111 | def httpGet(a: SendRest, r: LivecoinParams): Unit = {
112 | ws.url(r.url)
113 | .addHttpHeaders("Content-Type" -> "application/x-www-form-urlencoded")
114 | .addHttpHeaders("Sign" -> r.sign)
115 | .addHttpHeaders("API-key" -> a.bot.credentials.pKey)
116 | .withRequestTimeout(requestTimeoutSec seconds)
117 | .get()
118 | .map(response => parse(a, r.params, response.body[String]))
119 | .recover{
120 | case e: Exception => errorRetry(a, 0, e.getMessage, shouldEmail = false)
121 | }
122 | }
123 | }
124 |
125 | def parse(a: AbsRestActor.SendRest, request: String, raw: String): Unit = {
126 | info(logResponse(a, raw))
127 | val arriveMs = System.currentTimeMillis()
128 |
129 | a match {
130 | case p: NewOrder => op foreach (_ ! GotNewOrderId("unused", p.as, arriveMs, p))
131 | case _ =>
132 | }
133 |
134 | val x = Try(Json parse raw)
135 | x match {
136 | case Success(js) =>
137 | val success = js \ "success"
138 | if (success.isDefined) {
139 | if (!success.as[Boolean]) {
140 | if (raw.contains("invalid signature")) errorIgnore(-1, "invalid signature")
141 | else if (raw contains "incorrect key" ) errorShutdown(ShutdownCode.fatal, -1, "wrong API key: ${a.bot.pair}")
142 | else if (raw contains "Minimal amount is") errorIgnore(-1, "minimal amount too low: ${a.bot.pair}")
143 | else if (raw contains "insufficient funds") errorIgnore(-1, s"insufficient funds: ${a.bot.pair}")
144 | else if (raw contains "Cannot get a connection, pool error Timeout waiting for idle object" ) errorRetry(a, 0, raw, shouldEmail = false)
145 | else if (raw contains "Service is under maintenance") errorRetry(a, 0, raw, shouldEmail = false)
146 | else errorIgnore(-1, raw)
147 | }
148 | }
149 | else {
150 | a match {
151 |
152 | case a: GetTickerStartPrice =>
153 | val lastPrice = (js \ "last").as[BigDecimal]
154 | a.book ! GotTickerStartPrice(Some(lastPrice), arriveMs, a)
155 |
156 | case a: GetFilledOrders =>
157 | val offers = LivecoinActor.getUncounteredOrders((js \ "data").as[List[JsValue]])
158 | val lastCounterId = if (offers.nonEmpty) Some((offers.last.updatedAt.getOrElse(offers.last.createdAt) + 1L).toString) else None
159 | a.book ! GotUncounteredOrders(offers, lastCounterId, isSortedFromOldest = true, arriveMs, a)
160 |
161 | case a: GetActiveOrders =>
162 | val res = (js \ "data").as[List[JsValue]].map(LivecoinActor.toOffer)
163 | // val isNextPage = (js \ "totalRows").as[Int] > (js \ "endRow").as[Int] // broken
164 | a.book ! GotActiveOrders(res, a.page, nextPage = false , arriveMs, a)
165 | }
166 |
167 | }
168 |
169 | case Failure(e) =>
170 | raw match {
171 | case m if m contains "html" => errorRetry(a, 0, raw, shouldEmail = false)
172 | case m if m.isEmpty => errorRetry(a, 0, m, shouldEmail = false)
173 | case _ => errorIgnore(0, s"Unknown LivecoinActor#parse : $raw")
174 | }
175 | }
176 | }
177 |
178 | }
179 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/OrderWsActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import akka.actor.{ActorRef, Cancellable}
4 | import akka.dispatch.ExecutionContexts.global
5 | import me.mbcu.integrated.mmm.actors.OpWsActor.QueueWs
6 | import me.mbcu.integrated.mmm.actors.WsActor.WsRequestClient
7 | import me.mbcu.integrated.mmm.ops.Definitions.{ErrorShutdown, Settings, ShutdownCode}
8 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As
9 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As.As
10 | import me.mbcu.integrated.mmm.ops.common.AbsWsParser._
11 | import me.mbcu.integrated.mmm.ops.common.Side.Side
12 | import me.mbcu.integrated.mmm.ops.common._
13 | import me.mbcu.scala.MyLogging
14 |
15 | import scala.collection.concurrent.TrieMap
16 | import scala.concurrent.ExecutionContextExecutor
17 | import scala.concurrent.duration._
18 | import scala.language.postfixOps
19 |
20 | class OrderWsActor(bot: Bot, exchange: AbsExchange, req: AbsWsRequest) extends AbsOrder(bot) with MyLogging{
21 | private implicit val ec: ExecutionContextExecutor = global
22 | private val wsEx: AbsWsExchange = exchange.asInstanceOf[AbsWsExchange]
23 | var sels: TrieMap[String, Offer] = TrieMap.empty[String, Offer]
24 | var buys: TrieMap[String, Offer] = TrieMap.empty[String, Offer]
25 | var sortedSels: scala.collection.immutable.Seq[Offer] = scala.collection.immutable.Seq.empty[Offer]
26 | var sortedBuys: scala.collection.immutable.Seq[Offer] = scala.collection.immutable.Seq.empty[Offer]
27 | var selTrans : TrieMap[String, Offer] = TrieMap.empty[String, Offer]
28 | var buyTrans : TrieMap[String, Offer] = TrieMap.empty[String, Offer]
29 | var pendings : TrieMap[String, SendWs] = TrieMap.empty[String, SendWs]
30 | var isStartingPrice: Boolean = true
31 |
32 | var logCancellable: Option[Cancellable] = None
33 | private var op: Option[ActorRef] = None
34 |
35 | override def receive: Receive = {
36 |
37 | case "start" =>
38 | op = Some(sender())
39 | logCancellable = Some(context.system.scheduler.schedule(15 second, Settings.intervalLogSeconds seconds, self, message="log"))
40 |
41 | case "log" => info(Offer.dump(bot, sortedBuys, sortedSels))
42 |
43 | case WsRequestClient => // stop scheduler
44 |
45 | case GotSubscribe(requestId) => queue(pendings.values.toSeq)
46 |
47 | case GotActiveOrdersWs(offers, requestId) =>
48 |
49 | if (isStartingPrice) {
50 | addSort(offers)
51 |
52 | bot.seed match {
53 | case a if a.equals(StartMethods.lastTicker.toString) =>
54 | cancelOrders((buys ++ sels).values.toSeq, As.ClearOpenOrders)
55 | unlockInitSeed()
56 |
57 | case a if a.equals(StartMethods.cont.toString) => isStartingPrice = false
58 |
59 | case a if a.equals(StartMethods.lastOwn.toString) => op.foreach(_ ! ErrorShutdown(ShutdownCode.fatal, -1, "lastOwn is not supported"))
60 |
61 | case _ =>
62 | cancelOrders((buys ++ sels).values.toSeq, As.ClearOpenOrders)
63 | unlockInitSeed()
64 |
65 | }
66 | } else { // after ws gets reconnected
67 | val m = offers.map(p => p.id -> p).toMap
68 | val gones = (buys ++ sels).collect{case p @ (_: String, _: Offer) if !m.contains(p._1) => p._2}.toSeq
69 | prepareSend(gones.map(counter), As.Counter)
70 |
71 | resetAll()
72 | addSort(offers)
73 |
74 | val selGrows = grow(sortedBuys, sortedSels, Side.sell)
75 | val buyGrows = grow(sortedBuys, sortedSels, Side.buy)
76 | prepareSend(selGrows ++ buyGrows, As.Seed)
77 | trim()
78 | }
79 |
80 | case GotTickerPriceWs(price, requestId) =>
81 | queue1(req.unsubsTicker(bot.pair))
82 | seedFromStartPrice(price)
83 |
84 | case GotOfferWs(offer, requestId) =>
85 |
86 | offer.status match {
87 |
88 | case Status.filled =>
89 | removeSort(offer)
90 | prepareSend(counter(offer), As.Counter)
91 | prepareSend(grow(offer), As.Seed)
92 |
93 | case Status.partiallyFilled => addSort(offer)
94 |
95 | case Status.active =>
96 | addSort(offer)
97 | trim()
98 |
99 | case Status.cancelled =>
100 | removeSort(offer)
101 | if (isStartingPrice) {
102 | unlockInitSeed()
103 | } else {
104 | prepareSend(grow(offer), As.Seed)
105 | }
106 |
107 | case _ => info(s"OrderWsActor#GotOfferWs unhandled status ${offer.status} ${offer.id}")
108 |
109 | }
110 |
111 | case RemoveOfferWs(isRetry, orderId, side, requestId) =>
112 | if (isRetry){
113 | val retry = (buyTrans ++ selTrans)(orderId)
114 | val pending = pendings(requestId)
115 | prepareSend(retry, pending.as)
116 | }
117 | removeSort(side, orderId)
118 | removePending(requestId)
119 |
120 | case RemovePendingWs(requestId) => removePending(requestId)
121 |
122 | case RetryPendingWs(id) => queue1(pendings(id))
123 |
124 | }
125 |
126 | def unlockInitSeed(): Unit = {
127 | if (buys.isEmpty && sels.isEmpty) {
128 | isStartingPrice = false
129 | bot.seed match {
130 | case a if a.equals(StartMethods.lastTicker.toString) => queue1(req.subsTicker(bot.pair))
131 |
132 | case a if a.equals(StartMethods.cont.toString) =>
133 |
134 | case a if a.equals(StartMethods.lastOwn.toString) => // unsupported
135 |
136 | case _ =>
137 | val customStartPrice = Some(BigDecimal(bot.seed))
138 | seedFromStartPrice(customStartPrice)
139 | }
140 | }
141 | }
142 |
143 | def grow(offer: Offer): Seq[Offer] = grow(sortedBuys, sortedSels, offer.side)
144 |
145 | def removePending(requestId: String): Unit = pendings -= requestId
146 |
147 | def seedFromStartPrice(price: Option[BigDecimal]): Unit = price match {
148 | case Some(p) =>
149 | info(s"Got initial price ${bot.exchange} ${bot.pair} : $price, starting operation")
150 | val seed = initialSeed(sortedBuys, sortedSels, p)
151 | prepareSend(seed, As.Seed)
152 | case _ => error(s"Orderbook#GotStartPrice : Starting price for ${bot.exchange} / ${bot.pair} not found. Try different startPrice in bot")
153 | }
154 |
155 | def trim(): Unit = {
156 | val dupes = AbsOrder.getDuplicates(sortedBuys) ++ AbsOrder.getDuplicates(sortedSels)
157 | val trims = if (bot.isStrictLevels) trim(sortedBuys, sortedSels, Side.buy) ++ trim(sortedBuys, sortedSels, Side.sell) else Seq.empty[Offer]
158 | val cancels = AbsOrder.margeTrimAndDupes(trims, dupes)
159 | cancelOrders(cancels._1, As.Trim)
160 | cancelOrders(cancels._2, As.KillDupes)
161 | }
162 |
163 | def cancelOrders(o: Seq[Offer], as: As): Unit = {
164 | val cancels = o.map(p => req.cancelOrder(p.id, as))
165 | queue(cancels)
166 | }
167 |
168 | def prepareSend(o: Offer, as: As): Unit = prepareSend(Seq(o), as)
169 |
170 | def prepareSend(o: Seq[Offer], as: As): Unit = {
171 | val withId = o.map(withOrderId)
172 | addSort(withId)
173 |
174 | val newOrders = withId.map(p => req.newOrder(p, as))
175 | queue(newOrders)
176 | }
177 |
178 | def queue1(sendWs: SendWs): Unit = queue(Seq(sendWs))
179 |
180 |
181 | def queue(sendWs: Seq[SendWs]): Unit = {
182 | pendings ++= sendWs.map(p => p.requestId -> p)
183 | op foreach (_ ! QueueWs(sendWs))
184 | }
185 |
186 | def resetAll(): Unit = {
187 | buys.clear()
188 | sels.clear()
189 | sortedBuys = scala.collection.immutable.Seq.empty[Offer]
190 | sortedSels = scala.collection.immutable.Seq.empty[Offer]
191 | }
192 |
193 | def withOrderId(offer: Offer):Offer = offer.copy(id = wsEx.orderId(offer))
194 |
195 | def withOrderId(offers: Seq[Offer]): Seq[Offer] = offers.map(withOrderId)
196 |
197 | def addSort(offer: Offer): Unit = addSort(Seq(offer))
198 |
199 | def addSort(offers: Seq[Offer]): Unit = {
200 | val (b, s) = offers.partition(_.side == Side.buy)
201 | buys ++= b.map(p => p.id -> p)
202 | sels ++= s.map(p => p.id -> p)
203 | sort(Side.buy)
204 | sort(Side.sell)
205 | }
206 |
207 | def removeSort(offer: Offer): Unit = removeSort(offer.side, offer.id)
208 |
209 | def removeSort(side: Side, id: String): Unit = {
210 | remove(side, id)
211 | sort(side)
212 | }
213 |
214 | def remove(side: Side, clientOrderId: String): Unit = {
215 | var l = side match {
216 | case Side.buy => buys
217 | case Side.sell => sels
218 | case _ => TrieMap.empty[String, Offer]
219 | }
220 | l -= clientOrderId
221 | }
222 |
223 | def sort(side: Side): Unit = {
224 | side match {
225 | case Side.buy => sortedBuys = Offer.sortBuys(buys.values.toSeq)
226 | case _ => sortedSels = Offer.sortSels(sels.values.toSeq)
227 | }
228 | }
229 |
230 | }
231 |
--------------------------------------------------------------------------------
/src/main/scala/me/mbcu/integrated/mmm/actors/OrderGIActor.scala:
--------------------------------------------------------------------------------
1 | package me.mbcu.integrated.mmm.actors
2 |
3 | import akka.actor.{ActorRef, Cancellable}
4 | import akka.dispatch.ExecutionContexts.global
5 | import me.mbcu.integrated.mmm.actors.OrderGIActor.{CheckSafeForGI, SafeForGI}
6 | import me.mbcu.integrated.mmm.ops.Definitions.Settings
7 | import me.mbcu.integrated.mmm.ops.common.AbsOrder.{CheckSafeForSeed, QueueRequest, SafeForSeed}
8 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor.As.As
9 | import me.mbcu.integrated.mmm.ops.common.AbsRestActor._
10 | import me.mbcu.integrated.mmm.ops.common.Side.Side
11 | import me.mbcu.integrated.mmm.ops.common._
12 | import me.mbcu.scala.MyLogging
13 |
14 | import scala.collection.concurrent.TrieMap
15 | import scala.concurrent.ExecutionContextExecutor
16 | import scala.concurrent.duration._
17 | import scala.language.postfixOps
18 |
19 | object OrderGIActor {
20 |
21 | case class CheckSafeForGI(ref: ActorRef, bot: Bot)
22 |
23 | case class SafeForGI(yes: Boolean)
24 |
25 | }
26 |
27 | class OrderGIActor(bot: Bot, exchange: AbsExchange) extends AbsOrder(bot) with MyLogging{
28 | private implicit val ec: ExecutionContextExecutor = global
29 | var sels: TrieMap[String, Offer] = TrieMap.empty[String, Offer]
30 | var buys: TrieMap[String, Offer] = TrieMap.empty[String, Offer]
31 | var sortedSels: scala.collection.immutable.Seq[Offer] = scala.collection.immutable.Seq.empty[Offer]
32 | var sortedBuys: scala.collection.immutable.Seq[Offer] = scala.collection.immutable.Seq.empty[Offer]
33 | var maintainCancellable: Option[Cancellable] = None
34 | var logCancellable: Option[Cancellable] = None
35 | var seedCancellable: Option[Cancellable] = None
36 | private var op: Option[ActorRef] = None
37 | private implicit val book: ActorRef = self
38 | private implicit val imBot: Bot = bot
39 |
40 | override def receive: Receive = {
41 |
42 | case "start" =>
43 | op = Some(sender())
44 | logCancellable = Some(context.system.scheduler.schedule(15 second, Settings.intervalLogSeconds seconds, self, message="log"))
45 | queue1(GetActiveOrders(-1, Seq.empty[Offer], 1, As.Init))
46 |
47 | case "log" => info(Offer.dump(bot, sortedBuys, sortedSels))
48 |
49 | case GotActiveOrders(offers, currentPage, nextPage, arriveMs, send) =>
50 | if (nextPage) {
51 | queue1(GetActiveOrders(-1, offers, currentPage + 1, As.Init))
52 | } else {
53 | offers.foreach(add)
54 | sortBoth()
55 | self ! "keep or clear orderbook"
56 | }
57 |
58 | case "keep or clear orderbook" =>
59 | bot.seed match {
60 | case a if a.equalsIgnoreCase(StartMethods.lastOwn.toString) | a.equalsIgnoreCase(StartMethods.lastTicker.toString) =>
61 | if (sels.isEmpty && buys.isEmpty) self ! "init price" else cancelOrders((buys ++ sels).toSeq.map(_._2), As.ClearOpenOrders)
62 | case _ =>
63 | self ! "init price"
64 | info(s"Book ${bot.exchange} ${bot.pair} : ${bot.seed} ")
65 | }
66 |
67 | case "init price" =>
68 | bot.seed match {
69 | case s if s contains "last" => s match {
70 | case m if m.equalsIgnoreCase(StartMethods.lastOwn.toString) => queue1(GetOwnPastTrades(As.Init))
71 | case m if m.equalsIgnoreCase(StartMethods.lastTicker.toString) => queue1(GetTickerStartPrice(As.Init))
72 | }
73 | case s if s contains "cont" => gotStartPrice(None)
74 | case _ => gotStartPrice(Some(BigDecimal(bot.seed)))
75 | }
76 |
77 | case "maintain" => op.foreach(_ ! CheckSafeForGI(self, bot))
78 |
79 | case SafeForGI(yes) =>
80 | if (!yes) {
81 | scheduleMaintain(1)
82 | } else {
83 | queue((sortedBuys ++ sortedSels).map(_.id).map(GetOrderInfo(_, As.RoutineCheck)))
84 | }
85 |
86 | case "reseed" => op.foreach(_ ! CheckSafeForSeed(self, bot))
87 |
88 | case SafeForSeed(yes) =>
89 | if (yes) {
90 | val growth = grow(sortedBuys, sortedSels, Side.buy) ++ grow(sortedBuys, sortedSels, Side.sell)
91 | sendOrders(growth, As.Seed)
92 | }
93 |
94 | case GotOrderCancelled(as, arriveMs, send) =>
95 | remove(send.offer.side, send.offer.id)
96 | sort(send.offer.side)
97 | as match {
98 | case As.ClearOpenOrders => if (buys.isEmpty && sels.isEmpty) self ! "init price"
99 | case _ =>
100 | }
101 |
102 | case GotStartPrice(price, arriveMs, send) => gotStartPrice(price)
103 |
104 | case GotProvisionalOffer(newId, provisionalTs, noIdoffer) =>
105 | val offer = noIdoffer.copy(id = newId, createdAt = provisionalTs) // provisionalTs(e.g server time) is needed to correctly remove duplicates
106 | addSort(offer)
107 | queue1(GetOrderInfo(offer.id , As.RoutineCheck))
108 |
109 | case GotOrderInfo(offer, arriveMs, send) =>
110 | scheduleMaintain(1)
111 | offer.status match {
112 |
113 | case Status.active =>
114 | updateOffer(offer)
115 | val dupes = AbsOrder.getDuplicates(sortedBuys) ++ AbsOrder.getDuplicates(sortedSels)
116 | val trims = if (bot.isStrictLevels) trim(sortedBuys, sortedSels, Side.buy) ++ trim(sortedBuys, sortedSels, Side.sell) else Seq.empty[Offer]
117 | val cancels = AbsOrder.margeTrimAndDupes(trims, dupes)
118 | cancelOrders(cancels._1, As.Trim)
119 | cancelOrders(cancels._2, As.KillDupes)
120 |
121 | case Status.filled =>
122 | get(offer.id, offer.side).foreach(cache => { //cache contains original order info
123 | val ctr = counter(cache)
124 | sendOrders(Seq(ctr), as = As.Counter)
125 | })
126 | removeSort(offer)
127 |
128 | case Status.partiallyFilled => updateOffer(offer)
129 |
130 | case Status.cancelled => removeSort(offer)
131 |
132 | case _ => info(s"OrderbookActor_${bot.pair}#GotOrderInfo : unrecognized offer status")
133 |
134 | }
135 | }
136 |
137 | def gotStartPrice(price: Option[BigDecimal]) : Unit = {
138 | scheduleMaintain(Settings.getActiveSeconds)
139 | scheduleSeed(Settings.intervalSeedSeconds)
140 | price match {
141 | case Some(p) =>
142 | info(s"Got initial price ${bot.exchange} ${bot.pair} : $price, starting operation")
143 | val seed = initialSeed(sortedBuys, sortedSels, p)
144 | sendOrders(seed, As.Seed)
145 | case _ => error(s"Orderbook#GotStartPrice : Starting price for ${bot.exchange} / ${bot.pair} not found. Try different startPrice in bot")
146 | }
147 | }
148 |
149 | def queue(reqs: Seq[SendRest]): Unit = op foreach(_ ! QueueRequest(reqs))
150 |
151 | def queue1(req: SendRest): Unit = queue(Seq(req))
152 |
153 | def sendOrders(offers: Seq[Offer], as: As): Unit = queue(offers.map(NewOrder(_, as)))
154 |
155 | def cancelOrders(offers: Seq[Offer], as: As): Unit = queue(offers.map(CancelOrder(_, as)))
156 |
157 | def reseed(side: Side): Seq[Offer] = side match {
158 | case Side.buy => grow(sortedBuys, sortedSels, Side.buy)
159 | case _ => grow(sortedBuys, sortedSels, Side.sell)
160 | }
161 |
162 | def scheduleSeed(s: Int): Unit = {
163 | seedCancellable.foreach(_.cancel)
164 | seedCancellable = Some(context.system.scheduler.schedule(initialDelay = 5 second, s second, self, "reseed"))
165 | }
166 |
167 | def scheduleMaintain(s: Int): Unit = {
168 | maintainCancellable.foreach(_.cancel)
169 | maintainCancellable = Some(context.system.scheduler.scheduleOnce(s second, self, "maintain"))
170 | }
171 |
172 | def addSort(offer: Offer):Unit = {
173 | add(offer)
174 | sort(offer.side)
175 | }
176 |
177 | def sort(side: Side): Unit = {
178 | side match {
179 | case Side.buy => sortedBuys = Offer.sortBuys(buys.values.toSeq)
180 | case _ => sortedSels = Offer.sortSels(sels.values.toSeq)
181 | }
182 | }
183 |
184 | def removeSort(offer: Offer): Unit = {
185 | remove(offer.side, offer.id)
186 | sort(offer.side)
187 | }
188 |
189 | def remove(side: Side, clientOrderId: String): Unit = {
190 | var l = side match {
191 | case Side.buy => buys
192 | case Side.sell => sels
193 | case _ => TrieMap.empty[String, Offer]
194 | }
195 | l -= clientOrderId
196 | }
197 |
198 | def sortBoth(): Unit = {
199 | sort(Side.buy)
200 | sort(Side.sell)
201 | }
202 |
203 | def add(offer: Offer): Unit = {
204 | var l = offer.side match {
205 | case Side.buy => buys
206 | case Side.sell => sels
207 | case _ => TrieMap.empty[String, Offer]
208 | }
209 | l += (offer.id -> offer)
210 | }
211 |
212 | def updateOffer(in: Offer): Unit = get(in.id, in.side) foreach(c => addSort(c.copy(updatedAt = in.updatedAt, cumQuantity = in.cumQuantity)))
213 |
214 | def get(id: String, side: Side): Option[Offer] = side match {
215 | case Side.buy => Some(buys(id))
216 | case Side.sell => Some(sels(id))
217 | case _ => None
218 | }
219 |
220 | }
221 |
--------------------------------------------------------------------------------