Skip to content

Commit

Permalink
Added simple charts to web server
Browse files Browse the repository at this point in the history
  • Loading branch information
jbaron committed Aug 13, 2023
1 parent 0fab9e8 commit 670e498
Show file tree
Hide file tree
Showing 10 changed files with 175 additions and 72 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import org.roboquant.Roboquant
import org.roboquant.common.TimeSeries
import java.io.File
import java.nio.charset.StandardCharsets
import java.text.DecimalFormat

/**
* Generate an HTML report that contains the recorded metrics of one or more runs. The report will contain both a
Expand All @@ -41,7 +42,12 @@ class MetricsReport(

private val charts
get() = logger.metricNames.map {
{ TimeSeriesChart(roboquant.logger.getMetric(it)) }
{
val data = roboquant.logger.getMetric(it)
val chart = TimeSeriesChart(data)
chart.title = it
chart
}
}

private fun createCells(name: String, value: Any): String {
Expand All @@ -50,9 +56,13 @@ class MetricsReport(

private fun getTableCell(entry: Map.Entry<String, TimeSeries>): String {
val splitName = entry.key.split('.')
val formatter = DecimalFormat("#.####")
val name = splitName.subList(1, splitName.size).joinToString(" ")
// val value = if (entry.value.isFinite()) entry.value.round(2).toString().removeSuffix(".00") else "NaN"
val value = 2.0
val data = entry.value.values
val value = if (data.isNotEmpty() && data.last().isFinite())
formatter.format(entry.value.last().value)
else
"NaN"
return createCells(name, value)
}

Expand All @@ -70,7 +80,7 @@ class MetricsReport(
(function() {
let elem = document.getElementById("$id");
const option = $fragment
let myChart = echarts.init(elem, theme);
let myChart = echarts.init(elem);
myChart.setOption(option);
let resizeObserver = new ResizeObserver(() => myChart.resize());
resizeObserver.observe(elem);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import java.math.RoundingMode
* Deprecated, use [TimeSeriesChart] instead
*/
@Deprecated("Renamed to TimeSeriesChart", ReplaceWith("TimeSeriesChart", "org.roboquant.charts.TimeSeriesChart"))
@Suppress("unused")
typealias MetricChart = TimeSeriesChart

/**
Expand Down
6 changes: 5 additions & 1 deletion roboquant-server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
<artifactId>roboquant-server</artifactId>
<packaging>jar</packaging>
<name>roboquant server</name>
<description>server to run roboquant algorithmic trading strategies</description>
<description>webserver to run roboquant algorithmic trading strategies</description>

<properties>
<ktor_version>2.3.3</ktor_version>
Expand All @@ -48,6 +48,10 @@
<groupId>org.roboquant</groupId>
<artifactId>roboquant</artifactId>
</dependency>
<dependency>
<groupId>org.roboquant</groupId>
<artifactId>roboquant-charts</artifactId>
</dependency>
<dependency>
<groupId>io.ktor</groupId>
<artifactId>ktor-server-core-jvm</artifactId>
Expand Down
16 changes: 16 additions & 0 deletions roboquant-server/src/main/kotlin/org/roboquant/server/hx.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package org.roboquant.server

import kotlinx.html.FORM
import kotlinx.html.HTMLTag

fun HTMLTag.hxGet(value: String) {
Expand All @@ -29,7 +30,22 @@ fun HTMLTag.hxTarget(value: String) {
attributes += "hx-target" to value
}

fun FORM.hxPost(value: String) {
attributes += "hx-post" to value
}

fun HTMLTag.hxBoost(value: Boolean) {
attributes += "hx-boost" to value.toString()
}


fun HTMLTag.echarts() {
attributes += "hx-ext" to "echarts"
}




fun HTMLTag.hxExt(value: Boolean) {
attributes += "hx-ext" to value.toString()
}
23 changes: 23 additions & 0 deletions roboquant-server/src/main/kotlin/org/roboquant/server/page.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,34 @@ package org.roboquant.server

import kotlinx.html.*

fun HEAD.echarts() {
script(type = ScriptType.textJavaScript) {
unsafe {
raw("""
htmx.defineExtension('echarts', {
onEvent : function(name, evt) {
if (name == 'htmx:beforeSwap') {
let option = JSON.parse(evt.detail.serverResponse);
let elem = evt.detail.target;
elem.style.setProperty('display', 'block');
let chart = echarts.getInstanceByDom(elem);
chart.setOption(option);
return false
}
return true;
}
});
""")
}
}
}

fun HTML.page(title: String, bodyFn: BODY.() -> Unit) {
lang = "en"
head {
script { src = "https://cdnjs.cloudflare.com/ajax/libs/htmx/1.9.4/htmx.min.js" }
script { src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/echarts.min.js"}
echarts()
title { +title }
link {
href = "https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
Expand Down
163 changes: 101 additions & 62 deletions roboquant-server/src/main/kotlin/org/roboquant/server/routes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,72 +19,56 @@ package org.roboquant.server
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.html.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.server.util.*
import kotlinx.html.*
import org.roboquant.brokers.lines
import org.roboquant.charts.TimeSeriesChart
import org.roboquant.charts.renderJson
import org.roboquant.orders.lines

/*
val blocks = mutableMapOf<String, FlowContent.() -> Unit>()
fun FlowContent.serverside(id:String, block: FlowContent.() -> Unit) {
blocks[id] = block
}
fun Application.routes() {
routing {
get("/serverside/{id}") {
val id = call.parameters["id"]!!
val block = blocks[id]!!
call.respond(HttpStatusCode.OK) {
createHTML().article {
this.block()
}
}
}
}
}
fun FlowContent.test() {
val x = 12
a(href = "/serverside/123") {
hxBoost(true)
serverside("123") {
p {
+"test $x"
}
}
}
}
*/

