Finance

FxPositions

Open currency positions: pair, long or short, size, entry and current rate, profit or loss in pips and money, coloured, with the total.

#finance#forex#trading#table#no-js

Demo

Preview
Open positions5 open, in USD
PairSideSizeEntryCurrentPipsP/L in USD
EUR/USDLong100,000EUR1.081051.08421profit +31.6profit +$316.00
GBP/USDShort50,000GBP1.273101.27532loss -22.2loss −$111.00
USD/JPYShort200,000USD157.020156.251profit +76.9profit +$984.31
EUR/GBPLong75,000EUR0.852400.85026loss -21.4loss −$204.69
AUD/NZDLong30,000AUD1.098201.10010profit +19.0—
Total1 without a rate for USD left outprofit +$984.62

Get it

It needs theme.qwe, as every component does; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/fx_positions.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/fx_positions.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/fx_positions.qwe
view FxPositionsDemo() =    <FxPositions id="fx-positions-demo" account="USD" rates={{ USD: 1, EUR: 0.9224m, GBP: 0.7841m, JPY: 156.24m }} positions={[        { id: "T-20418", pair: "EUR/USD", side: "buy", size: 100000, entry: 1.08105m, current: 1.08421m, href: "#T-20418" },        { id: "T-20422", pair: "GBP/USD", side: "sell", size: 50000, entry: 1.27310m, current: 1.27532m },        { id: "T-20431", pair: "USD/JPY", side: "sell", size: 200000, entry: 157.020m, current: 156.251m },        { id: "T-20437", pair: "EUR/GBP", side: "buy", size: 75000, entry: 0.85240m, current: 0.85026m },        { id: "T-20440", pair: "AUD/NZD", side: "buy", size: 30000, entry: 1.09820m, current: 1.10010m },    ]}/>

Source

