Add market stats page

This commit is contained in:
ekzyis 2023-11-29 04:11:44 +01:00
parent 8e3492d560
commit c9de76ac75
8 changed files with 191 additions and 1 deletions

View File

@ -219,3 +219,50 @@ func (db *DB) FindOrderMatches(tx *sql.Tx, ctx context.Context, o1 *Order, o2 *O
"ORDER BY o.created_at ASC LIMIT 1"
return tx.QueryRowContext(ctx, query, o1.Pubkey, o1.Quantity, o1.Share.MarketId, o1.ShareId, o1.Side, o1.Price).Scan(&o2.Id)
}
// [
//
// { "x": <timestamp>, "y": { <share_description>: <score>, ... } },
//
// ]
type MarketStat struct {
X time.Time `json:"x"`
Y map[string]int `json:"y"`
}
type MarketStats = []MarketStat
func (db *DB) FetchMarketStats(marketId int64, stats *MarketStats) error {
query := "" +
"SELECT " +
"s.description, " +
"GREATEST(i.confirmed_at, i2.confirmed_at) AS confirmed_at, " +
"SUM(o.price * o.quantity) OVER (PARTITION BY o.share_id ORDER BY o.created_at ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS score " +
"FROM orders o " +
"JOIN orders o2 ON o2.id = o.order_id " +
"JOIN shares s ON s.id = o.share_id " +
"JOIN invoices i ON i.id = o.invoice_id " +
"JOIN invoices i2 ON i2.id = o2.invoice_id " +
"WHERE s.market_id = $1 AND i.confirmed_at IS NOT NULL AND o.order_id IS NOT NULL ORDER BY i.confirmed_at ASC"
rows, err := db.Query(query, marketId)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var stat MarketStat
var (
timestamp time.Time
description string
score int
)
rows.Scan(&description, &timestamp, &score)
stat.X = timestamp
stat.Y = map[string]int{
description: score,
}
log.Println(timestamp, description, score)
*stats = append(*stats, stat)
}
return nil
}

View File

@ -228,3 +228,20 @@ func HandleOrders(sc context.ServerContext) echo.HandlerFunc {
return c.JSON(http.StatusOK, orders)
}
}
func HandleMarketStats(sc context.ServerContext) echo.HandlerFunc {
return func(c echo.Context) error {
var (
marketId int64
stats db.MarketStats
err error
)
if marketId, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, "Bad Request")
}
if err = sc.Db.FetchMarketStats(marketId, &stats); err != nil {
return err
}
return c.JSON(http.StatusOK, stats)
}
}

View File

@ -47,6 +47,7 @@ func addBackendRoutes(e *echo.Echo, sc ServerContext) {
middleware.LNDGuard)
GET(e, sc, "/api/market/:id", handler.HandleMarket)
GET(e, sc, "/api/market/:id/orders", handler.HandleMarketOrders)
GET(e, sc, "/api/market/:id/stats", handler.HandleMarketStats)
POST(e, sc, "/api/order",
handler.HandleOrder,
middleware.SessionGuard,

View File

@ -9,12 +9,14 @@
},
"dependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"chart.js": "^4.4.0",
"core-js": "^3.8.3",
"pinia": "^2.1.7",
"register-service-worker": "^1.7.2",
"s-ago": "^2.2.0",
"vite": "^4.5.0",
"vue": "^3.2.13",
"vue-chartjs": "^5.2.0",
"vue-router": "4"
},
"devDependencies": {

View File

@ -14,6 +14,7 @@
<nav>
<StyledLink :to="'/market/' + marketId + '/form'">form</StyledLink>
<StyledLink :to="'/market/' + marketId + '/orders'">orders</StyledLink>
<StyledLink :to="'/market/' + marketId + '/stats'">stats</StyledLink>
</nav>
</header>
<Suspense>

View File

@ -0,0 +1,103 @@
<template>
<div>
<div class="mb-1">
<span>YES: {{ currentYes * 100 }}%</span>
<span>NO: {{ currentNo * 100 }}%</span>
</div>
<div class="mb-2">
<span>Volume: {{ volume }} sats</span>
</div>
<Line :data="chartData" :options="chartOptions" :plugins="chartPlugins" />
</div>
</template>
<script setup>
import { ref } from 'vue'
import { useRoute } from 'vue-router'
import { Line } from 'vue-chartjs'
import { Chart as ChartJS, Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, LineElement, PointElement } from 'chart.js'
import ago from 's-ago'
ChartJS.register(Title, Tooltip, Legend, BarElement, CategoryScale, LinearScale, TimeScale, LineElement, PointElement)
const route = useRoute()
const marketId = route.params.id
const stats = ref([])
const url = '/api/market/' + marketId + '/stats'
await fetch(url)
.then(r => r.json())
.then(body => {
stats.value = body
})
.catch(console.error)
const getFilterData = key => {
const y = []
for (let i = 0; i < stats.value.length - 1; i += 2) {
const s1 = stats.value[i]
const s2 = stats.value[i + 1]
const sum = s1.y.YES + s2.y.NO
volume = sum
key === 'YES' ? y.push(s1.y.YES / sum) : y.push(s2.y.NO / sum)
}
return y
}
let volume = 0
const yesData = getFilterData('YES')
const noData = getFilterData('NO')
const currentYes = yesData.at(-1)
const currentNo = noData.at(-1)
const chartData = {
labels: stats.value.map(({ x }) => ago(new Date(x))),
datasets: [
{
label: 'YES',
borderColor: '#35df8d',
backgroundColor: '#35df8d',
data: yesData,
fill: 'origin'
},
{
label: 'NO',
borderColor: '#ff7386',
backgroundColor: '#ff7386',
data: noData,
fill: 'origin'
}
]
}
const chartOptions = {
responsive: true,
plugins: {
background: {
color: 'white'
}
}
}
const chartPlugins = [{
id: 'background',
beforeDraw: (chart, args, options) => {
const { ctx } = chart
ctx.save()
ctx.globalCompositeOperation = 'destination-over'
ctx.fillStyle = options.color
ctx.fillRect(0, 0, chart.width, chart.height)
ctx.restore()
}
}]
</script>
<style scoped>
span {
margin: 0 0.5em;
}
canvas {
width: auto;
}
</style>

View File

@ -15,6 +15,7 @@ import UserInvoices from '@/components/UserInvoices'
import UserOrders from '@/components/UserOrders'
import OrderForm from '@/components/OrderForm'
import MarketOrders from '@/components/MarketOrders'
import MarketStats from '@/components/MarketStats'
const routes = [
{
@ -37,7 +38,8 @@ const routes = [
component: MarketView,
children: [
{ path: 'form', name: 'form', component: OrderForm },
{ path: 'orders', name: 'market-orders', component: MarketOrders }
{ path: 'orders', name: 'market-orders', component: MarketOrders },
{ path: 'stats', name: 'market-stats', component: MarketStats }
]
},
{

View File

@ -1205,6 +1205,11 @@
"@jridgewell/resolve-uri" "^3.1.0"
"@jridgewell/sourcemap-codec" "^1.4.14"
"@kurkle/color@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f"
integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1"
resolved "https://registry.yarnpkg.com/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"
@ -2045,6 +2050,13 @@ chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.2:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chart.js@^4.4.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.0.tgz#df843fdd9ec6bd88d7f07e2b95348d221bd2698c"
integrity sha512-vQEj6d+z0dcsKLlQvbKIMYFHd3t8W/7L2vfJIbYcfyPcRx92CsHqECpueN8qVGNlKyDcr5wBrYAYKnfu/9Q1hQ==
dependencies:
"@kurkle/color" "^0.3.0"
chokidar@^3.5.3:
version "3.5.3"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
@ -4582,6 +4594,11 @@ vite@^4.5.0:
optionalDependencies:
fsevents "~2.3.2"
vue-chartjs@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/vue-chartjs/-/vue-chartjs-5.2.0.tgz#3d0076ccf8016d1bf8fab5ccd837e7fb81005ded"
integrity sha512-d3zpKmGZr2OWHQ1xmxBcAn5ShTG917+/UCLaSpaCDDqT0U7DBsvFzTs69ZnHCgKoXT55GZDW8YEj9Av+dlONLA==
vue-demi@>=0.14.5:
version "0.14.6"
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92"