Skip to content

Commit

Permalink
charts api reworked
Browse files Browse the repository at this point in the history
  • Loading branch information
GusevTimofey committed Sep 12, 2022
1 parent 0923431 commit e065d72
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ project/metals.sbt

config.env

/.bsp/
/.bsp/
/.bsp/sbt.json
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,5 @@ object App extends EnvApp[AppContext] {
.eval(wr.concurrentEffect)
.flatMap(implicit ce => AsyncHttpClientFs2Backend.resource[RunF](blocker))
.mapK(wr.runContextK(ctx))

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import org.ergoplatform.common.http.HttpError
import org.ergoplatform.common.models.TimeWindow
import org.ergoplatform.dex.domain.amm.PoolId
import org.ergoplatform.dex.markets.api.v1.models.amm._
import org.ergoplatform.dex.markets.api.v1.models.charts.ChartGap
import org.ergoplatform.dex.markets.api.v1.models.locks.LiquidityLockInfo
import org.ergoplatform.dex.markets.configs.RequestConfig
import sttp.tapir.json.circe.jsonBody
Expand All @@ -15,7 +16,7 @@ final class AmmStatsEndpoints(conf: RequestConfig) {
val Group = "ammStats"

def endpoints: List[Endpoint[_, _, _, _]] =
getSwapTxs :: getDepositTxs :: getPoolLocks :: getPlatformStats :: getPoolStats :: getAvgPoolSlippage :: getPoolPriceChart :: getAmmMarkets :: convertToFiat :: Nil
getSwapTxs :: getDepositTxs :: getPoolLocks :: getPlatformStats :: getPoolStats :: getAvgPoolSlippage :: getPoolPriceChart :: getPoolPriceChartWithGaps :: getAmmMarkets :: convertToFiat :: Nil

def getSwapTxs: Endpoint[TimeWindow, HttpError, TransactionsInfo, Any] =
baseEndpoint.get
Expand Down Expand Up @@ -81,6 +82,27 @@ final class AmmStatsEndpoints(conf: RequestConfig) {
.name("Pool chart")
.description("Get price chart by pool")

def getPoolPriceChartWithGaps: Endpoint[(PoolId, TimeWindow, ChartGap), HttpError, List[PricePoint], Any] =
baseEndpoint.get
.in(PathPrefix / "pool" / path[PoolId].description("Pool reference") / "charts")
.in(timeWindow)
.in(
query[ChartGap]("gap")
.default(ChartGap.Gap1h)
.description(
"""
|This field is used for time gap definition.
|Example: If you want to get charts with gaps of 5 minutes, e.g. 17:30, 17:35, 17:40 etc.,
|you have to put the value 5min.
|If desired gap is 1 hour, e.g. 17:00, 18:00, 19:00, value to input is 1h.
|""".stripMargin
)
)
.out(jsonBody[List[PricePoint]])
.tag(Group)
.name("Pool charts")
.description("Get price chart by pool using gaps")

def getAmmMarkets: Endpoint[TimeWindow, HttpError, List[AmmMarketSummary], Any] =
baseEndpoint.get
.in(PathPrefix / "markets")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package org.ergoplatform.dex.markets.api.v1.models.charts

import cats.Show
import doobie.util.Put
import enumeratum.values.{StringEnum, StringEnumEntry}
import io.circe.{Decoder, Encoder}
import sttp.tapir.{Codec, Schema}
import cats.syntax.either._
import org.ergoplatform.dex.markets.api.v1.services.{Utc, Zone}
import sttp.tapir.Schema.SName
import sttp.tapir.generic.Derived
import tofu.logging.Loggable
import sttp.tapir.generic.auto._

import java.time.{Instant, LocalDateTime, ZoneOffset}
import java.time.temporal.ChronoUnit
import scala.concurrent.duration.{DurationInt, FiniteDuration}

sealed abstract class ChartGap(
val value: String,
val dateFormat: String,
val timeWindow: FiniteDuration,
val pgValue: Int, /** This value is used only for minutes aggregation. E.g. 5 means the gap is 5 minute, 15 - 15 means gap. */
val minimalGap: Long,
val javaDateFormat: String
) extends StringEnumEntry

object ChartGap extends StringEnum[ChartGap] {
case object Gap5min extends ChartGap("5min", "YYYY-mm-dd HH24:MI", 5.minutes, 5, 1.hour.toMillis, "yyyy-MM-dd HH:mm")

case object Gap15min
extends ChartGap("15min", "YYYY-mm-dd HH24:MI", 15.minutes, 15, 6.hours.toMillis, "yyyy-MM-dd HH:mm")

case object Gap30min
extends ChartGap("30min", "YYYY-mm-dd HH24:MI", 30.minutes, 30, 1.day.toMillis, "yyyy-MM-dd HH:mm")
case object Gap1h extends ChartGap("1h", "YYYY-mm-dd HH24", 1.hour, 1, 1.day.toMillis, "yyyy-MM-dd HH")
case object Gap1d extends ChartGap("1d", "YYYY-mm-dd", 1.day, 1, 7.days.toMillis, "yyyy-MM-dd")
case object Gap1m extends ChartGap("1m", "YYYY-mm", 30.days, 1, 180.days.toMillis, "yyyy-MM")
case object Gap1y extends ChartGap("1y", "YYYY", 365.days, 1, 1095.days.toMillis, "yyyy")

val values = findValues

implicit val put: Put[ChartGap] = implicitly[Put[String]].contramap(_.dateFormat)

implicit val schema: Schema[ChartGap] = implicitly[Derived[Schema[ChartGap]]].value
.modify(_.value)(
_.description("The gap's value. min means minute, h means hour, d means day, m means month, y means year.")
)
.default(ChartGap.Gap1h)
.name(SName("Chart gap"))

implicit val decoder: Decoder[ChartGap] =
Decoder.decodeString.emap(s => Either.catchNonFatal(ChartGap.withValue(s)).leftMap(_.getMessage))

implicit val encoder: Encoder[ChartGap] = Encoder.encodeString.contramap(_.value)

implicit def plainCodec: Codec.PlainCodec[ChartGap] = Codec.stringCodec(s => ChartGap.withValue(s))

implicit val show: Show[ChartGap] = _.value

implicit def loggable: Loggable[ChartGap] = Loggable.show

def round(gap: ChartGap, from: Long): Long =
gap match {
case ChartGap.Gap5min | ChartGap.Gap15min | ChartGap.Gap30min | ChartGap.Gap1h =>
from / gap.timeWindow.toMillis * gap.timeWindow.toMillis
case ChartGap.Gap1d =>
LocalDateTime
.ofInstant(Instant.ofEpochMilli(from), Zone)
.withHour(0)
.withMinute(0)
.withSecond(0)
.truncatedTo(ChronoUnit.DAYS)
.toEpochSecond(ZoneOffset.UTC) * 1000
case ChartGap.Gap1m =>
LocalDateTime
.ofInstant(Instant.ofEpochMilli(from), Zone)
.withDayOfMonth(1)
.withHour(0)
.withMinute(0)
.withSecond(0)
.toEpochSecond(ZoneOffset.UTC) * 1000
case ChartGap.Gap1y =>
LocalDateTime
.ofInstant(Instant.ofEpochMilli(from), Zone)
.withDayOfYear(1)
.withDayOfMonth(1)
.withHour(0)
.withMinute(0)
.withSecond(0)
.toEpochSecond(ZoneOffset.UTC) * 1000
}

def updateWithGap(gap: ChartGap, time: Long): Long =
gap match {
case ChartGap.Gap5min | ChartGap.Gap15min | ChartGap.Gap30min | ChartGap.Gap1h =>
time + gap.timeWindow.toMillis
case ChartGap.Gap1d =>
LocalDateTime
.ofInstant(Instant.ofEpochMilli(time), Zone)
.plusDays(1)
.toEpochSecond(Utc) * 1000
case ChartGap.Gap1m =>
LocalDateTime
.ofInstant(Instant.ofEpochMilli(time), Zone)
.plusMonths(1)
.toEpochSecond(Utc) * 1000
case ChartGap.Gap1y =>
LocalDateTime
.ofInstant(Instant.ofEpochMilli(time), Zone)
.plusYears(1)
.toEpochSecond(Utc) * 1000
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,16 @@ import org.ergoplatform.common.http.AdaptThrowable.AdaptThrowableEitherT
import org.ergoplatform.common.http.HttpError
import org.ergoplatform.common.http.cache.CacheMiddleware.CachingMiddleware
import org.ergoplatform.common.http.syntax._
import org.ergoplatform.common.models.TimeWindow
import org.ergoplatform.dex.markets.api.v1.endpoints.AmmStatsEndpoints
import org.ergoplatform.dex.markets.api.v1.models.charts.ChartGap
import org.ergoplatform.dex.markets.api.v1.services.{AmmStats, LqLocks}
import org.ergoplatform.dex.markets.configs.RequestConfig
import org.http4s.HttpRoutes
import sttp.tapir.server.http4s.{Http4sServerInterpreter, Http4sServerOptions}
import cats.syntax.either._
import cats.syntax.applicative._
import org.ergoplatform.dex.markets.api.v1.models.amm.PricePoint

final class AmmStatsRoutes[
F[_]: Concurrent: ContextShift: Timer: AdaptThrowableEitherT[*[_], HttpError]
Expand All @@ -24,7 +29,7 @@ final class AmmStatsRoutes[
private val interpreter = Http4sServerInterpreter(opts)

def routes: HttpRoutes[F] =
getSwapTxsR <+> getDepositTxsR <+> getPoolLocksR <+> getPlatformStatsR <+> getPoolStatsR <+> getAvgPoolSlippageR <+> getPoolPriceChartR <+> getAmmMarketsR <+> convertToFiatR
getSwapTxsR <+> getDepositTxsR <+> getPoolLocksR <+> getPlatformStatsR <+> getPoolStatsR <+> getAvgPoolSlippageR <+> getPoolPriceChartR <+> getPoolPriceChartWithGapsR <+> getAmmMarketsR <+> convertToFiatR

def getSwapTxsR: HttpRoutes[F] =
interpreter.toRoutes(getSwapTxs)(tw => stats.getSwapTransactions(tw).adaptThrowable.value)
Expand Down Expand Up @@ -52,6 +57,19 @@ final class AmmStatsRoutes[
stats.getPoolPriceChart(poolId, window, res).adaptThrowable.value
}

def getPoolPriceChartWithGapsR: HttpRoutes[F] = interpreter.toRoutes(getPoolPriceChartWithGaps) {
case (poolId, window, gap) =>
def validateChartGap(gap: ChartGap, window: TimeWindow): Boolean =
(for {
from <- window.from
to <- window.to
period = to - from
} yield period <= gap.minimalGap).getOrElse(true)

if (validateChartGap(gap, window)) stats.getChartsByGap(gap, poolId, window).adaptThrowable.value
else (HttpError.Unknown(500, s"Invalid from to value for selected gap."): HttpError).asLeft[List[PricePoint]].pure
}

def getAmmMarketsR: HttpRoutes[F] = interpreter.toRoutes(getAmmMarkets) { tw =>
stats.getMarkets(tw).adaptThrowable.value
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,34 +3,30 @@ package org.ergoplatform.dex.markets.api.v1.services
import cats.Monad
import cats.data.OptionT
import cats.effect.Clock
import cats.syntax.option._
import mouse.anyf._
import org.ergoplatform.common.models.TimeWindow
import org.ergoplatform.dex.domain.amm.PoolId
import org.ergoplatform.dex.markets.api.v1.models.amm.{
AmmMarketSummary,
FiatEquiv,
PlatformSummary,
PoolSlippage,
PoolSummary,
PricePoint,
TransactionsInfo
}
import org.ergoplatform.dex.domain.{AssetClass, CryptoUnits, FullAsset, MarketId}
import org.ergoplatform.dex.markets.api.v1.models.amm.types._
import org.ergoplatform.dex.markets.api.v1.models.amm._
import org.ergoplatform.dex.markets.api.v1.models.charts.ChartGap
import org.ergoplatform.dex.markets.currencies.UsdUnits
import org.ergoplatform.dex.markets.db.models.amm._
import org.ergoplatform.dex.markets.domain.{CryptoVolume, Fees, TotalValueLocked, Volume}
import org.ergoplatform.dex.markets.modules.AmmStatsMath
import org.ergoplatform.dex.markets.modules.PriceSolver.FiatPriceSolver
import org.ergoplatform.dex.markets.repositories.{Orders, Pools}
import tofu.doobie.transactor.Txr
import mouse.anyf._
import cats.syntax.traverse._
import org.ergoplatform.dex.markets.db.models.amm.{PoolSnapshot, PoolTrace, PoolVolumeSnapshot}
import org.ergoplatform.dex.domain.{AssetClass, CryptoUnits, FullAsset, MarketId}
import org.ergoplatform.dex.markets.modules.AmmStatsMath
import org.ergoplatform.dex.markets.services.TokenFetcher
import org.ergoplatform.ergo.TokenId
import org.ergoplatform.ergo.modules.ErgoNetwork
import tofu.doobie.transactor.Txr
import tofu.syntax.foption._
import tofu.syntax.monadic._
import tofu.syntax.time.now._
import mouse.anyf._
import cats.syntax.traverse._
import java.text.SimpleDateFormat
import java.util.concurrent.TimeUnit
import scala.concurrent.duration._

trait AmmStats[F[_]] {
Expand All @@ -45,6 +41,8 @@ trait AmmStats[F[_]] {

def getPoolPriceChart(poolId: PoolId, window: TimeWindow, resolution: Int): F[List[PricePoint]]

def getChartsByGap(gap: ChartGap, poolId: PoolId, window: TimeWindow): F[List[PricePoint]]

def getMarkets(window: TimeWindow): F[List[AmmMarketSummary]]

def getSwapTransactions(window: TimeWindow): F[TransactionsInfo]
Expand All @@ -67,7 +65,7 @@ object AmmStats {
fiatSolver: FiatPriceSolver[F]
): AmmStats[F] = new Live[F, D]()

final class Live[F[_]: Monad, D[_]: Monad](implicit
final class Live[F[_]: Monad: Clock, D[_]: Monad](implicit
txr: Txr.Aux[F, D],
pools: Pools[D],
orders: Orders[D],
Expand Down Expand Up @@ -225,6 +223,65 @@ object AmmStats {
}
}

def getChartsByGap(gap: ChartGap, poolId: PoolId, window: TimeWindow): F[List[PricePoint]] =
Clock[F].realTime(TimeUnit.MILLISECONDS).flatMap { now =>
val from = window.from.getOrElse(now - gap.minimalGap)
val to = window.to.getOrElse(from + gap.minimalGap)
val formatter = new SimpleDateFormat(gap.javaDateFormat)

val queryPoolData = for {
points <- pools.getChartsByGap(gap, poolId, from, to)
snapshots <- pools.snapshot(poolId)
latestOpt <- if (points.isEmpty) pools.getLatestChartByGap(gap, poolId).map {
_.map { asset =>
AvgAssetAmounts(asset.amountX, asset.amountY, formatter.parse(asset.timestamp).getTime, 0)
}
}
else noneF[D, AvgAssetAmounts]
} yield (latestOpt, points, snapshots)

txr
.trans(queryPoolData)
.map {
case (Some(latest: AvgAssetAmounts), Nil, s @ Some(_: PoolSnapshot)) =>
(latest :: Nil, s)
case (_, points: List[AvgAssetAmountsWithPrev], s @ Some(_: PoolSnapshot)) =>
val timeWindow: Long = to - from
val gapsNum: Int = (timeWindow / gap.timeWindow.toMillis).toInt
val roundedFrom: Long = ChartGap.round(gap, from)

(0 to gapsNum)
.foldLeft(roundedFrom, List.empty[AvgAssetAmounts]) { case ((currentTime, acc), _) =>
val point = points.find(p => formatter.parse(p.current).getTime == currentTime)

val avg: Option[AvgAssetAmounts] =
(acc, point) match {
case (Nil, None) =>
points.lastOption
.flatMap(_.getPrev(new SimpleDateFormat(gap.javaDateFormat)))
.map(_.copy(timestamp = currentTime))
case (x :: _, None) => x.copy(timestamp = currentTime) some
case (_, Some(p)) => AvgAssetAmounts(p.amountX, p.amountY, currentTime, 0).some
case _ => none
}

(ChartGap.updateWithGap(gap, currentTime), avg.fold(acc)(_ :: acc))
}
._2 -> s

case _ => List.empty -> none[PoolSnapshot]
}
.map {
case (amounts, Some(snap: PoolSnapshot)) =>
amounts.reverse.map { amount =>
val price =
RealPrice.calculate(amount.amountX, snap.lockedX.decimals, amount.amountY, snap.lockedY.decimals)
PricePoint(amount.timestamp, price.setScale(RealPrice.defaultScale))
}
case _ => List.empty
}
}

def getMarkets(window: TimeWindow): F[List[AmmMarketSummary]] = {
val queryPoolStats = for {
volumes <- pools.volumes(window)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.ergoplatform.dex.markets.api.v1

import java.time.{ZoneId, ZoneOffset}
import java.util.Locale

package object services {
val Zone: ZoneId = ZoneId.of("UTC")
val Utc: ZoneOffset = ZoneOffset.UTC
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import org.ergoplatform.dex.domain.{FullAsset, Ticker}
import org.ergoplatform.dex.domain.amm.PoolId
import org.ergoplatform.ergo.TokenId

import java.text.SimpleDateFormat

object amm {

final case class PoolInfo(confirmedAt: Long)
Expand Down Expand Up @@ -52,4 +54,28 @@ object amm {
timestamp: Long,
index: Long
)

final case class AvgAssetAmount(timestamp: String, amountX: Long, amountY: Long)

object AvgAssetAmounts {
def empty: AvgAssetAmounts = AvgAssetAmounts(0, 0, 0, 0)
}

final case class AvgAssetAmountsWithPrev(
current: String,
prev: Option[String],
amountX: Long,
prevX: Option[Long],
amountY: Long,
prevY: Option[Long]
) {

def getPrev(formatter: SimpleDateFormat): Option[AvgAssetAmounts] =
for {
ts <- prev
x <- prevX
y <- prevY
} yield AvgAssetAmounts(x, y, formatter.parse(ts).getTime, 0)
}

}
Loading

0 comments on commit e065d72

Please sign in to comment.