Add market stats page
This commit is contained in:
parent
8e3492d560
commit
c9de76ac75
47
db/market.go
47
db/market.go
|
@ -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, ×tamp, &score)
|
||||
stat.X = timestamp
|
||||
stat.Y = map[string]int{
|
||||
description: score,
|
||||
}
|
||||
log.Println(timestamp, description, score)
|
||||
*stats = append(*stats, stat)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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 }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue