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"
|
"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)
|
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)
|
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)
|
middleware.LNDGuard)
|
||||||
GET(e, sc, "/api/market/:id", handler.HandleMarket)
|
GET(e, sc, "/api/market/:id", handler.HandleMarket)
|
||||||
GET(e, sc, "/api/market/:id/orders", handler.HandleMarketOrders)
|
GET(e, sc, "/api/market/:id/orders", handler.HandleMarketOrders)
|
||||||
|
GET(e, sc, "/api/market/:id/stats", handler.HandleMarketStats)
|
||||||
POST(e, sc, "/api/order",
|
POST(e, sc, "/api/order",
|
||||||
handler.HandleOrder,
|
handler.HandleOrder,
|
||||||
middleware.SessionGuard,
|
middleware.SessionGuard,
|
||||||
|
|
|
@ -9,12 +9,14 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitejs/plugin-vue": "^4.4.0",
|
"@vitejs/plugin-vue": "^4.4.0",
|
||||||
|
"chart.js": "^4.4.0",
|
||||||
"core-js": "^3.8.3",
|
"core-js": "^3.8.3",
|
||||||
"pinia": "^2.1.7",
|
"pinia": "^2.1.7",
|
||||||
"register-service-worker": "^1.7.2",
|
"register-service-worker": "^1.7.2",
|
||||||
"s-ago": "^2.2.0",
|
"s-ago": "^2.2.0",
|
||||||
"vite": "^4.5.0",
|
"vite": "^4.5.0",
|
||||||
"vue": "^3.2.13",
|
"vue": "^3.2.13",
|
||||||
|
"vue-chartjs": "^5.2.0",
|
||||||
"vue-router": "4"
|
"vue-router": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
<nav>
|
<nav>
|
||||||
<StyledLink :to="'/market/' + marketId + '/form'">form</StyledLink>
|
<StyledLink :to="'/market/' + marketId + '/form'">form</StyledLink>
|
||||||
<StyledLink :to="'/market/' + marketId + '/orders'">orders</StyledLink>
|
<StyledLink :to="'/market/' + marketId + '/orders'">orders</StyledLink>
|
||||||
|
<StyledLink :to="'/market/' + marketId + '/stats'">stats</StyledLink>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
<Suspense>
|
<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 UserOrders from '@/components/UserOrders'
|
||||||
import OrderForm from '@/components/OrderForm'
|
import OrderForm from '@/components/OrderForm'
|
||||||
import MarketOrders from '@/components/MarketOrders'
|
import MarketOrders from '@/components/MarketOrders'
|
||||||
|
import MarketStats from '@/components/MarketStats'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
|
@ -37,7 +38,8 @@ const routes = [
|
||||||
component: MarketView,
|
component: MarketView,
|
||||||
children: [
|
children: [
|
||||||
{ path: 'form', name: 'form', component: OrderForm },
|
{ 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/resolve-uri" "^3.1.0"
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.14"
|
"@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":
|
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
|
||||||
version "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"
|
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"
|
ansi-styles "^4.1.0"
|
||||||
supports-color "^7.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:
|
chokidar@^3.5.3:
|
||||||
version "3.5.3"
|
version "3.5.3"
|
||||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||||
|
@ -4582,6 +4594,11 @@ vite@^4.5.0:
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
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:
|
vue-demi@>=0.14.5:
|
||||||
version "0.14.6"
|
version "0.14.6"
|
||||||
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92"
|
resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.6.tgz#dc706582851dc1cdc17a0054f4fec2eb6df74c92"
|
||||||
|
|
Loading…
Reference in New Issue