Skip to content

Commit

Permalink
Added futures support (OKEx, Binance, Huobi, FTX)
Browse files Browse the repository at this point in the history
  • Loading branch information
aneonex committed Oct 7, 2021
1 parent c547010 commit 037a711
Show file tree
Hide file tree
Showing 18 changed files with 535 additions and 297 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ object MarketsConfig {
CoinJarExchange(),
CoinEgg(),
Phemex(),

FtxFutures(),
HuobiFutures()
)

val MARKETS: Map<String, Market> = registeredMarkets.map{it.key to it}.toMap()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package com.aneonex.bitcoinchecker.datamodule.model

import java.util.*

class CheckerInfo(currencyBase: String, currencyCounter: String, currencyPairId: String?, val contractType: Int)
: CurrencyPairInfo(currencyBase, currencyCounter, currencyPairId) {
class CheckerInfo(currencyBase: String, currencyCounter: String, currencyPairId: String?, contractType: FuturesContractType)
: CurrencyPairInfo(currencyBase, currencyCounter, currencyPairId, contractType) {
val currencyBaseLowerCase: String
get() = currencyBase.lowercase(Locale.US)
val currencyCounterLowerCase: String
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,34 @@ package com.aneonex.bitcoinchecker.datamodule.model
open class CurrencyPairInfo(
val currencyBase: String,
val currencyCounter: String,
val currencyPairId: String?
val currencyPairId: String?,
val contractType: FuturesContractType = FuturesContractType.NONE
) : Comparable<CurrencyPairInfo> {

@Suppress("unused") // Used by Gson
private constructor() : this("", "", null)

@Throws(NullPointerException::class)
override fun compareTo(other: CurrencyPairInfo): Int {
val compBase = currencyBase.compareTo(other.currencyBase, ignoreCase = true)
return if (compBase != 0) compBase else currencyCounter.compareTo(
var compBase = currencyBase.compareTo(other.currencyBase, ignoreCase = true)
if (compBase != 0) return compBase

compBase = currencyCounter.compareTo(
other.currencyCounter,
ignoreCase = true
)
if (compBase != 0) return compBase

return contractType.compareTo(other.contractType)
}

override fun toString(): String {
fun tryGetContactName(): String {
val resultName = FuturesContractType.getShortName(contractType)
return if(resultName == null) "" else ":$resultName"
}

return currencyPairId ?:
"$currencyBase:$currencyCounter" + tryGetContactName()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.aneonex.bitcoinchecker.datamodule.model

import java.time.DayOfWeek
import java.time.LocalDate
import java.time.ZoneOffset
import java.time.temporal.TemporalAdjusters

enum class FuturesContractType(val value: Int) {
NONE(0),
PERPETUAL(1),
WEEKLY(2),
BIWEEKLY(3),
MONTHLY(4),
BIMONTHLY(5),
QUARTERLY(6),
BIQUARTERLY(7);

override fun toString(): String {
return getShortName(this) ?: "None"
}

companion object {
fun fromInt(value: Int): FuturesContractType = values().firstOrNull { it.value == value } ?: NONE

fun getShortName(contractType: FuturesContractType): String? =
when (contractType) {
NONE -> null
PERPETUAL ->
@Suppress("SpellCheckingInspection")
"Perp"
WEEKLY -> "1W"
BIWEEKLY -> "2W"
MONTHLY -> "1M"
BIMONTHLY -> "2M"
QUARTERLY -> "1Q"
BIQUARTERLY -> "2Q"
}

fun getDeliveryDate(contractType: FuturesContractType): LocalDate? {
fun getCurrentDate() = LocalDate.now(ZoneOffset.UTC)

return when(contractType) {
NONE,
PERPETUAL ->
null

WEEKLY -> getEndOfWeek(getCurrentDate())
BIWEEKLY -> getEndOfWeek(getCurrentDate().plusWeeks(1))

MONTHLY -> getEndOfMonth(getCurrentDate())
BIMONTHLY -> getEndOfMonth(getCurrentDate().plusMonths(1))

QUARTERLY -> getEndOfQuarter(getCurrentDate())
BIQUARTERLY -> getEndOfQuarter(getCurrentDate().plusMonths(3))
// else -> throw MarketParseException("Unexpected contact type")
}
}

private fun getEndOfWeek(currentDate: LocalDate): LocalDate =
if(currentDate.dayOfWeek == DayOfWeek.FRIDAY) currentDate
else currentDate.with(TemporalAdjusters.next(DayOfWeek.FRIDAY))

private fun getEndOfMonth(currentDate: LocalDate): LocalDate =
currentDate.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY))

private fun getEndOfQuarter(currentDate: LocalDate): LocalDate {
val firstDayOfQuarter: LocalDate =
currentDate.with(currentDate.month.firstMonthOfQuarter())
.with(TemporalAdjusters.firstDayOfMonth())

return firstDayOfQuarter.plusMonths(2)
.with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY))
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,12 @@ abstract class Market(
@Throws(Exception::class)
fun parseTickerMain(requestId: Int, responseString: String, ticker: Ticker, checkerInfo: CheckerInfo): Ticker {
parseTicker(requestId, responseString, ticker, checkerInfo)
if (ticker.timestamp <= 0) ticker.timestamp = System.currentTimeMillis() else ticker.timestamp = TimeUtils.parseTimeToMillis(ticker.timestamp)

if (ticker.timestamp <= 0)
ticker.timestamp = System.currentTimeMillis()
else
ticker.timestamp = TimeUtils.parseTimeToMillis(ticker.timestamp)

return ticker
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,15 @@ import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo
import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo
import com.aneonex.bitcoinchecker.datamodule.model.Market
import com.aneonex.bitcoinchecker.datamodule.model.Ticker
import com.aneonex.bitcoinchecker.datamodule.model.market.generic.SimpleMarket
import com.aneonex.bitcoinchecker.datamodule.util.forEachJSONObject
import org.json.JSONObject

class Binance : Market(NAME, TTS_NAME, null) {

companion object {
private const val NAME = "Binance"
private const val TTS_NAME = NAME
private const val URL = "https://api.binance.com/api/v3/ticker/24hr?symbol=%1\$s"
private const val URL_CURRENCY_PAIRS = "https://api.binance.com/api/v3/exchangeInfo"
}

override fun getUrl(requestId: Int, checkerInfo: CheckerInfo): String {
return String.format(URL, checkerInfo.currencyPairId)
}

class Binance : SimpleMarket(
"Binance",
"https://api.binance.com/api/v3/exchangeInfo",
"https://api.binance.com/api/v3/ticker/24hr?symbol=%1\$s"
) {
@Throws(Exception::class)
override fun parseTickerFromJsonObject(requestId: Int, jsonObject: JSONObject, ticker: Ticker, checkerInfo: CheckerInfo) {
ticker.bid = jsonObject.getDouble("bidPrice")
Expand All @@ -35,13 +28,6 @@ class Binance : Market(NAME, TTS_NAME, null) {
ticker.timestamp = jsonObject.getLong("closeTime")
}

// ====================
// Get currency pairs
// ====================
override fun getCurrencyPairsUrl(requestId: Int): String {
return URL_CURRENCY_PAIRS
}

@Throws(Exception::class)
override fun parseCurrencyPairsFromJsonObject(requestId: Int, jsonObject: JSONObject, pairs: MutableList<CurrencyPairInfo>) {
jsonObject.getJSONArray("symbols").forEachJSONObject { marketJsonObject ->
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.aneonex.bitcoinchecker.datamodule.model.market

import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo
import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo
import com.aneonex.bitcoinchecker.datamodule.model.Market
import com.aneonex.bitcoinchecker.datamodule.model.Ticker
import com.aneonex.bitcoinchecker.datamodule.model.*
import com.aneonex.bitcoinchecker.datamodule.util.forEachJSONObject
import org.json.JSONArray
import org.json.JSONObject
import java.time.format.DateTimeFormatter
import java.util.*

class BinanceFutures : Market(NAME, TTS_NAME, null) {
companion object {
Expand All @@ -23,6 +22,8 @@ class BinanceFutures : Market(NAME, TTS_NAME, null) {

private fun isCoinMPair(pairId: String) = pairId.startsWith(COIN_M_PREFIX)

private val FUTURES_DATE_FORMAT = DateTimeFormatter.ofPattern("yyMMdd", Locale.ROOT)

private fun parseTicker(jsonObject: JSONObject, ticker: Ticker) {
jsonObject.apply {
ticker.vol = getDouble("volume")
Expand All @@ -38,13 +39,25 @@ class BinanceFutures : Market(NAME, TTS_NAME, null) {
}

override fun getUrl(requestId: Int, checkerInfo: CheckerInfo): String {
return if(isCoinMPair(checkerInfo.currencyPairId!!)) {
val pairId = checkerInfo.currencyPairId.substring(COIN_M_PREFIX.length)
String.format(URL_COIN_M, pairId)
val utlTemplate: String
val pairId: String

if(isCoinMPair(checkerInfo.currencyPairId!!)) {
pairId = checkerInfo.currencyPairId.substring(COIN_M_PREFIX.length)
// String.format(URL_COIN_M, pairId)
utlTemplate = URL_COIN_M
}
else {
String.format(URL_USD_M, checkerInfo.currencyPairId)
// String.format(URL_USD_M, checkerInfo.currencyPairId)
pairId = checkerInfo.currencyPairId
utlTemplate = URL_USD_M
}

val deliveryDate = FuturesContractType.getDeliveryDate(checkerInfo.contractType) ?:
return String.format(utlTemplate, pairId)

val pairIdWithDeliveryDate = "${checkerInfo.currencyBase}${checkerInfo.currencyCounter}_${FUTURES_DATE_FORMAT.format(deliveryDate)}"
return String.format(utlTemplate, pairIdWithDeliveryDate)
}

override fun parseTicker(
Expand All @@ -67,10 +80,20 @@ class BinanceFutures : Market(NAME, TTS_NAME, null) {

@Throws(Exception::class)
override fun parseCurrencyPairsFromJsonObject(requestId: Int, jsonObject: JSONObject, pairs: MutableList<CurrencyPairInfo>) {
fun parseContractType(value: String): FuturesContractType? =
when(value) {
"PERPETUAL" -> FuturesContractType.PERPETUAL
"CURRENT_QUARTER" -> FuturesContractType.QUARTERLY
"NEXT_QUARTER" -> FuturesContractType.BIQUARTERLY
else -> null
}

jsonObject.getJSONArray("symbols").forEachJSONObject { marketJsonObject ->
// Tha app UI supports only perpetual futures
if(marketJsonObject.getString("contractType") != "PERPETUAL")
return@forEachJSONObject
val contractType = parseContractType(marketJsonObject.getString("contractType"))
?: return@forEachJSONObject
//if(marketJsonObject.getString("contractType") != "PERPETUAL")
// return@forEachJSONObject

val symbol = marketJsonObject.getString("symbol").let {
if(requestId > 0) COIN_M_PREFIX + it else it
Expand All @@ -79,9 +102,10 @@ class BinanceFutures : Market(NAME, TTS_NAME, null) {
val quoteAsset = marketJsonObject.getString("quoteAsset")

pairs.add(CurrencyPairInfo(
baseAsset,
quoteAsset,
symbol))
baseAsset,
quoteAsset,
symbol,
contractType))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,16 @@ package com.aneonex.bitcoinchecker.datamodule.model.market

import com.aneonex.bitcoinchecker.datamodule.model.CheckerInfo
import com.aneonex.bitcoinchecker.datamodule.model.CurrencyPairInfo
import com.aneonex.bitcoinchecker.datamodule.model.Market
import com.aneonex.bitcoinchecker.datamodule.model.Ticker
import com.aneonex.bitcoinchecker.datamodule.model.market.generic.SimpleMarket
import com.aneonex.bitcoinchecker.datamodule.util.forEachJSONObject
import org.json.JSONObject

class Ftx : Market(NAME, TTS_NAME, null) {
companion object {
private const val NAME = "FTX"
private const val TTS_NAME = "FTX"
private const val URL = "https://ftx.com/api/markets/%1\$s"
private const val URL_CURRENCY_PAIRS = "https://ftx.com/api/markets"
}

override fun getCurrencyPairsUrl(requestId: Int): String {
return URL_CURRENCY_PAIRS
}

class Ftx : SimpleMarket(
"FTX",
"https://ftx.com/api/markets",
"https://ftx.com/api/markets/%1\$s"
) {
override fun parseCurrencyPairsFromJsonObject(requestId: Int, jsonObject: JSONObject, pairs: MutableList<CurrencyPairInfo>) {
jsonObject.getJSONArray("result").forEachJSONObject { market ->
if(market.getString("type") != "spot") return@forEachJSONObject
Expand All @@ -29,11 +22,6 @@ class Ftx : Market(NAME, TTS_NAME, null) {
market.getString("name"),
))
}

}

override fun getUrl(requestId: Int, checkerInfo: CheckerInfo): String {
return String.format(URL, checkerInfo.currencyPairId)
}

override fun parseTickerFromJsonObject(requestId: Int, jsonObject: JSONObject, ticker: Ticker, checkerInfo: CheckerInfo) {
Expand All @@ -46,7 +34,6 @@ class Ftx : Market(NAME, TTS_NAME, null) {
if(ticker.last > 0) {
ticker.volQuote = market.getDouble("quoteVolume24h")
ticker.vol = ticker.volQuote / ticker.last // Calculated base volume

}
}
}
Loading

0 comments on commit 037a711

Please sign in to comment.