private fun FlowContent.table(caption: String, list: List<List<Any>>) {
table(classes = "table text-end") {
private fun FlowContent.table(caption: String, list: List<List<Any>>) {
table(classes = "table text-end my-4") {
caption {
+caption
}
tr {
for (header in list.first()) {
th { +header.toString() }
thead {
tr {
for (header in list.first()) {
th { +header.toString() }
}
}
}

for (row in list.drop(1)) {
tr {
for (c in row) {
td { +c.toString() }
tbody {
for (row in list.drop(1)) {
tr {
for (c in row) {
td { +c.toString() }
}
}
}
}
}
}


fun Route.getChart() {
post("/echarts") {
val parameters = call.receiveParameters()
val metric = parameters.getOrFail("metric")
val run = parameters.getOrFail("run")
val logger = runs.getValue(run).roboquant.logger
val data = logger.getMetric(metric)
val chart = TimeSeriesChart(data)
chart.title = metric
val json = chart.getOption().renderJson()
call.respondText(json)
}
}

fun Route.listRuns() {
get("/") {
val params = call.request.queryParameters
Expand All @@ -106,28 +90,28 @@ fun Route.listRuns() {
tr {
th { +"run name" }
th { +"state" }
th { +"events"}
th { +"last update"}
th { +"actions"}
th { +"events" }
th { +"last update" }
th { +"actions" }
}
runs.forEach { (run, info) ->
val acc = info.metric.account!!
val policy = info.roboquant.policy as PausablePolicy
val state = if (policy.pause) "pause" else "running"
tr {
td {+run}
td {+state}
td {+info.metric.events.toString()}
td {+acc.lastUpdate.toString()}
td { +run }
td { +state }
td { +info.metric.events.toString() }
td { +acc.lastUpdate.toString() }
td {
a(href = "/run/$run") {
+"details"
}
br{}
br {}
a(href = "/?action=pause&run=$run") {
+"pause"
}
br{}
br {}
a(href = "/?action=resume&run=$run") {
+"resume"
}
Expand All @@ -143,23 +127,78 @@ fun Route.listRuns() {
}


private fun FlowContent.echarts(id: String, width: String = "100%", height: String = "800px") {
div {
attributes["id"] = id
attributes["hx-ext"] = "echarts"
style = "width:$width;height:$height;"
}
script(type = ScriptType.textJavaScript) {
unsafe {
raw(
"""
(function() {
let elem = document.getElementById('$id')
let chart = echarts.init(elem);
let resizeObserver = new ResizeObserver(() => chart.resize());
resizeObserver.observe(elem);
elem.style.setProperty('display', 'none');
})();
""".trimIndent()
)
}
}
}

fun FlowContent.metricForm(target: String, run: String, metricNames: List<String>) {
form {
hxPost("/echarts")
hxTarget(target)
select(classes = "form-select") {
name = "metric"
metricNames.forEach {
option { value = it; +it }
}
}

input(type = InputType.hidden, name = "run") { value=run }

button(type = ButtonType.submit, classes = "btn btn-primary") {
+"Get Chart"

}
}
}


fun List<List<Any>>.takeLastPlusHeader(n: Int): List<List<Any>> {
return listOf(first()) + drop(1).takeLast(n)
}

fun Route.getRun() {
get("/run/{id}") {
val id = call.parameters["id"] ?: ""
val info = runs.getValue(id)
val metric = info.metric
val metricNames = info.roboquant.logger.metricNames
val acc = info.metric.account!!
call.respondHtml(HttpStatusCode.OK) {
page("Details $id") {
a(href="/") {
+"Back to overview"
a(href = "/") { +"Back to overview" }
table("account summary", metric.getsummary())
div(classes = "row my-4") {
div(classes = "col-2") {
metricForm("#echarts123456", id, metricNames)
}
div(classes = "col-10") {
echarts("echarts123456", height = "400px")
}
}
table("account summary",metric.getsummary())
table("cash", metric.getCash())
table("open positions", acc.positions.lines())
table("open orders", acc.openOrders.lines())
table("closed orders", acc.closedOrders.lines())
table("trades", acc.trades.lines())
table("closed orders", acc.closedOrders.lines().takeLastPlusHeader(10))
table("trades", acc.trades.lines().takeLastPlusHeader(10))
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal fun Application.setup() {
routing {
listRuns()
getRun()
getChart()
}
}

Expand Down Expand Up @@ -64,6 +65,7 @@ internal fun Application.setupSecure(username: String, password: String) {
authenticate("auth-digest") {
listRuns()
getRun()
getChart()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,6 @@ fun main() {
jobs.joinAllBlocking()
server.stop()
feed.close()
}
}


Loading

0 comments on commit 670e498

Please sign in to comment.