LogStatus
Refreshes the status bar of a backtest or live trading page with text, tables, buttons or component objects with a type (KPI cards, charts, tabs and more); each call replaces the previous content.
LogStatus(...msgs)Examples
-
Supports setting the color of the output content.
javascriptfunction main() { LogStatus('This is a normal status message') LogStatus('This is a red font status message#ff0000') LogStatus('This is a multi-line status message\nI am the second line') }pythondef main(): LogStatus('This is a normal status message') LogStatus('This is a red font status message#ff0000') LogStatus('This is a multi-line status message\nI am the second line')rustfn main() { LogStatus!("This is a normal status message"); LogStatus!("This is a red font status message#ff0000"); LogStatus!("This is a multi-line status message\nI am the second line"); } -
Pass a component object directly: a line of text followed by a last-price KPI card.
javascriptfunction main() { var ticker = exchange.GetTicker() if (!ticker) return // Pass the component object directly: no JSON.stringify, no backticks LogStatus("Updated " + _D(), {type: "kpi", title: "Last", value: ticker.Last}) // Output: the text "Updated ..." followed by a last-price KPI card }pythondef main(): ticker = exchange.GetTicker() if not ticker: return # Pass the component dict directly: no json.dumps, no backticks LogStatus("Updated " + _D(), {"type": "kpi", "title": "Last", "value": ticker["Last"]}) # Output: the text "Updated ..." followed by a last-price KPI cardrustfn main() { let Ok(t) = exchange.GetTicker(None) else { return }; // The component is one-line JSON text after the text, without backticks let kpi = format!(r#"{{"type": "kpi", "title": "Last", "value": {}}}"#, t.Last); LogStatus!(format!("Updated {}", _D(None)), kpi); // Output: the text "Updated ..." followed by a last-price KPI card } -
A dashboard: a grid of KPI cards, an equity curve and a positions table.
javascriptfunction main() { exchange.SetContractType("swap") var times = [], curve = [], f2 = {precision: 2} while (true) { var account = exchange.GetAccount() var positions = exchange.GetPositions() if (account && positions) { var profit = 0 var rows = positions.map(function (p) { profit += p.Profit var side = p.Type == PD_LONG || p.Type == PD_LONG_YD ? "Long" : "Short" return [p.Symbol, side, p.Amount, p.Price, p.Profit] }) // Futures accounts report Equity; otherwise use available + frozen var equity = account.Equity || account.Balance + account.FrozenBalance times.push(Date.now()) curve.push(equity) if (curve.length > 500) { times.shift(); curve.shift() } var color = profit >= 0 ? "up" : "down" LogStatus({type: "grid", cols: 3, items: [ {type: "kpi", title: "Equity", value: equity, unit: "USDT", format: f2}, {type: "kpi", title: "Available", value: account.Balance, format: f2}, {type: "kpi", title: "Floating PnL", value: profit, color: color}, {type: "chart", span: 3, kind: "area", title: "Equity curve", x: times, series: [{name: "Equity", data: curve}]}, {type: "table", span: 3, title: "Positions", rows: rows, cols: ["Symbol", "Side", "Amount", "Avg price", "Floating PnL"]} ]}) } Sleep(60 * 1000) } }pythonimport time def main(): exchange.SetContractType("swap") times, curve, f2 = [], [], {"precision": 2} while True: account = exchange.GetAccount() positions = exchange.GetPositions() if account and positions is not None: rows, profit = [], 0 for p in positions: profit += p["Profit"] side = "Long" if p["Type"] in (PD_LONG, PD_LONG_YD) else "Short" rows.append([p["Symbol"], side, p["Amount"], p["Price"], p["Profit"]]) # Futures accounts report Equity; otherwise use available + frozen equity = account.get("Equity") or account["Balance"] + account["FrozenBalance"] times.append(int(time.time() * 1000)) curve.append(equity) del times[:-500], curve[:-500] color = "up" if profit >= 0 else "down" LogStatus({"type": "grid", "cols": 3, "items": [ {"type": "kpi", "title": "Equity", "value": equity, "unit": "USDT", "format": f2}, {"type": "kpi", "title": "Available", "value": account["Balance"], "format": f2}, {"type": "kpi", "title": "Floating PnL", "value": profit, "color": color}, {"type": "chart", "span": 3, "kind": "area", "title": "Equity curve", "x": times, "series": [{"name": "Equity", "data": curve}]}, {"type": "table", "span": 3, "title": "Positions", "rows": rows, "cols": ["Symbol", "Side", "Amount", "Avg price", "Floating PnL"]} ]}) Sleep(60 * 1000)rustfn join<T: ToString>(v: &[T]) -> String { v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(",") } fn main() { if exchange.SetContractType("swap").is_err() { return; } let (mut times, mut curve) = (Vec::new(), Vec::new()); loop { if let (Ok(a), Ok(ps)) = (exchange.GetAccount(), exchange.GetPositions(None)) { let profit: f64 = ps.iter().map(|p| p.Profit).sum(); let rows: Vec<String> = ps.iter().map(|p| { let long = p.Type == PD_LONG || p.Type == PD_LONG_YD; let side = if long { "Long" } else { "Short" }; format!(r#"["{}","{}",{},{},{}]"#, p.Symbol, side, p.Amount, p.Price, p.Profit) }).collect(); // Futures accounts report Equity; otherwise use available + frozen let equity = if a.Equity > 0.0 { a.Equity } else { a.Balance + a.FrozenBalance }; times.push(UnixNano() / 1_000_000); curve.push(equity); if curve.len() > 500 { times.remove(0); curve.remove(0); } let color = if profit >= 0.0 { "up" } else { "down" }; LogStatus!(format!(r#"{{"type": "grid", "cols": 3, "items": [ {{"type": "kpi", "title": "Equity", "value": {}, "unit": "USDT", "format": {{"precision": 2}}}}, {{"type": "kpi", "title": "Available", "value": {}, "format": {{"precision": 2}}}}, {{"type": "kpi", "title": "Floating PnL", "value": {}, "color": "{}"}}, {{"type": "chart", "span": 3, "kind": "area", "title": "Equity curve", "x": [{}], "series": [{{"name": "Equity", "data": [{}]}}]}}, {{"type": "table", "span": 3, "title": "Positions", "rows": [{}], "cols": ["Symbol", "Side", "Amount", "Avg price", "Floating PnL"]}}]}}"#, equity, a.Balance, profit, color, join(×), join(&curve), rows.join(","))); } Sleep(60 * 1000); } } -
Enhanced table: column formats, up/down colors and units; clicking a header sorts by the raw numbers.
javascriptfunction main() { exchange.SetContractType("swap") var positions = exchange.GetPositions() if (!positions) return var rows = positions.map(function (p) { var ratio = p.Margin > 0 ? p.Profit / p.Margin : 0 // Cell object: unit and color for this cell only var lever = {value: p.MarginLevel, unit: "x", color: p.MarginLevel > 10 ? "warning" : "muted"} return [p.Symbol, p.Amount, p.Price, p.Profit, ratio, lever] }) var f2 = {precision: 2}, pct = {percent: true, precision: 2} // A table on its own: JavaScript and Python add the backticks automatically LogStatus({type: "table", title: "Positions", rows: rows, cols: [ "Symbol", {title: "Amount", align: "right"}, {title: "Avg price", format: f2, align: "right"}, {title: "Floating PnL", format: f2, color: "sign", unit: "USDT", align: "right"}, {title: "Return", format: pct, color: "sign", align: "right"}, {title: "Leverage", align: "right"} ]}) // Output: PnL and return colored by sign, sorted by value rather than text }pythondef main(): exchange.SetContractType("swap") positions = exchange.GetPositions() if positions is None: return rows = [] for p in positions: ratio = p["Profit"] / p["Margin"] if p["Margin"] > 0 else 0 # Cell object: unit and color for this cell only lever = {"value": p["MarginLevel"], "unit": "x", "color": "warning" if p["MarginLevel"] > 10 else "muted"} rows.append([p["Symbol"], p["Amount"], p["Price"], p["Profit"], ratio, lever]) f2, pct = {"precision": 2}, {"percent": True, "precision": 2} # A table on its own: JavaScript and Python add the backticks automatically LogStatus({"type": "table", "title": "Positions", "rows": rows, "cols": [ "Symbol", {"title": "Amount", "align": "right"}, {"title": "Avg price", "format": f2, "align": "right"}, {"title": "Floating PnL", "format": f2, "color": "sign", "unit": "USDT", "align": "right"}, {"title": "Return", "format": pct, "color": "sign", "align": "right"}, {"title": "Leverage", "align": "right"} ]}) # Output: PnL and return colored by sign, sorted by value rather than textrustfn main() { if exchange.SetContractType("swap").is_err() { return; } let Ok(positions) = exchange.GetPositions(None) else { return }; let rows: Vec<String> = positions.iter().map(|p| { let ratio = if p.Margin > 0.0 { p.Profit / p.Margin } else { 0.0 }; let warn = if p.MarginLevel > 10.0 { "warning" } else { "muted" }; // Cell object: unit and color for this cell only let lever = format!(r#"{{"value": {}, "unit": "x", "color": "{}"}}"#, p.MarginLevel, warn); format!(r#"["{}", {}, {}, {}, {}, {}]"#, p.Symbol, p.Amount, p.Price, p.Profit, ratio, lever) }).collect(); let table = format!(r#"{{"type": "table", "title": "Positions", "rows": [{}], "cols": [ "Symbol", {{"title": "Amount", "align": "right"}}, {{"title": "Avg price", "format": {{"precision": 2}}, "align": "right"}}, {{"title": "Floating PnL", "format": {{"precision": 2}}, "color": "sign", "unit": "USDT", "align": "right"}}, {{"title": "Return", "format": {{"percent": true, "precision": 2}}, "color": "sign", "align": "right"}}, {{"title": "Leverage", "align": "right"}}]}}"#, rows.join(",")); // A table on its own needs backticks, and the JSON inside must be one line LogStatus!(format!("`{}`", table.replace('\n', ""))); // Output: PnL and return colored by sign, sorted by value rather than text } -
Tabs for market data, positions and the account; the selected tab survives refreshes.
javascriptfunction main() { exchange.SetContractType("swap") while (true) { var ticker = exchange.GetTicker() var account = exchange.GetAccount() var positions = exchange.GetPositions() if (ticker && account && positions) { var rows = positions.map(function (p) { return [p.Symbol, p.Amount, p.Profit] }) var table = {type: "table", cols: ["Symbol", "Amount", "Floating PnL"], rows: rows} LogStatus({type: "tabs", id: "main", items: [ {title: "Market", cols: 3, items: [ {type: "kpi", title: "Last", value: ticker.Last}, {type: "kpi", title: "Bid", value: ticker.Buy}, {type: "kpi", title: "Ask", value: ticker.Sell} ]}, {title: "Positions", items: [table]}, {title: "Account", items: [{type: "stat", cols: 2, items: [ {label: "Available", value: account.Balance}, {label: "Frozen", value: account.FrozenBalance} ]}]} ]}) } Sleep(5000) } }pythondef main(): exchange.SetContractType("swap") while True: ticker = exchange.GetTicker() account = exchange.GetAccount() positions = exchange.GetPositions() if ticker and account and positions is not None: rows = [[p["Symbol"], p["Amount"], p["Profit"]] for p in positions] table = {"type": "table", "cols": ["Symbol", "Amount", "Floating PnL"], "rows": rows} LogStatus({"type": "tabs", "id": "main", "items": [ {"title": "Market", "cols": 3, "items": [ {"type": "kpi", "title": "Last", "value": ticker["Last"]}, {"type": "kpi", "title": "Bid", "value": ticker["Buy"]}, {"type": "kpi", "title": "Ask", "value": ticker["Sell"]} ]}, {"title": "Positions", "items": [table]}, {"title": "Account", "items": [{"type": "stat", "cols": 2, "items": [ {"label": "Available", "value": account["Balance"]}, {"label": "Frozen", "value": account["FrozenBalance"]} ]}]} ]}) Sleep(5000)rustfn main() { if exchange.SetContractType("swap").is_err() { return; } loop { let t = exchange.GetTicker(None); let (a, ps) = (exchange.GetAccount(), exchange.GetPositions(None)); if let (Ok(t), Ok(a), Ok(ps)) = (t, a, ps) { let rows: Vec<String> = ps.iter() .map(|p| format!(r#"["{}", {}, {}]"#, p.Symbol, p.Amount, p.Profit)) .collect(); LogStatus!(format!(r#"{{"type": "tabs", "id": "main", "items": [ {{"title": "Market", "cols": 3, "items": [{{"type": "kpi", "title": "Last", "value": {}}}, {{"type": "kpi", "title": "Bid", "value": {}}}, {{"type": "kpi", "title": "Ask", "value": {}}}]}}, {{"title": "Positions", "items": [{{"type": "table", "cols": ["Symbol", "Amount", "Floating PnL"], "rows": [{}]}}]}}, {{"title": "Account", "items": [{{"type": "stat", "cols": 2, "items": [ {{"label": "Available", "value": {}}}, {{"label": "Frozen", "value": {}}}]}}]}}]}}"#, t.Last, t.Buy, t.Sell, rows.join(","), a.Balance, a.FrozenBalance)); } Sleep(5000); } } -
Several charts: sparkline, gauge, progress, pie, histogram and heatmap.
javascriptfunction main() { var r = exchange.GetRecords(PERIOD_H1) var account = exchange.GetAccount() if (!r || r.length < 73 || !account) return r = r.slice(-73) var closes = r.map(function (b) { return b.Close }) var rets = closes.slice(1).map(function (c, i) { return c / closes[i] - 1 }) var day = r.slice(-24) var hi = Math.max.apply(null, day.map(function (b) { return b.High })) var lo = Math.min.apply(null, day.map(function (b) { return b.Low })) var pos = hi > lo ? (closes[72] - lo) / (hi - lo) : 0.5 var total = account.Balance + account.FrozenBalance var frozen = total > 0 ? account.FrozenBalance / total : 0 var pct = {percent: true, precision: 2} LogStatus({type: "grid", cols: 3, items: [ {type: "sparkline", title: "Last 24 hours", data: closes.slice(-24)}, {type: "gauge", title: "Position in 24h range", value: pos, format: {percent: true}, thresholds: [{value: 0.8, color: "up"}]}, {type: "progress", title: "Frozen share", value: frozen}, {type: "pie", title: "Funds", items: [ {name: "Available", value: account.Balance}, {name: "Frozen", value: account.FrozenBalance}]}, {type: "histogram", title: "Hourly change distribution", data: rets, format: pct}, {type: "heatmap", title: "Hourly change", format: pct, x: day.map(function (b) { return _D(b.Time, "hh:00") }), y: ["2 days ago", "Yesterday", "Today"], z: [rets.slice(0, 24), rets.slice(24, 48), rets.slice(48, 72)]} ]}) }pythondef main(): r = exchange.GetRecords(PERIOD_H1) account = exchange.GetAccount() if not r or len(r) < 73 or not account: return r = r[-73:] closes = [b["Close"] for b in r] rets = [closes[i] / closes[i - 1] - 1 for i in range(1, 73)] day = r[-24:] hi, lo = max(b["High"] for b in day), min(b["Low"] for b in day) pos = (closes[72] - lo) / (hi - lo) if hi > lo else 0.5 total = account["Balance"] + account["FrozenBalance"] frozen = account["FrozenBalance"] / total if total > 0 else 0 pct = {"percent": True, "precision": 2} LogStatus({"type": "grid", "cols": 3, "items": [ {"type": "sparkline", "title": "Last 24 hours", "data": closes[-24:]}, {"type": "gauge", "title": "Position in 24h range", "value": pos, "format": {"percent": True}, "thresholds": [{"value": 0.8, "color": "up"}]}, {"type": "progress", "title": "Frozen share", "value": frozen}, {"type": "pie", "title": "Funds", "items": [ {"name": "Available", "value": account["Balance"]}, {"name": "Frozen", "value": account["FrozenBalance"]}]}, {"type": "histogram", "title": "Hourly change distribution", "data": rets, "format": pct}, {"type": "heatmap", "title": "Hourly change", "format": pct, "x": [_D(b["Time"] / 1000, "%H:00") for b in day], "y": ["2 days ago", "Yesterday", "Today"], "z": [rets[0:24], rets[24:48], rets[48:72]]} ]})rustfn join<T: ToString>(v: &[T]) -> String { v.iter().map(|x| x.to_string()).collect::<Vec<_>>().join(",") } fn main() { let Ok(r) = exchange.GetRecords(None, PERIOD_H1, None) else { return }; let Ok(a) = exchange.GetAccount() else { return }; if r.len() < 73 { return; } let r = &r[r.len() - 73..]; let closes: Vec<f64> = r.iter().map(|b| b.Close).collect(); let rets: Vec<f64> = closes.windows(2).map(|w| w[1] / w[0] - 1.0).collect(); let day = &r[49..]; let hi = day.iter().map(|b| b.High).fold(f64::MIN, f64::max); let lo = day.iter().map(|b| b.Low).fold(f64::MAX, f64::min); let pos = if hi > lo { (closes[72] - lo) / (hi - lo) } else { 0.5 }; let total = a.Balance + a.FrozenBalance; let frozen = if total > 0.0 { a.FrozenBalance / total } else { 0.0 }; // Rust's _D() always prints UTC time let hours: Vec<String> = day.iter() .map(|b| format!("\"{}:00\"", &_D(b.Time)[11..13])) .collect(); let z = format!("[{}],[{}],[{}]", join(&rets[..24]), join(&rets[24..48]), join(&rets[48..])); LogStatus!(format!(r#"{{"type": "grid", "cols": 3, "items": [ {{"type": "sparkline", "title": "Last 24 hours", "data": [{}]}}, {{"type": "gauge", "title": "Position in 24h range", "value": {}, "format": {{"percent": true}}, "thresholds": [{{"value": 0.8, "color": "up"}}]}}, {{"type": "progress", "title": "Frozen share", "value": {}}}, {{"type": "pie", "title": "Funds", "items": [{{"name": "Available", "value": {}}}, {{"name": "Frozen", "value": {}}}]}}, {{"type": "histogram", "title": "Hourly change distribution", "data": [{}], "format": {{"percent": true, "precision": 2}}}}, {{"type": "heatmap", "title": "Hourly change", "format": {{"percent": true, "precision": 2}}, "x": [{}], "y": ["2 days ago", "Yesterday", "Today"], "z": [{}]}}]}}"#, join(&closes[49..]), pos, frozen, a.Balance, a.FrozenBalance, join(&rets), hours.join(","), z)); } -
A title line with badges, followed by an alert, a Markdown note and a timeline of events.
javascriptfunction main() { var events = [{time: Date.now(), text: "Strategy started", color: "primary"}] while (true) { var account = exchange.GetAccount() if (account) { var low = account.Balance < 100 if (low) { events.unshift({time: Date.now(), text: "Funds below 100", color: "down"}) } events = events.slice(0, 10) var alert = low ? {type: "alert", level: "warning", title: "Low funds", text: "Below 100, opening paused"} : {type: "alert", level: "success", title: "Risk check passed"} // "\n" splits lines: text and badges, then stacked components LogStatus("Grid strategy", {type: "badge", items: [ {text: IsVirtual() ? "Backtest" : "Live", color: "muted"}, {text: low ? "Paused" : "Running", color: low ? "warning" : "up"} ]}, "\n", [ alert, {type: "markdown", text: "#### Notes\n- Spacing **0.5%**\n- 0.01 per grid"}, {type: "timeline", title: "Recent events", items: events} ]) } Sleep(10 * 1000) } }pythonimport time def main(): now = lambda: int(time.time() * 1000) events = [{"time": now(), "text": "Strategy started", "color": "primary"}] while True: account = exchange.GetAccount() if account: low = account["Balance"] < 100 if low: events.insert(0, {"time": now(), "text": "Funds below 100", "color": "down"}) del events[10:] if low: alert = {"type": "alert", "level": "warning", "title": "Low funds", "text": "Below 100, opening paused"} else: alert = {"type": "alert", "level": "success", "title": "Risk check passed"} # "\n" splits lines: text and badges, then stacked components LogStatus("Grid strategy", {"type": "badge", "items": [ {"text": "Backtest" if IsVirtual() else "Live", "color": "muted"}, {"text": "Paused" if low else "Running", "color": "warning" if low else "up"} ]}, "\n", [ alert, {"type": "markdown", "text": "#### Notes\n- Spacing **0.5%**\n- 0.01 per grid"}, {"type": "timeline", "title": "Recent events", "items": events} ]) Sleep(10 * 1000)rustfn item(text: &str, color: &str) -> String { format!(r#"{{"text": "{}", "color": "{}"}}"#, text, color) } fn ev(text: &str, color: &str) -> String { let ms = UnixNano() / 1_000_000; format!(r#"{{"time": {}, "text": "{}", "color": "{}"}}"#, ms, text, color) } fn main() { let mut events = vec![ev("Strategy started", "primary")]; loop { if let Ok(a) = exchange.GetAccount() { let low = a.Balance < 100.0; if low { events.insert(0, ev("Funds below 100", "down")); events.truncate(10); } let mode = if IsVirtual() { "Backtest" } else { "Live" }; let (state, color) = if low { ("Paused", "warning") } else { ("Running", "up") }; let badge = format!(r#"{{"type": "badge", "items": [{}, {}]}}"#, item(mode, "muted"), item(state, color)); let (level, title, text) = match low { true => ("warning", "Low funds", "Below 100, opening paused"), false => ("success", "Risk check passed", ""), }; let alert = format!( r#"{{"type": "alert", "level": "{}", "title": "{}", "text": "{}"}}"#, level, title, text ); let notes = r"#### Notes\n- Spacing **0.5%**\n- 0.01 per grid"; let md = format!(r#"{{"type": "markdown", "text": "{}"}}"#, notes); let tl = format!(r#"{{"type": "timeline", "title": "Recent events", "items": [{}]}}"#, events.join(",")); // "\n" splits lines: text and badges, then stacked components (one line each) LogStatus!("Grid strategy", badge, "\n", format!("[{},{},{}]", alert, md, tl)); } Sleep(10 * 1000); } } -
Mixing with the old style: a backtick table, a button and new components on separate lines.
javascriptfunction main() { var account = exchange.GetAccount() if (!account) return var table = {type: "table", title: "Account", cols: ["Available", "Frozen"], rows: [[account.Balance, account.FrozenBalance]]} var btn = {type: "button", cmd: "pause", name: "Pause"} LogStatus( "`" + JSON.stringify(table) + "`", // Old style: backtick JSON, shown as before "\nSummary", {type: "grid", cols: 3, items: [ // New components go at the end of the line {type: "kpi", title: "Available", value: account.Balance}, table, btn // No backticks inside a grid ]}, "\n`" + JSON.stringify(btn) + "`" ) // Output: the old table, then text with a component grid, then a button read with GetCommand() }pythonimport json def main(): account = exchange.GetAccount() if not account: return table = {"type": "table", "title": "Account", "cols": ["Available", "Frozen"], "rows": [[account["Balance"], account["FrozenBalance"]]]} btn = {"type": "button", "cmd": "pause", "name": "Pause"} LogStatus( "`" + json.dumps(table) + "`", # Old style: backtick JSON, shown as before "\nSummary", {"type": "grid", "cols": 3, "items": [ # New components go at the end of the line {"type": "kpi", "title": "Available", "value": account["Balance"]}, table, btn # No backticks inside a grid ]}, "\n`" + json.dumps(btn) + "`" ) # Output: the old table, then text with a component grid, then a button read with GetCommand()rustfn main() { let Ok(a) = exchange.GetAccount() else { return }; // JSON in backticks or after text must be one line, so drop the line breaks let table = format!(r#"{{"type": "table", "title": "Account", "cols": ["Available", "Frozen"], "rows": [[{}, {}]]}}"#, a.Balance, a.FrozenBalance).replace('\n', ""); let btn = r#"{"type": "button", "cmd": "pause", "name": "Pause"}"#; let kpi = format!(r#"{{"type": "kpi", "title": "Available", "value": {}}}"#, a.Balance); // New components go at the end of the line;No backticks inside a grid let grid = format!(r#"{{"type": "grid", "cols": 3, "items": [{}, {}, {}]}}"#, kpi, table, btn); // Old style: backtick JSON, shown as before LogStatus!(format!("`{}`", table), "\nSummary", grid, format!("\n`{}`", btn)); // Output: the old table, then text with a component grid, then a button read with GetCommand() } -
Example of data output in the status bar.
javascriptfunction main() { var table = {type: 'table', title: 'Position Info', cols: ['Column 1', 'Column 2'], rows: [ ['abc', 'def'], ['ABC', 'support color #ff0000']]} // After JSON serialization, add ` characters at both ends of the string to have it recognized as a complex message format (tables are currently supported) LogStatus('`' + JSON.stringify(table) + '`') // Table information can also be displayed within multi-line text LogStatus('First line message\n`' + JSON.stringify(table) + '`\nThird line message') // Multiple tables can be displayed simultaneously, and will be grouped and shown as tabs (TAB) LogStatus('`' + JSON.stringify([table, table]) + '`') // Buttons can also be constructed within the table; the strategy receives the content of the cmd property via GetCommand var table = { type: 'table', title: 'Position Operation', cols: ['Column 1', 'Column 2', 'Action'], rows: [ ['abc', 'def', {'type':'button', 'cmd': 'coverAll', 'name': 'Close All'}] ] } LogStatus('`' + JSON.stringify(table) + '`') // Or construct a standalone button LogStatus('`' + JSON.stringify({'type':'button', 'cmd': 'coverAll', 'name': 'Close All'}) + '`') // Button styles can be customized (bootstrap button attributes) LogStatus('`' + JSON.stringify({'type':'button', 'class': 'btn btn-xs btn-danger', 'cmd': 'coverAll', 'name': 'Close All'}) + '`') }pythonimport json def main(): table = {"type": "table", "title": "Position Info", "cols": ["Column 1", "Column 2"], "rows": [["abc", "def"], ["ABC", "support color #ff0000"]]} LogStatus('`' + json.dumps(table) + '`') LogStatus('First line message\n`' + json.dumps(table) + '`\nThird line message') LogStatus('`' + json.dumps([table, table]) + '`') table = { "type" : "table", "title" : "Position Operation", "cols" : ["Column 1", "Column 2", "Action"], "rows" : [ ["abc", "def", {"type": "button", "cmd": "coverAll", "name": "Close All"}] ] } LogStatus('`' + json.dumps(table) + '`') LogStatus('`' + json.dumps({"type": "button", "cmd": "coverAll", "name": "Close All"}) + '`') LogStatus('`' + json.dumps({"type": "button", "class": "btn btn-xs btn-danger", "cmd": "coverAll", "name": "Close All"}) + '`')rustfn main() { let table = r#"{"type": "table", "title": "Position Info", "cols": ["Column 1", "Column 2"], "rows": [["abc", "def"], ["ABC", "support color #ff0000"]]}"#; // Add ` characters at both ends of the JSON string to have it recognized as a complex message format (tables are currently supported) LogStatus!(format!("`{}`", table)); // Table information can also be displayed within multi-line text LogStatus!(format!("First line message\n`{}`\nThird line message", table)); // Multiple tables can be displayed simultaneously, and will be grouped and shown as tabs (TAB) LogStatus!(format!("`[{},{}]`", table, table)); // Buttons can also be constructed within the table; the strategy receives the content of the cmd property via GetCommand let table = String::from(r#"{"type": "table", "title": "Position Operation", "cols": ["Column 1", "Column 2", "Action"], "rows": ["#) + r#"["abc", "def", {"type": "button", "cmd": "coverAll", "name": "Close All"}]"# + r#"]}"#; LogStatus!(format!("`{}`", table)); // Or construct a standalone button LogStatus!(format!("`{}`", r#"{"type": "button", "cmd": "coverAll", "name": "Close All"}"#)); // Button styles can be customized (bootstrap button attributes) LogStatus!(format!("`{}`", r#"{"type": "button", "class": "btn btn-xs btn-danger", "cmd": "coverAll", "name": "Close All"}"#)); } -
Supports designing button controls in the status bar.
javascriptfunction main() { var table = { type: "table", title: "Status Bar Button Styles", cols: ["Default", "Primary", "Success", "Info", "Warning", "Danger"], rows: [ [ {"type":"button", "class": "btn btn-xs btn-default", "name": "Default"}, {"type":"button", "class": "btn btn-xs btn-primary", "name": "Primary"}, {"type":"button", "class": "btn btn-xs btn-success", "name": "Success"}, {"type":"button", "class": "btn btn-xs btn-info", "name": "Info"}, {"type":"button", "class": "btn btn-xs btn-warning", "name": "Warning"}, {"type":"button", "class": "btn btn-xs btn-danger", "name": "Danger"} ] ] } LogStatus("`" + JSON.stringify(table) + "`") }pythonimport json def main(): table = { "type": "table", "title": "Status Bar Button Styles", "cols": ["Default", "Primary", "Success", "Info", "Warning", "Danger"], "rows": [ [ {"type":"button", "class": "btn btn-xs btn-default", "name": "Default"}, {"type":"button", "class": "btn btn-xs btn-primary", "name": "Primary"}, {"type":"button", "class": "btn btn-xs btn-success", "name": "Success"}, {"type":"button", "class": "btn btn-xs btn-info", "name": "Info"}, {"type":"button", "class": "btn btn-xs btn-warning", "name": "Warning"}, {"type":"button", "class": "btn btn-xs btn-danger", "name": "Danger"} ] ] } LogStatus("`" + json.dumps(table) + "`")rustfn main() { let table = String::from(r#"{"type": "table", "title": "Status Bar Button Styles", "cols": ["Default", "Primary", "Success", "Info", "Warning", "Danger"], "rows": [["#) + r#"{"type": "button", "class": "btn btn-xs btn-default", "name": "Default"},"# + r#"{"type": "button", "class": "btn btn-xs btn-primary", "name": "Primary"},"# + r#"{"type": "button", "class": "btn btn-xs btn-success", "name": "Success"},"# + r#"{"type": "button", "class": "btn btn-xs btn-info", "name": "Info"},"# + r#"{"type": "button", "class": "btn btn-xs btn-warning", "name": "Warning"},"# + r#"{"type": "button", "class": "btn btn-xs btn-danger", "name": "Danger"}"# + r#"]]}"#; LogStatus!(format!("`{}`", table)); } -
Configure the disable and description features of status bar buttons.
javascriptfunction main() { var table = { type: "table", title: "Status Bar Button Disable and Description Test", cols: ["Column 1", "Column 2", "Column 3"], rows: [] } var button1 = {"type": "button", "name": "Button 1", "cmd": "button1", "description": "This is the first button"} var button2 = {"type": "button", "name": "Button 2", "cmd": "button2", "description": "This is the second button, set to disabled", "disabled": true} var button3 = {"type": "button", "name": "Button 3", "cmd": "button3", "description": "This is the third button, set to enabled", "disabled": false} table.rows.push([button1, button2, button3]) LogStatus("`" + JSON.stringify(table) + "`") }pythonimport json def main(): table = { "type": "table", "title": "Status Bar Button Disable and Description Test", "cols": ["Column 1", "Column 2", "Column 3"], "rows": [] } button1 = {"type": "button", "name": "Button 1", "cmd": "button1", "description": "This is the first button"} button2 = {"type": "button", "name": "Button 2", "cmd": "button2", "description": "This is the second button, set to disabled", "disabled": True} button3 = {"type": "button", "name": "Button 3", "cmd": "button3", "description": "This is the third button, set to enabled", "disabled": False} table["rows"].append([button1, button2, button3]) LogStatus("`" + json.dumps(table) + "`")rustfn main() { let button1 = r#"{"type": "button", "name": "Button 1", "cmd": "button1", "description": "This is the first button"}"#; let button2 = r#"{"type": "button", "name": "Button 2", "cmd": "button2", "description": "This is the second button, set to disabled", "disabled": true}"#; let button3 = r#"{"type": "button", "name": "Button 3", "cmd": "button3", "description": "This is the third button, set to enabled", "disabled": false}"#; let table = format!( r#"{{"type": "table", "title": "Status Bar Button Disable and Description Test", "cols": ["Column 1", "Column 2", "Column 3"], "rows": [[{}, {}, {}]]}}"#, button1, button2, button3 ); LogStatus!(format!("`{}`", table)); } -
In combination with the
GetCommand()function, build the interactive functionality of status bar buttons.javascriptfunction test1() { Log("Calling custom function") } function main() { while (true) { var table = { type: 'table', title: 'Operation', cols: ['Column 1', 'Column 2', 'Action'], rows: [ ['a', '1', { 'type': 'button', 'cmd': "CoverAll", 'name': 'Close All' }], ['b', '1', { 'type': 'button', 'cmd': 10, 'name': 'Send Number' }], ['c', '1', { 'type': 'button', 'cmd': _D(), 'name': 'Call Function' }], ['d', '1', { 'type': 'button', 'cmd': 'test1', 'name': 'Call Custom Function' }] ] } LogStatus(_D(), "\n", '`' + JSON.stringify(table) + '`') var str_cmd = GetCommand() if (str_cmd) { Log("Received interaction data str_cmd:", "Type:", typeof(str_cmd), "Value:", str_cmd) if(str_cmd == "test1") { test1() } } Sleep(500) } }pythonimport json def test1(): Log("Calling custom function") def main(): while True: table = { "type": "table", "title": "Operation", "cols": ["Column 1", "Column 2", "Action"], "rows": [ ["a", "1", { "type": "button", "cmd": "CoverAll", "name": "Close All" }], ["b", "1", { "type": "button", "cmd": 10, "name": "Send Number" }], ["c", "1", { "type": "button", "cmd": _D(), "name": "Call Function" }], ["d", "1", { "type": "button", "cmd": "test1", "name": "Call Custom Function" }] ] } LogStatus(_D(), "\n", "`" + json.dumps(table) + "`") str_cmd = GetCommand() if str_cmd: Log("Received interaction data str_cmd", "Type:", type(str_cmd), "Value:", str_cmd) if str_cmd == "test1": test1() Sleep(500)rustfn test1() { Log!("Calling custom function"); } fn main() { loop { let table = String::from(r#"{"type": "table", "title": "Operation", "cols": ["Column 1", "Column 2", "Action"], "rows": ["#) + r#"["a", "1", {"type": "button", "cmd": "CoverAll", "name": "Close All"}],"# + r#"["b", "1", {"type": "button", "cmd": 10, "name": "Send Number"}],"# + &format!(r#"["c", "1", {{"type": "button", "cmd": "{}", "name": "Call Function"}}],"#, _D(None)) + r#"["d", "1", {"type": "button", "cmd": "test1", "name": "Call Custom Function"}]"# + r#"]}"#; LogStatus!(_D(None), "\n", format!("`{}`", table)); if let Some(str_cmd) = GetCommand(-1) { Log!("Received interaction data str_cmd:", "Type:", "String", "Value:", &str_cmd); if str_cmd == "test1" { test1(); } } Sleep(500); } } -
A button in a status bar table opens a group of input controls (
group), where the price control is shown only when a limit order is selected; afterGetCommand()receivesopen:{...}the strategy parses the values of the controls. The fields are described under "Status bar buttons" above.javascriptfunction main() { var btnOpen = { type: "button", cmd: "open", name: "开仓|Open", group: [{ type: "selected", name: "tradeType", label: "下单类型|Order type", default: 0, settings: {options: ["市价单|Market", "限价单|Limit"]} }, { type: "selected", name: "direction", label: "方向|Direction", default: "buy", settings: {render: "segment", options: [{name: "买入|Buy", value: "buy"}, {name: "卖出|Sell", value: "sell"}]} }, { // shown only when a limit order (tradeType 1) is selected type: "number", name: "price", label: "价格|Price", required: true, settings: {filter: "tradeType==1"} }, { type: "number", name: "amount", label: "数量|Amount", default: 0.01, required: true, settings: {min: 0} }] } var tbl = {type: "table", title: "操作|Actions", cols: ["品种|Symbol", "操作|Action"], rows: [["BTC_USDT", btnOpen]]} while (true) { LogStatus(_D(), "\n", "`" + JSON.stringify(tbl) + "`") var cmd = GetCommand() if (cmd && cmd.indexOf("open:") === 0) { // e.g. open:{"tradeType":1,"direction":"buy","price":60000,"amount":0.01} var msg = JSON.parse(cmd.slice(5)) Log("direction:", msg.direction, "type:", msg.tradeType == 0 ? "market" : "limit", "price:", msg.price, "amount:", msg.amount) } Sleep(1000) } }pythonimport json def main(): btnOpen = { "type": "button", "cmd": "open", "name": "开仓|Open", "group": [{ "type": "selected", "name": "tradeType", "label": "下单类型|Order type", "default": 0, "settings": {"options": ["市价单|Market", "限价单|Limit"]} }, { "type": "selected", "name": "direction", "label": "方向|Direction", "default": "buy", "settings": {"render": "segment", "options": [{"name": "买入|Buy", "value": "buy"}, {"name": "卖出|Sell", "value": "sell"}]} }, { # shown only when a limit order (tradeType 1) is selected "type": "number", "name": "price", "label": "价格|Price", "required": True, "settings": {"filter": "tradeType==1"} }, { "type": "number", "name": "amount", "label": "数量|Amount", "default": 0.01, "required": True, "settings": {"min": 0} }] } tbl = {"type": "table", "title": "操作|Actions", "cols": ["品种|Symbol", "操作|Action"], "rows": [["BTC_USDT", btnOpen]]} while True: LogStatus(_D(), "\n", "`" + json.dumps(tbl) + "`") cmd = GetCommand() if cmd and cmd.startswith("open:"): # e.g. open:{"tradeType":1,"direction":"buy","price":60000,"amount":0.01} msg = json.loads(cmd[5:]) Log("direction:", msg["direction"], "type:", "market" if msg["tradeType"] == 0 else "limit", "price:", msg.get("price"), "amount:", msg["amount"]) Sleep(1000)rustfn main() { // the price control is shown only when a limit order (tradeType 1) is selected let btn_open = String::from(r#"{"type": "button", "cmd": "open", "name": "开仓|Open", "group": ["#) + r#"{"type": "selected", "name": "tradeType", "label": "下单类型|Order type", "default": 0, "settings": {"options": ["市价单|Market", "限价单|Limit"]}},"# + r#"{"type": "selected", "name": "direction", "label": "方向|Direction", "default": "buy", "settings": {"render": "segment", "options": [{"name": "买入|Buy", "value": "buy"}, {"name": "卖出|Sell", "value": "sell"}]}},"# + r#"{"type": "number", "name": "price", "label": "价格|Price", "required": true, "settings": {"filter": "tradeType==1"}},"# + r#"{"type": "number", "name": "amount", "label": "数量|Amount", "default": 0.01, "required": true, "settings": {"min": 0}}"# + r#"]}"#; let tbl = format!(r#"{{"type": "table", "title": "操作|Actions", "cols": ["品种|Symbol", "操作|Action"], "rows": [["BTC_USDT", {}]]}}"#, btn_open); loop { LogStatus!(_D(None), "\n", format!("`{}`", tbl)); if let Some(cmd) = GetCommand(-1) { // e.g. open:{"tradeType":1,"direction":"buy","price":60000,"amount":0.01} if cmd.starts_with("open:") { if let Some(msg) = JSONParse(&cmd[5..]) { Log!("direction:", msg["direction"].as_str().unwrap_or(""), "type:", if msg["tradeType"].as_i64().unwrap_or(0) == 0 { "market" } else { "limit" }, "price:", msg["price"].as_f64().unwrap_or(0.0), "amount:", msg["amount"].as_f64().unwrap_or(0.0)); } } } Sleep(1000); } }
Arguments
| Name | Type | Required | Description |
msg | string / number / bool / object / array | No | Content to show: text, numbers, table or button objects, |
See Also
Remarks
During live trading, the information output by the LogStatus() function is not written to the log records; it only updates the content of the current live trading status bar. The live trading database keeps only the latest status bar content, and each call overwrites the previous one.
Input forms
The arguments are turned into text and joined with spaces: strings as is, numbers as text, objects and arrays as JSON (in JavaScript and Python a table object whose type is table is also wrapped in backticks). The result is split into lines at \n; the table shows how each line is displayed.
| Form | Display |
|---|---|
| Plain text | Text; a trailing #ff0000 sets the color, the [trans] tag follows the page language |
Component object with a type, or an array containing components | Status Bar Components; components in an array are stacked top to bottom |
Backtick-wrapped JSON: `{...}` | Tables, arrays of tables (shown as tabs) and buttons, or components |
| The whole status bar is one component JSON (it may span several lines) | Recognized as a whole |
- At most one JSON block per line. A component without backticks must come last on its line, optionally after some text (for example
LogStatus("Updated", kpi)); to show several components on one line put them in an array or agrid, or split lines with"\n". - Backtick-wrapped JSON may have text before and after it. Once a line contains backticks, components without backticks on that line are no longer recognized.
- Without backticks only components are recognized: objects whose
typeis not a component (such as the result ofGetAccount()or a lone button object) and arrays of tables only are shown as JSON text, as before.
Status bar components
Component objects are rendered by type: grid and tabs handle layout; kpi, stat, progress, gauge, sparkline, chart, heatmap, calendar, histogram, pie, timeline, markdown, alert and badge display data; table and button objects can also go into the items of grid and tabs. For the fields and examples of each component see Status Bar Components.
griddrops columns with the container width, so it also works on narrow screens.- A malformed component (missing required field, unknown type, data over a limit) shows an error card only in its own place, naming the faulty field and showing its JSON; the other components render as usual.
- Colors follow the page's light or dark theme;
upanddownfollow the page's up/down color setting (FMZ defaults to green for up and red for down). - Fully compatible with the old style: text, backtick tables and buttons look the same and can be mixed with components. Tables also support column formats, up/down colors and units; see
LogStatus-table. - Components are for display only; use the status bar buttons below for interaction. Buttons can go into table cells or into
gridandtabs. - Limits: 5000 points per chart series, 40000 heatmap cells, 6 nesting levels of
gridandtabs; a component over a limit shows an error card.
Status bar buttons
Output a button object as JSON.stringify() wrapped in ` to the status bar, or put it in a status bar table cell (an array of buttons for several buttons in one cell). Each status bar line can contain only one `-wrapped JSON block, so output several buttons on separate lines or in a table. After a click the strategy reads the command with GetCommand; when the live trading is not running a click only shows an error.
Button fields:
| Field | Description |
|---|---|
| type | Always button |
| name | Text on the button |
| cmd | Command sent to the strategy on click; a click does nothing when it is empty |
| description | Hint shown on mouse hover |
| disabled | true disables the button |
| class | Style: containing btn-primary, btn-success, btn-info, btn-warning or btn-danger gives that color; btn-sm / btn-lg give medium / large size, small by default |
| input | Opens one input control on click; fields under "Input controls" below |
| group | Opens a group of input controls on click; an array whose items have the fields under "Input controls" below |
Command received by GetCommand():
| Button | Command | Example |
|---|---|---|
No input / group | cmd as is (a confirmation dialog is shown first) | open |
With input | cmd:value, strings unquoted, multi-select as an array | open:1, open:abc, open:[0,2] |
With group | cmd:JSON keyed by each control's name; controls left empty are omitted | open:{"tradeType":0,"price":100} |
Input control fields (input or each item of group):
| Field | Description |
|---|---|
| type | number, string, selected (drop-down) or boolean (switch) |
| name | In group, the key of this control in the command JSON; in input, used as the title when there is no label |
| label | Control title; description is used when not set |
| description | Control description |
| default | Default value, also accepted as defValue; for a drop-down, the option index or value |
| required | true makes the control mandatory |
| settings | Control configuration, see below |
settings fields:
| Field | Description |
|---|---|
| options | Drop-down options: ["A", "B"] (the command carries the option index) or [{"name": "A", "value": "a"}] (the command carries value) |
| multiple | Multi-select drop-down; the command value is an array |
| customizable | The drop-down accepts new options typed by the user; selecting one puts the typed text in the command |
| filter | Visibility expression referring to the name of other controls in the group, for example "tradeType==1"; the control is hidden when it is false |
| disabled | Disables the control |
| min, max | Minimum and maximum of a number |
| step | Step of a number shown as a slider |
| unit | Unit text shown after the input |
| render | Appearance. number: slider, date (date-time picker, value in millisecond timestamp); string: textarea (multi-line), date (value yyyy-MM-dd hh:mm:ss), color (value #FF00FF); selected: segment; boolean is always a switch |
The older form without settings still works: a drop-down sets its options with defValue: "A|B|C" (the command carries the index), and a visibility condition is written into name as "price@tradeType==1".
Button text, control titles and descriptions, drop-down options, table titles and column names accept bilingual text "中文|English", shown according to the page language; for bilingual text in plain status bar text and table cells use [trans]中文|English[/trans].
Buttons can also go into the items of grid or tabs next to status bar components, without backticks.
The LogStatus() function supports printing base64-encoded images. The image string starts with ` and ends with `. For example: LogStatus("`data:image/png;base64,AAAA`").
When a strategy is running live, if you scroll through the historical records on the live trading page, the status bar enters a dormant state and stops updating; the status bar data is only refreshed when the log is on the first page. The status bar supports outputting base64-encoded images, and also supports outputting base64-encoded images within tables displayed in the status bar. Since encoded image string data is usually very long, no example code is shown here.
Rust has no object literals: write a component as JSON text (with format!, write { and } as {{ and }}), or pass a JsonValue from JSONParse(); the frontend recognizes both. String arguments are never wrapped in backticks, so a table on its own is written as format!("`{}`", table); inside grid, tabs or an array with components no backticks are needed. Numbers must be finite: NaN makes the whole JSON unparsable and it is shown as text.
The LogStatus() function supports directly passing in a Python matplotlib.pyplot object. As long as the object contains the savefig method, it can be passed as a parameter to the LogStatus() function. For example:
python
import matplotlib.pyplot as plt
def main():
plt.plot([3,6,2,4,7,1])
LogStatus(plt)