Finance

CurrencyPairCard

One currency pair: the bid and ask large with the pip digits biggest, the spread, the day's change, low and high, and a sparkline of the day.

#finance#forex#card#chart#no-js

Demo

Preview

EUR/USDEuro / US dollar

up+23.5 pips (+0.22%)
Sell at the bid
0.8spread
Buy at the ask
Low1.08133High1.08502

Indicative, 14:05 UTC

USD/JPYUS dollar / Yen

down-65.8 pips (-0.42%)
Sell at the bid
1.3spread
Buy at the ask
Low156.020High157.115

USD/PHPUS dollar / Philippine peso

Sell at the bid
3.5spread
Buy at the ask

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/currency_pair_card.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/currency_pair_card.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/currency_pair_card.qwe
view CurrencyPairCardDemo() =    <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px">        <CurrencyPairCard id="currency-pair-card-demo-eur" pair="EUR/USD" bid={1.08421m} ask={1.08429m} open={1.08190m}                          high={1.08502m} low={1.08133m} href="#eurusd" updated="Indicative, 14:05 UTC"                          values={[1.0819, 1.0815, 1.0824, 1.0831, 1.0827, 1.0836, 1.0849, 1.0838, 1.0842]}/>        <CurrencyPairCard id="currency-pair-card-demo-jpy" pair="USD/JPY" bid={156.238m} ask={156.251m} open={156.902m}                          high={157.115m} low={156.020m} values={[156.90, 157.05, 156.71, 156.62, 156.40, 156.31, 156.24]}/>        <CurrencyPairCard id="currency-pair-card-demo-php" pair="USD/PHP" name="US dollar / Philippine peso" bid={58.105m} ask={58.140m}/>    </div>

Source