ui/fx_positions.qwe
// FxPositions: open currency positions, a row each: the pair, long or short, the// size, the entry and the current rate, and the profit or loss so far in pips and// in the account's currency, green or red, with the total underneath.////   <FxPositions account="USD" rates={{ USD: 1, GBP: 0.7841m, JPY: 156.24m }} positions={[//       { pair: "EUR/USD", side: "buy", size: 100000, entry: 1.08105m, current: 1.08421m },//       { pair: "USD/JPY", side: "sell", size: 200000, entry: 157.020m, current: 156.251m }]}/>//// positions: a list of { pair, side, size, entry, current, id, href }: pair as "EUR/USD",// side "buy" (long the base) or "sell" (short), size in units of the base, entry the// rate it was opened at and current the rate it would close at now (decimals best;// numbers are read as decimals). id: names the row (the pair by default); href: a link// on the pair (to close or edit it). account: the currency profit and loss is shown in// ("USD"). A pair's profit is in its quote currency; it is turned into the account's// by the pair's own rate when the account is the base or the quote, else by rates: a// map of code to the units of it one `account` buys ({ GBP: 0.7841m }), and shown as// "—" when it is not there (and left out of the total). locale: how numbers are// written ("en-US"). label: the caption ("Open positions"). id: the element's id.//// fx_positions_pnl(position, account, rates) gives { pips, quote, money } for one// position (money null when it cannot be turned into the account's currency).import ui/theme.qwe use ui_clslet fx_positions_css = css`.ui-fxpos { min-width: 0; overflow-x: auto; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface);  font: 13px/1.35 var(--ui-font); color: var(--ui-fg); }.ui-fxpos-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }.ui-fxpos-table caption { padding: 12px 14px; text-align: left; font-size: 14px; font-weight: 650; }.ui-fxpos-table caption small { margin-left: 8px; color: var(--ui-muted); font-size: 12px; font-weight: 400; }.ui-fxpos-table th, .ui-fxpos-table td { padding: 9px 14px; text-align: right; white-space: nowrap; }.ui-fxpos-table thead th { border-top: 1px solid var(--ui-border); border-bottom: 1px solid var(--ui-border); background: var(--ui-subtle);  color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; }.ui-fxpos-table th:first-child, .ui-fxpos-table td:first-child { text-align: left; }.ui-fxpos-table tbody tr:not(:last-child) > * { border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 60%, transparent); }.ui-fxpos-table tbody tr:hover { background: var(--ui-subtle); }.ui-fxpos-table tbody th { font-weight: 650; }.ui-fxpos-table tbody th a { color: inherit; text-decoration: none; }.ui-fxpos-table tbody th a:hover { color: var(--ui-accent); text-decoration: underline; }.ui-fxpos-table tbody th a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-fxpos-side { display: inline-block; min-width: 40px; padding: 1px 7px; border-radius: 4px; font-size: 11px; font-weight: 650;  letter-spacing: .03em; text-align: center; text-transform: uppercase; }.ui-fxpos-buy { color: var(--ui-success); background: var(--ui-success-soft); }.ui-fxpos-sell { color: var(--ui-danger); background: var(--ui-danger-soft); }.ui-fxpos-size small { margin-left: 4px; color: var(--ui-muted); font-size: 11px; }.ui-fxpos-gain { --k: var(--ui-success); } .ui-fxpos-loss { --k: var(--ui-danger); } .ui-fxpos-even { --k: var(--ui-muted); }.ui-fxpos-pl { color: var(--k); font-weight: 650; }.ui-fxpos-none { color: var(--ui-muted); }.ui-fxpos-table tfoot th, .ui-fxpos-table tfoot td { border-top: 2px solid var(--ui-border); font-size: 14px; font-weight: 650; }.ui-fxpos-table tfoot small { display: block; color: var(--ui-muted); font-size: 11px; font-weight: 400; }`// A pip's size for "EUR/USD": 0.01 when the quote is priced in the hundreds, else 0.0001.fn fx_positions_pip(pair: str) -> dec {    let parts = split(upper(pair), "/")    contains(["JPY", "HUF", "KRW", "IDR", "VND", "CLP", "ISK", "THB", "INR", "PHP", "RUB"], len(parts) > 1 ? parts[1] : "") ? 0.01m : 0.0001m}// One position's result: { pips, quote, money }: pips signed (to one decimal), quote the// profit in the pair's quote currency, money in the account's (to its minor units, or// null when no rate turns it into it).fn fx_positions_pnl(p, account: str, rates: map) {    let parts = split(upper(p.pair), "/")    let base = parts[0]    let quote = len(parts) > 1 ? parts[1] : parts[0]    let sign = lower(p.side) == "sell" ? -1 : 1    let move = (dec(p.current) - dec(p.entry)) * sign    let in_quote = move * dec(p.size)    let r = rates[quote]    let raw = quote == account ? in_quote        : (base == account ? in_quote / dec(p.current)        : (r != null && dec(r) > 0 ? in_quote / dec(r) : null))    let m = raw != null ? money.of(raw, account, "half_up") : null    {        pips: round(move / fx_positions_pip(p.pair), 1, "half_up"),        quote: in_quote,        money: m == null ? null : (type_of(m) == "error" ? round(raw, 2, "half_up") : m.amount),    }}// An amount of the account's currency, signed: "+$316.00", "−$111.00".fn fx_positions_signed(d: dec, code: str, locale: str) -> str {    let m = money.of(abs(d), code, "half_up")    let text = type_of(m) == "error" ? `${format_number(abs(d), locale, 2)} ${code}` : money.format(m, locale)    `${d > 0 ? "+" : (d < 0 ? "−" : "")}${text}`}view FxPositions(positions: list = [], account: str = "USD", rates: map = {}, locale: str = "en-US", label: str = "Open positions",                 id: str? = null, class: str? = null) {    style global fx_positions_css    let code = upper(account)    let rows = positions | map((p) => {        let result = fx_positions_pnl(p, code, rates)        { ...p, result }    })    let known = rows | filter((r) => r.result.money != null)    let total = sum(known | map((r) => r.result.money))    let mood = (d) => d == null ? "ui-fxpos-even" : (d > 0 ? "ui-fxpos-gain" : (d < 0 ? "ui-fxpos-loss" : "ui-fxpos-even"))    let word = (d) => d == null ? "" : (d > 0 ? "profit " : (d < 0 ? "loss " : ""))    let places = (pair: str) => fx_positions_pip(pair) == 0.01m ? 3 : 5    <div class={ui_cls(["ui-fxpos", class])} id={id}>        <table.ui-fxpos-table>            <caption>{label}<small>{len(rows)} open, in {code}</small></caption>            <thead><tr>                <th scope="col">Pair</th><th scope="col">Side</th><th scope="col">Size</th><th scope="col">Entry</th>                <th scope="col">Current</th><th scope="col">Pips</th><th scope="col">P/L <span.ui-sr>in {code}</span></th>            </tr></thead>            <tbody>                {for r in rows {                    let key = get(r, "id") ?? r.pair                    let sell = lower(r.side) == "sell"                    <tr key={key} data-id={key} class={mood(r.result.money ?? r.result.pips)}>                        <th scope="row">{get(r, "href") != null ? <a href={r.href}>{r.pair}</a> : r.pair}</th>                        <td><span class={sell ? "ui-fxpos-side ui-fxpos-sell" : "ui-fxpos-side ui-fxpos-buy"}>{sell ? "Short" : "Long"}</span></td>                        <td.ui-fxpos-size>{format_number(dec(r.size), locale, 0)}<small>{split(upper(r.pair), "/")[0]}</small></td>                        <td>{format_number(round(dec(r.entry), places(r.pair), "half_up"), locale, places(r.pair))}</td>                        <td>{format_number(round(dec(r.current), places(r.pair), "half_up"), locale, places(r.pair))}</td>                        <td.ui-fxpos-pl data-col="pips"><span.ui-sr>{word(r.result.pips)}</span>{r.result.pips > 0 ? "+" : ""}{format_number(r.result.pips, locale, 1)}</td>                        <td.ui-fxpos-pl data-col="pl">{r.result.money != null                            ? [<span.ui-sr>{word(r.result.money)}</span>, fx_positions_signed(r.result.money, code, locale)]                            : <span.ui-fxpos-none title={`No rate for ${code}`}>—</span>}</td>                    </tr>                }}            </tbody>            {len(rows) > 0 && <tfoot><tr class={mood(total)}>                <th scope="row" colspan="6">Total                    {len(known) < len(rows) && <small>{len(rows) - len(known)} without a rate for {code} left out</small>}                </th>                <td.ui-fxpos-pl data-col="total"><span.ui-sr>{word(total)}</span>{fx_positions_signed(total, code, locale)}</td>            </tr></tfoot>}        </table>    </div>}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

On FxPositions: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.