Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package co.nilin.opex.api.core.inout

import java.time.LocalDateTime

data class CandleData(
val openTime: LocalDateTime,
val closeTime: LocalDateTime,
val open: Double,
val close: Double,
val high: Double,
val low: Double,
val volume: Double,
val quoteAssetVolume: Double,
val trades: Int,
val takerBuyBaseAssetVolume: Double,
val takerBuyQuoteAssetVolume: Double,
)
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,12 @@ interface MarketQueryHandler {

suspend fun lastPrice(symbol: String?): List<PriceTickerResponse>

suspend fun getCandleInfo(
symbol: String,
interval: String,
startTime: Long?,
endTime: Long?,
limit: Int
): List<CandleData>

}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class SecurityConfig(private val webClient: WebClient) {
.pathMatchers("/v3/trades").permitAll()
.pathMatchers("/v3/ticker/**").permitAll()
.pathMatchers("/v3/exchangeInfo").permitAll()
.pathMatchers("/v3/klines").permitAll()
.pathMatchers(HttpMethod.OPTIONS, "/**").permitAll()
.pathMatchers("/**").hasAuthority("SCOPE_trust")
.anyExchange().authenticated()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,10 @@ package co.nilin.opex.port.api.binance.controller

import co.nilin.opex.api.core.spi.MarketQueryHandler
import co.nilin.opex.api.core.spi.SymbolMapper
import co.nilin.opex.port.api.binance.data.OrderBookResponse
import co.nilin.opex.api.core.inout.PriceChangeResponse
import co.nilin.opex.api.core.inout.PriceTickerResponse
import co.nilin.opex.api.core.spi.AccountantProxy
import co.nilin.opex.port.api.binance.data.ExchangeInfoResponse
import co.nilin.opex.port.api.binance.data.ExchangeInfoSymbol
import co.nilin.opex.port.api.binance.data.RecentTradeResponse
import co.nilin.opex.port.api.binance.data.*
import co.nilin.opex.utility.error.data.OpexError
import co.nilin.opex.utility.error.data.OpexException
import co.nilin.opex.utility.error.data.throwError
Expand All @@ -23,8 +20,6 @@ import java.security.Principal
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.collections.ArrayList

@RestController
Expand All @@ -35,7 +30,7 @@ class MarketController(
) {

private val orderBookValidLimits = arrayListOf(5, 10, 20, 50, 100, 500, 1000, 5000)
private val validDurations = arrayListOf("24h", "7d", "1m")
private val validDurations = arrayListOf("24h", "7d", "1M")

// Limit - Weight
// 5, 10, 20, 50, 100 - 1
Expand Down Expand Up @@ -107,7 +102,7 @@ class MarketController(
}
}

@GetMapping("/v3/ticker/{duration:24h|7d|1m}")
@GetMapping("/v3/ticker/{duration:24h|7d|1M}")
suspend fun priceChange(
@PathVariable("duration")
duration: String,
Expand All @@ -122,16 +117,7 @@ class MarketController(
if (!validDurations.contains(duration))
throwError(OpexError.InvalidPriceChangeDuration)

val now = Date().time
val before = when (duration) {
"24h" -> Date(now - TimeUnit.DAYS.toMillis(1))
"7d" -> Date(now - TimeUnit.DAYS.toMillis(7))
"1m" -> Date(now - TimeUnit.DAYS.toMillis(31))
else -> Date(now - TimeUnit.DAYS.toMillis(1))
}

val instant = Instant.ofEpochMilli(before.time)
val startDate = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
val startDate = Interval.findByLabel(duration)?.getLocalDateTime() ?: Interval.Day.getLocalDateTime()

return if (symbol.isNullOrEmpty())
marketQueryHandler.getTradeTickerData(startDate)
Expand Down Expand Up @@ -173,4 +159,48 @@ class MarketController(
return ExchangeInfoResponse(symbols = pairConfigs)
}

// Weight(IP): 1
@GetMapping("/v3/klines")
suspend fun klines(
@RequestParam("symbol")
symbol: String,
@RequestParam("interval")
interval: String,
@RequestParam("startTime", required = false)
startTime: Long?,
@RequestParam("endTime", required = false)
endTime: Long?,
@RequestParam("limit", required = false)
limit: Int? // Default 500; max 1000.
): List<List<Any>> {
val validLimit = limit ?: 500
val localSymbol = symbolMapper.unmap(symbol) ?: throw OpexException(OpexError.SymbolNotFound)
if (validLimit !in 1..1000)
throwError(OpexError.InvalidLimitForRecentTrades)

val i = Interval.findByLabel(interval) ?: throw OpexException(OpexError.InvalidInterval)

val list = ArrayList<ArrayList<Any>>()
marketQueryHandler.getCandleInfo(localSymbol, "${i.duration} ${i.unit}", startTime, endTime, validLimit)
.forEach {
list.add(
arrayListOf(
it.openTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
it.open.toString(),
it.high.toString(),
it.low.toString(),
it.close.toString(),
it.volume.toString(),
it.closeTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
it.quoteAssetVolume.toString(),
it.trades,
it.takerBuyBaseAssetVolume.toString(),
it.takerBuyQuoteAssetVolume.toString(),
"0.0"
)
)
}
return list
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package co.nilin.opex.port.api.binance.data

import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
import java.util.concurrent.TimeUnit

enum class Interval(val label: String, val unit: TimeUnit, val duration: Long) {

Minute("1m", TimeUnit.MINUTES, 1),
ThreeMinutes("3m", TimeUnit.MINUTES, 3),
FiveMinutes("5m", TimeUnit.MINUTES, 5),
FifteenMinutes("15m", TimeUnit.MINUTES, 15),
ThirtyMinutes("30m", TimeUnit.MINUTES, 30),
Hour("1h", TimeUnit.HOURS, 1),
TwoHours("2h", TimeUnit.HOURS, 2),
FourHours("4h", TimeUnit.HOURS, 4),
SixHours("6h", TimeUnit.HOURS, 6),
EightHours("8h", TimeUnit.HOURS, 8),
TwelveHours("12h", TimeUnit.HOURS, 12),
TwentyFourHours("24h", TimeUnit.HOURS, 24),
Day("1d", TimeUnit.DAYS, 1),
ThreeDays("3d", TimeUnit.DAYS, 3),
Week("1w", TimeUnit.DAYS, 7),
Month("1M", TimeUnit.DAYS, 31);

private fun getOffsetTime() = unit.toMillis(duration)

fun getDate() = Date(Date().time - getOffsetTime())

fun getLocalDateTime(): LocalDateTime = with(Instant.ofEpochMilli(getDate().time)) {
LocalDateTime.ofInstant(this, ZoneId.systemDefault())
}

companion object {
fun findByLabel(label: String): Interval? {
return values().find { it.label == label }
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,18 @@ class PostgresConfig(db: DatabaseClient) {
INSERT INTO symbol_maps(symbol, value) VALUES('btc_usdt', 'BTCUSDT') ON CONFLICT DO NOTHING;
INSERT INTO symbol_maps(symbol, value) VALUES('eth_usdt', 'ETHUSDT') ON CONFLICT DO NOTHING;
INSERT INTO symbol_maps(symbol, value) VALUES('eth_btc', 'ETHBTC') ON CONFLICT DO NOTHING;

create or replace function interval_generator(start_ts timestamp without TIME ZONE, end_ts timestamp without TIME ZONE, round_interval INTERVAL)
returns TABLE(start_time timestamp without TIME ZONE, end_time timestamp without TIME ZONE) as $$
BEGIN
return query
SELECT
(n) start_time,
(n + round_interval) end_time
FROM generate_series(date_trunc('minute', start_ts), end_ts, round_interval) n;
END
$$
LANGUAGE 'plpgsql';
"""
val initDb = db.sql { sql }
initDb // initialize the database
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package co.nilin.opex.port.api.postgres.dao

import co.nilin.opex.port.api.postgres.model.CandleInfoData
import co.nilin.opex.port.api.postgres.model.TradeModel
import co.nilin.opex.port.api.postgres.model.TradeTickerData
import kotlinx.coroutines.flow.Flow
Expand Down Expand Up @@ -106,4 +107,44 @@ interface TradeRepository : ReactiveCrudRepository<TradeModel, Long> {

@Query("select * from trades where create_date in (select max(create_date) from trades group by symbol)")
fun findAllGroupBySymbol(): Flux<TradeModel>

@Query(
"""
with intervals as (select * from interval_generator((:startTime), (:endTime), :interval ::INTERVAL))
select
f.start_time as open_time,
f.end_time as close_time,
(select taker_price from trades tt where tt.create_date >= f.start_time and tt.create_date < f.end_time order by tt.create_date asc limit 1) as open,
max(t.taker_price) as high,
min(t.taker_price) as low,
(select taker_price from trades tt where tt.create_date >= f.start_time and tt.create_date < f.end_time order by tt.create_date desc limit 1) as close,
sum(t.matched_quantity) as volume,
count(id) as trades
from trades t
right join intervals f
on t.create_date >= f.start_time and t.create_date < f.end_time
where symbol = :symbol or symbol is null
group by f.start_time, f.end_time
order by f.end_time desc
limit :limit
"""
)
suspend fun candleData(
@Param("symbol")
symbol: String,
@Param("interval")
interval: String,
@Param("startTime")
startTime: LocalDateTime,
@Param("endTime")
endTime: LocalDateTime,
@Param("limit")
limit: Int,
): Flux<CandleInfoData>

@Query("select * from trades order by create_date desc limit 1")
suspend fun findLastByCreateDate(): Mono<TradeModel>

@Query("select * from trades order by create_date asc limit 1")
suspend fun findFirstByCreateDate(): Mono<TradeModel>
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import kotlinx.coroutines.reactive.awaitFirstOrElse
import kotlinx.coroutines.reactive.awaitFirstOrNull
import org.springframework.stereotype.Component
import java.lang.Exception
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.ZoneOffset
Expand Down Expand Up @@ -125,6 +126,49 @@ class MarketQueryHandlerImpl(

}

override suspend fun getCandleInfo(
symbol: String,
interval: String,
startTime: Long?,
endTime: Long?,
limit: Int
): List<CandleData> {
val st = if (startTime == null)
tradeRepository.findFirstByCreateDate().awaitFirstOrNull()?.createDate
?: LocalDateTime.now()
else
with(Instant.ofEpochMilli(startTime)) {
LocalDateTime.ofInstant(this, ZoneId.systemDefault())
}

val et = if (endTime == null)
tradeRepository.findLastByCreateDate().awaitFirstOrNull()?.createDate
?: LocalDateTime.now()
else
with(Instant.ofEpochMilli(endTime)) {
LocalDateTime.ofInstant(this, ZoneId.systemDefault())
}

return tradeRepository.candleData(symbol, interval, st, et, limit)
.collectList()
.awaitFirstOrElse { emptyList() }
.map {
CandleData(
it.openTime,
it.closeTime,
it.open ?: 0.0,
it.close ?: 0.0,
it.high ?: 0.0,
it.low ?: 0.0,
it.volume ?: 0.0,
0.0,
it.trades,
0.0,
0.0
)
}
}

private fun OrderModel.asQueryOrderResponse() = QueryOrderResponse(
symbol,
ouid,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package co.nilin.opex.port.api.postgres.model

import org.springframework.data.relational.core.mapping.Column
import java.time.LocalDateTime

data class CandleInfoData(
@Column("open_time")
val openTime: LocalDateTime,
@Column("close_time")
val closeTime: LocalDateTime,
val open: Double?,
val close: Double?,
val high: Double?,
val low: Double?,
val volume: Double?,
val trades: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ enum class OpexError(val code: Int, val message: String?, val status: HttpStatus
InvalidLimitForOrderBook(7003, "Valid limits: [5, 10, 20, 50, 100, 500, 1000, 5000]", HttpStatus.BAD_REQUEST),
InvalidLimitForRecentTrades(7004, "Valid limits: 1 min - 1000 max", HttpStatus.BAD_REQUEST),
InvalidPriceChangeDuration(7005, "Valid durations: [24h, 7d, 1m]", HttpStatus.BAD_REQUEST),
CancelOrderNotAllowed(7006, "Canceling this order is not allowed", HttpStatus.FORBIDDEN);
CancelOrderNotAllowed(7006, "Canceling this order is not allowed", HttpStatus.FORBIDDEN),
InvalidInterval(7007, "Invalid interval", HttpStatus.BAD_REQUEST);

companion object {
fun findByCode(code: Int?): OpexError? {
Expand Down