ui/currency_pair_card.qwe
// CurrencyPairCard: one currency pair at a glance: the bid (sell) and the ask (buy)// written large, with the two pip digits biggest as dealers show them (1.08 42 1),// the spread between them, the day's change from the open, the day's low and high// with where the price stands between them, and a sparkline of the day.////   <CurrencyPairCard pair="EUR/USD" bid={1.08421m} ask={1.08429m} open={1.08190m} high={1.08502m} low={1.08133m}//                     values={[1.0819, 1.0824, 1.0831, 1.0827, 1.0838, 1.0842]} updated="14:05 UTC"/>//// pair: "EUR/USD", base over quote. bid and ask: the prices (decimals best; numbers are// read as decimals). open: the day's opening mid price, for the change (none hides it).// high and low: the day's range (none hides it). values: the day's prices for the// sparkline, oldest first (two or more; numbers). name: under the pair ("Euro / US// dollar"; the common ones are known). pip: its size (0.01 when the quote is JPY or// another priced in the hundreds, else 0.0001); decimals: digits shown (one past the// pip). locale: how numbers are written ("en-US"). updated: when the prices are from.// href: makes the pair a link (to its chart, a deal ticket). id: the element's id.import ui/theme.qwe use ui_clslet currency_pair_card_css = css`.ui-fxpair { display: grid; gap: 12px; min-width: 0; max-width: 360px; padding: 16px 18px; border: 1px solid var(--ui-border);  border-radius: var(--ui-radius); background: var(--ui-surface); font: 14px/1.35 var(--ui-font); color: var(--ui-fg);  font-variant-numeric: tabular-nums; align-content: start; --k: var(--ui-muted); }.ui-fxpair-up { --k: var(--ui-success); } .ui-fxpair-down { --k: var(--ui-danger); }.ui-fxpair-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 6px 10px; }.ui-fxpair .ui-fxpair-title { margin: 0; display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 650; }.ui-fxpair-title a { color: inherit; text-decoration: none; }.ui-fxpair-title a:hover { color: var(--ui-accent); }.ui-fxpair-title a:focus-visible { outline: none; box-shadow: var(--ui-focus); border-radius: 3px; }.ui-fxpair-flags { flex: none; font-size: 18px; letter-spacing: -.15em; white-space: nowrap; }.ui-fxpair-name { display: block; color: var(--ui-muted); font-size: 12px; font-weight: 400; }.ui-fxpair-chg { display: inline-flex; align-items: center; gap: 3px; padding: 2px 8px; border-radius: 999px; font-size: 13px; font-weight: 600;  color: var(--k); background: color-mix(in srgb, var(--k) 13%, transparent); white-space: nowrap; }.ui-fxpair-quotes { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: stretch; gap: 8px; }.ui-fxpair-side { display: grid; gap: 2px; min-width: 0; padding: 8px 10px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius-sm); background: var(--ui-subtle); }.ui-fxpair-side:last-child { text-align: right; }.ui-fxpair-side small { color: var(--ui-muted); font-size: 11px; font-weight: 600; letter-spacing: .05em; text-transform: uppercase; }.ui-fxpair-price { font-size: 14px; font-weight: 600; white-space: nowrap; }.ui-fxpair-big { font-size: 26px; font-weight: 700; line-height: 1; letter-spacing: -.01em; }.ui-fxpair-frac { vertical-align: super; font-size: 12px; font-weight: 600; }.ui-fxpair-spread { align-self: center; display: grid; justify-items: center; color: var(--ui-muted); font-size: 12px; }.ui-fxpair-spread b { color: var(--ui-fg); font-size: 14px; font-weight: 600; }.ui-fxpair-range { display: grid; grid-template-columns: auto minmax(24px, 1fr) auto; align-items: center; gap: 10px; font-size: 12px; color: var(--ui-muted); }.ui-fxpair-range b { display: block; color: var(--ui-fg); font-size: 13px; font-weight: 600; }.ui-fxpair-range > :last-child { text-align: right; }.ui-fxpair-track { position: relative; height: 4px; border-radius: 2px; background: var(--ui-border); }.ui-fxpair-dot { position: absolute; top: 50%; width: 10px; height: 10px; margin: -5px 0 0 -5px; border: 2px solid var(--ui-surface); border-radius: 50%;  background: var(--k); box-sizing: border-box; }.ui-fxpair-spark { display: block; width: 100%; height: 48px; overflow: visible; }.ui-fxpair-spark path { fill: none; stroke: var(--k); stroke-width: 1.75; stroke-linejoin: round; stroke-linecap: round; }.ui-fxpair-spark .ui-fxpair-area { fill: var(--k); opacity: .1; stroke: none; }.ui-fxpair-spark line { stroke: var(--ui-strong); stroke-dasharray: 3 3; }.ui-fxpair-foot { margin: 0; color: var(--ui-muted); font-size: 12px; }`let currency_pair_card_flags = { USD: "๐Ÿ‡บ๐Ÿ‡ธ", EUR: "๐Ÿ‡ช๐Ÿ‡บ", GBP: "๐Ÿ‡ฌ๐Ÿ‡ง", JPY: "๐Ÿ‡ฏ๐Ÿ‡ต", CHF: "๐Ÿ‡จ๐Ÿ‡ญ", CAD: "๐Ÿ‡จ๐Ÿ‡ฆ", AUD: "๐Ÿ‡ฆ๐Ÿ‡บ", NZD: "๐Ÿ‡ณ๐Ÿ‡ฟ",                                 CNY: "๐Ÿ‡จ๐Ÿ‡ณ", HKD: "๐Ÿ‡ญ๐Ÿ‡ฐ", SGD: "๐Ÿ‡ธ๐Ÿ‡ฌ", INR: "๐Ÿ‡ฎ๐Ÿ‡ณ", PHP: "๐Ÿ‡ต๐Ÿ‡ญ", MXN: "๐Ÿ‡ฒ๐Ÿ‡ฝ", ZAR: "๐Ÿ‡ฟ๐Ÿ‡ฆ", SEK: "๐Ÿ‡ธ๐Ÿ‡ช",                                 NOK: "๐Ÿ‡ณ๐Ÿ‡ด", PLN: "๐Ÿ‡ต๐Ÿ‡ฑ", TRY: "๐Ÿ‡น๐Ÿ‡ท", KRW: "๐Ÿ‡ฐ๐Ÿ‡ท" }let currency_pair_card_names = { USD: "US dollar", EUR: "Euro", GBP: "Pound", JPY: "Yen", CHF: "Swiss franc", CAD: "Canadian dollar",                                 AUD: "Aussie", NZD: "Kiwi", CNY: "Yuan", INR: "Rupee", PHP: "Philippine peso", MXN: "Mexican peso" }// A pip's size for "EUR/USD": 0.01 when the quote is priced in the hundreds, else 0.0001.fn currency_pair_card_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}// A price split as dealers show it: { lead, big, frac }, the two pip digits big and the// fractional pip after them: 1.08421 is "1.08", "42", "1" (with 4 pip and 5 shown decimals).fn currency_pair_card_split(text: str, pip_places: int, places: int) {    let n = len(text)    let extra = places > pip_places ? places - pip_places : 0    let end = n - extra    { lead: slice(text, 0, end - 2), big: slice(text, end - 2, end), frac: slice(text, end) }}// A coordinate, to two decimals.fn currency_pair_card_at(n: num) -> str {    let r = round(n * 100) / 100    r == floor(r) ? str(floor(r)) : str(r)}// The sparkline in a 200 by 48 box: { d, open_y }, the path and the open's height.fn currency_pair_card_spark(pts: list, open: float?) {    let all = pts + (open != null ? [open] : [])    let vmin = min(all)    let vmax = max(all)    let y = (v) => vmax == vmin ? 24 : 4 + (vmax - v) * 40 / (vmax - vmin)    let n = len(pts)    let d = "M" + join(range(n) | map((i) => `${currency_pair_card_at(i * 200 / (n - 1))},${currency_pair_card_at(y(pts[i]))}`), " L")    { d, open_y: open != null ? currency_pair_card_at(y(open)) : null }}view CurrencyPairCard(pair: str, bid, ask, open = null, high = null, low = null, values: list? = null, name: str? = null,                      pip = null, decimals: int? = null, locale: str = "en-US", updated: str? = null, href: str? = null,                      id: str? = null, class: str? = null) {    style global currency_pair_card_css    let parts = split(upper(pair), "/")    let size = pip != null ? dec(pip) : currency_pair_card_pip(pair)    let pip_places = size >= 0.01m ? 2 : (size >= 0.001m ? 3 : 4)    let places = decimals ?? pip_places + 1    let b = dec(bid)    let a = dec(ask)    let mid = (b + a) / 2    let o = open != null ? dec(open) : null    let pips = o != null ? round((mid - o) / size, 1, "half_up") : null    let pct = o != null && o != 0 ? round((mid - o) * 100 / o, 2, "half_up") : null    let mood = pips == null || pips == 0 ? "flat" : (pips > 0 ? "up" : "down")    let show = (d: dec) => format_number(round(d, places, "half_up"), locale, places)    let price = (d: dec) => {        let s = currency_pair_card_split(show(d), pip_places, places)        <span.ui-fxpair-price aria-label={show(d)}>            <span aria-hidden="true">{s.lead}</span><span.ui-fxpair-big aria-hidden="true">{s.big}</span>{s.frac != "" && <span.ui-fxpair-frac aria-hidden="true">{s.frac}</span>}        </span>    }    let title = name ?? (len(parts) > 1 && currency_pair_card_names[parts[0]] != null && currency_pair_card_names[parts[1]] != null        ? `${currency_pair_card_names[parts[0]]} / ${currency_pair_card_names[parts[1]]}` : null)    let hi = high != null ? dec(high) : null    let lo = low != null ? dec(low) : null    let where = hi != null && lo != null && hi > lo ? float((mid < lo ? lo : (mid > hi ? hi : mid)) - lo) * 100 / float(hi - lo) : 50    let pts = values != null && len(values) > 1 ? values | map((v) => float(v)) : null    let spark = pts != null ? currency_pair_card_spark(pts, o != null ? float(o) : null) : null    <div class={ui_cls(["ui-fxpair", mood != "flat" && `ui-fxpair-${mood}`, class])} id={id} role="group" aria-label={`${pair} quote`}>        <div.ui-fxpair-head>            <h3.ui-fxpair-title>                <span.ui-fxpair-flags aria-hidden="true">{currency_pair_card_flags[parts[0]] ?? ""}{len(parts) > 1 ? currency_pair_card_flags[parts[1]] ?? "" : ""}</span>                <span>{href != null ? <a href={href}>{pair}</a> : pair}{title != null && <span.ui-fxpair-name>{title}</span>}</span>            </h3>            {pips != null && <span.ui-fxpair-chg data-pips={str(pips)}>                <span aria-hidden="true">{mood == "up" ? "โ†‘" : (mood == "down" ? "โ†“" : "โ†’")}</span>                <span.ui-sr>{mood == "up" ? "up" : (mood == "down" ? "down" : "unchanged")}</span>                {pips > 0 ? "+" : ""}{format_number(pips, locale, 1)} pips{pct != null && ` (${pct > 0 ? "+" : ""}${format_number(pct, locale, 2)}%)`}            </span>}        </div>        <div.ui-fxpair-quotes>            <div.ui-fxpair-side data-side="bid"><small>Sell <span.ui-sr>at the bid</span></small>{price(b)}</div>            <div.ui-fxpair-spread><b>{format_number(round((a - b) / size, 1, "half_up"), locale, 1)}</b>spread</div>            <div.ui-fxpair-side data-side="ask"><small>Buy <span.ui-sr>at the ask</span></small>{price(a)}</div>        </div>        {hi != null && lo != null && <div.ui-fxpair-range>            <span>Low<b>{show(lo)}</b></span>            <span.ui-fxpair-track role="img" aria-label={`Now ${round(where)}% of the way from the day's low to its high`}>                <span.ui-fxpair-dot style={`left: ${currency_pair_card_at(where)}%`}></span>            </span>            <span>High<b>{show(hi)}</b></span>        </div>}        {spark != null && <svg.ui-fxpair-spark viewBox="0 0 200 48" preserveAspectRatio="none" aria-hidden="true">            {spark.open_y != null && <line x1="0" x2="200" y1={spark.open_y} y2={spark.open_y} vector-effect="non-scaling-stroke"/>}            <path.ui-fxpair-area d={`${spark.d} L200,48 L0,48 Z`}/>            <path d={spark.d} vector-effect="non-scaling-stroke"/>        </svg>}        {updated != null && <p.ui-fxpair-foot>{updated}</p>}    </div>}
Thread
76Useful? Like it, or add to its thread below.

Thread 0 posts

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

No posts yet. The first one opens the thread.