repos
/ finance-rust master

finance-rust

mirror archived upstream

Single-binary self-hosted market watcher for stocks, ETFs, indexes, and futures: live charts, key stats, fundamentals, SEC filings, and SSE streaming.

axumdockerfinancerustself-hostedsqlitestocksvite

7.1 KB · 134 lines · HTML Raw History
  1{# Reusable fragments. `ticker_card` renders one symbol tile; its data-ticker
  2   and data-field hooks let the live stream client patch it in place. #}
  3
  4{# `verdict_badge` renders the rolled-up strong / fair / weak standing badge
  5   (Phase 20). `s` is a compute::Standing: `s.grade` is the colour hook
  6   (good/ok/bad), `s.verdict` the word (Strong/Fair/Weak). #}
  7{% macro verdict_badge(s) %}<span class="vbadge vbadge--{{ s.grade }}">{{ s.verdict }}</span>{% endmacro %}
  8
  9{% macro ticker_card(c) %}
 10<a class="ticker-card" href="/s/{{ c.ticker|urlencode }}" data-ticker="{{ c.ticker }}">
 11  <div class="ticker-card__head">
 12    <span class="ticker-card__sym">{{ c.ticker }}</span>
 13    <span class="ticker-card__tags">
 14      {% if c.strength %}{{ verdict_badge(c.strength) }}{% endif %}
 15      <span class="ticker-card__kind">{{ c.kind }}</span>
 16    </span>
 17  </div>
 18  <div class="ticker-card__name">{{ c.name }}</div>
 19  <div class="ticker-card__foot">
 20    <span class="ticker-card__price num" data-field="price">{{ c.price|money }}</span>
 21    <span class="ticker-card__chg num{% if c.change_pct is none %} is-flat{% elif c.change_pct >= 0 %} is-up{% else %} is-down{% endif %}" data-field="change_pct">{{ c.change_pct|pct }}</span>
 22  </div>
 23</a>
 24{% endmacro %}
 25
 26{# `spark_card` renders one dashboard tile: an intraday sparkline with a live
 27   price and day change. data-lo/data-hi let the stream client place a live
 28   quote on the same y-scale and nudge the trailing point. #}
 29{% macro spark_card(c) %}
 30<a class="spark-card{% if c.up %} is-up-card{% else %} is-down-card{% endif %}"
 31   href="/s/{{ c.ticker|urlencode }}" data-ticker="{{ c.ticker }}">
 32  <div class="spark-card__head">
 33    <span class="spark-card__sym">{{ c.ticker }}</span>
 34    <span class="spark-card__name">{{ c.name }}</span>
 35  </div>
 36  {% if c.spark %}
 37  <svg class="spark" viewBox="0 0 100 36" preserveAspectRatio="none"
 38       data-lo="{{ c.spark.lo }}" data-hi="{{ c.spark.hi }}" aria-hidden="true">
 39    <polygon class="spark__area" points="{{ c.spark.area }}"/>
 40    {% if c.spark.baseline is not none %}
 41    <line class="spark__base" x1="0" x2="100" y1="{{ c.spark.baseline }}" y2="{{ c.spark.baseline }}"/>
 42    {% endif %}
 43    <polyline class="spark__line" points="{{ c.spark.line }}"/>
 44  </svg>
 45  {% else %}
 46  <div class="spark spark--empty">no intraday data</div>
 47  {% endif %}
 48  <div class="spark-card__foot">
 49    <span class="spark-card__price num" data-field="price">{{ c.price|money }}</span>
 50    <span class="spark-card__chg num{% if c.change_pct is none %} is-flat{% elif c.change_pct >= 0 %} is-up{% else %} is-down{% endif %}" data-field="change_pct">{{ c.change_pct|pct }}</span>
 51  </div>
 52</a>
 53{% endmacro %}
 54
 55{# `etf_card` renders one curated ETF tile (Phase 5): the same intraday
 56   sparkline + live price as `spark_card`, plus the Phase-4 quality verdict as a
 57   small pill in the head so the fund's quality reads at a glance beside its
 58   move. `e.card` is a SparkCard; `e.quality` an EtfQuality (None until ≥2
 59   factors grade). #}
 60{% macro etf_card(e) %}
 61<a class="spark-card{% if e.card.up %} is-up-card{% else %} is-down-card{% endif %}"
 62   href="/s/{{ e.card.ticker|urlencode }}" data-ticker="{{ e.card.ticker }}">
 63  <div class="spark-card__head">
 64    <span class="spark-card__sym">{{ e.card.ticker }}</span>
 65    <span class="spark-card__name">{{ e.card.name }}</span>
 66    {% if e.quality %}<span class="qchip qchip--{{ e.quality.overall }}" title="ETF quality {{ e.quality.percent }}% — cost {{ e.quality.cost_label }}, tracking {{ e.quality.tracking_label }}, diversification {{ e.quality.diversification_label }}, size {{ e.quality.size_label }}">{{ e.quality.verdict }}</span>{% endif %}
 67  </div>
 68  {% if e.card.spark %}
 69  <svg class="spark" viewBox="0 0 100 36" preserveAspectRatio="none"
 70       data-lo="{{ e.card.spark.lo }}" data-hi="{{ e.card.spark.hi }}" aria-hidden="true">
 71    <polygon class="spark__area" points="{{ e.card.spark.area }}"/>
 72    {% if e.card.spark.baseline is not none %}
 73    <line class="spark__base" x1="0" x2="100" y1="{{ e.card.spark.baseline }}" y2="{{ e.card.spark.baseline }}"/>
 74    {% endif %}
 75    <polyline class="spark__line" points="{{ e.card.spark.line }}"/>
 76  </svg>
 77  {% else %}
 78  <div class="spark spark--empty">no intraday data</div>
 79  {% endif %}
 80  <div class="spark-card__foot">
 81    <span class="spark-card__price num" data-field="price">{{ e.card.price|money }}</span>
 82    <span class="spark-card__chg num{% if e.card.change_pct is none %} is-flat{% elif e.card.change_pct >= 0 %} is-up{% else %} is-down{% endif %}" data-field="change_pct">{{ e.card.change_pct|pct }}</span>
 83  </div>
 84</a>
 85{% endmacro %}
 86
 87{# `etf_pill` renders one compact ETF mover (Phase 5): ticker + day move, with a
 88   small quality dot tinting the pill by the fund's overall grade. #}
 89{% macro etf_pill(m) %}
 90<a class="etf-pill {{ 'is-up' if m.change_pct >= 0 else 'is-down' }}" href="/s/{{ m.ticker|urlencode }}">
 91  {% if m.quality %}<span class="etf-pill__dot etf-pill__dot--{{ m.quality.overall }}" aria-hidden="true"></span>{% endif %}
 92  <span class="etf-pill__sym">{{ m.ticker }}</span>
 93  <span class="etf-pill__chg num">{{ m.change_pct|pct }}</span>
 94</a>
 95{% endmacro %}
 96
 97{# `mover_row` renders one gainer/loser row. The `--bar` custom property sizes
 98   a soft magnitude tint behind the row, scaled by the route. The badge cell
 99   is always present (empty for a stock without a synced standing) so the
100   row's grid keeps a fixed column count. #}
101{% macro mover_row(m) %}
102<a class="mover {{ 'mover--up' if m.change_pct >= 0 else 'mover--down' }}"
103   href="/s/{{ m.ticker|urlencode }}" style="--bar: {{ m.bar }}%">
104  <span class="mover__sym num">{{ m.ticker }}</span>
105  <span class="mover__name">{{ m.name }}</span>
106  <span class="mover__badge">{% if m.strength %}{{ verdict_badge(m.strength) }}{% endif %}</span>
107  <span class="mover__price num">{{ m.price|money }}</span>
108  <span class="mover__chg num {{ 'is-up' if m.change_pct >= 0 else 'is-down' }}">{{ m.change_pct|pct }}</span>
109</a>
110{% endmacro %}
111
112{# `health_row` renders one quality-leaderboard row (Phase 17, reframed in
113   Phase 3). The overall verdict badge sits on the right; the three sub-readings
114   ride below the name as a compact line so the synthesis is legible without
115   expanding, and the trailing-year return sits between as a quiet price anchor.
116   `--bar` sizes the magnitude tint, scaled by the route to the largest score
117   shown. #}
118{% macro health_row(r) %}
119<a class="hrow {{ 'hrow--up' if r.up else 'hrow--down' }}"
120   href="/s/{{ r.ticker|urlencode }}" style="--bar: {{ r.bar }}%">
121  <span class="hrow__sym num">{{ r.ticker }}</span>
122  <span class="hrow__body">
123    <span class="hrow__name">{{ r.name }}</span>
124    <span class="hrow__sub">
125      <span class="hrow__chip hrow__chip--{{ r.health.strength }}">{{ r.health.strength_label }}</span>
126      <span class="hrow__chip hrow__chip--{{ r.health.trajectory }}">{{ r.health.trajectory_label }}</span>
127      <span class="hrow__chip hrow__chip--{{ r.health.stability }}">{{ r.health.stability_label }}</span>
128    </span>
129  </span>
130  <span class="hrow__ret num">{% if r.ret_12m is not none %}{{ r.ret_12m|pct }}{% endif %}</span>
131  {{ verdict_badge({"grade": r.health.overall, "verdict": r.health.verdict}) }}
132</a>
133{% endmacro %}