Embed interactive live stock price badges anywhere on your site. Badges display real-time price, change, and direction and open a full-detail popup on click — all with zero dependencies and support for four CSS frameworks.
- Features
- Requirements
- Installation
- Quick Start
- Data Providers
- Tracked Companies & Company Manager
- TextFormatter — Automatic Badge Injection
- PHP API
- CSS Frameworks
- Untracked Ticker Modes
- Caching & Circuit Breaker
- Asset Injection
- Adding a Custom Provider
- Module Configuration Reference
- Changelog
- License
- Author
- Live price badges — ticker symbol, current price, change amount and percentage, up/down direction arrow
- Popup detail panel — click any badge to open a floating panel with open/high/low, previous close, volume, 52-week range, P/E ratio, EPS, beta, dividend yield, market state, and data timestamp
- Four CSS frameworks — Vanilla CSS (built-in), Tailwind CSS, Bootstrap 5, UIkit 3; auto-detected or manually selected
- Four data providers — Yahoo Finance (free, no key), Finnhub (free 60 req/min), Alpha Vantage (free 25 req/day)
- TextFormatter module — three parse modes for automatic badge injection into any text field: explicit tags, cashtag/hashtag notation, full auto-detection by company name and aliases
- Company Manager — admin UI for tracking companies with names, aliases, enable/disable toggles, bulk CSV import
- File-based cache — configurable TTL, per-ticker cache clear from the admin panel
- Circuit breaker — automatically pauses API calls after repeated failures and returns stale cache instead
- Stale data indicator —
~marker on badges served from stale cache - Accessible markup —
role="button",aria-expanded,tabindex="0", keyboard-navigable popups
- ProcessWire 3.0.0 or newer
- PHP 8.2 or newer
allow_url_fopen = Onor cURL enabled (for HTTP requests to data providers)
Via admin:
- Download the ZIP from github.com/mxmsmnv/Stocks
- In your PW admin go to Modules → Install → Upload ZIP
- Install both Stocks and (optionally) TextFormatter Stocks
Then go to Modules → Refresh and install.
Go to Modules → Configure → Stocks:
- Choose a data provider (Yahoo Finance works without any API key)
- Add a few tracked companies (ticker, name, optional aliases)
- Leave everything else at defaults
<?php
$stocks = $modules->get('Stocks');
echo $stocks->renderBadge('AAPL');That's it — the badge renders with the current price, and the JS popup is injected automatically before </head>.
Go to Fields → your_body_field → Details → Text formatters and add TextFormatter Stocks. Now you can write [stock:AAPL] or $AAPL directly in your content and badges appear automatically.
| Provider | Cost | Rate Limit | Data Depth | API Key |
|---|---|---|---|---|
| Yahoo Finance | Free | Unofficial | Price, 52w, exchange | Not required |
| Finnhub | Free tier | 60 req/min | Real-time US, profile, metrics | finnhub.io/register |
| Alpha Vantage | Free tier | 25 req/day | Basic — no 52w, no currency | alphavantage.co |
Yahoo Finance is the default and requires no setup. It uses the unofficial v8 chart API — reliable for most use cases, but unofficial (may change without notice).
Finnhub is the recommended free provider if you need real company names, P/E ratio, and better reliability. The free tier covers 60 requests per minute, which is more than enough when caching is enabled.
The Company Manager is the admin UI under Modules → Configure → Stocks → Tracked Companies.
Only companies in this list will show live badges. Untracked tickers are handled by the Untracked Ticker Mode setting.
Each company record has:
| Field | Description |
|---|---|
| Ticker | Stock symbol, e.g. AAPL, TSLA, BRK.B |
| Company Name | Human-readable name, e.g. Apple Inc. — used in the popup header and in auto-parse mode |
| Aliases | Comma-separated alternative names, e.g. apple, iphone, ios — matched in auto-parse mode |
| Auto-parse | When enabled, company name and aliases trigger badge auto-injection in text fields (mode 3) |
| Active | When disabled, the ticker is treated as untracked even though it is listed |
The collapsed Bulk Import section accepts one ticker per line in three formats:
AAPL
AAPL=Apple Inc.
AAPL=Apple Inc.=apple,iphone,ios
Check Merge with existing to add to the current list, or uncheck to replace all.
Install TextFormatter Stocks and attach it to any text field. The formatter runs after HTML formatters (Markdown, Textile, etc.) and is HTML-safe — it never touches content inside <pre>, <code>, <script>, <style>, or <a> tags, and never modifies HTML attributes.
Configure the formatter under Modules → Configure → TextFormatter Stocks.
Only [stock:...] tags are recognised. No automatic scanning. Best for content written by editors who want full control.
By ticker:
Investors are watching [stock:AAPL] closely this quarter.
By company name (resolves to ticker):
[stock:Apple Inc] reported record earnings.
With inline options (reserved for future renderer options):
[stock:AAPL theme="dark"]
Unknown tag (neither a valid ticker nor a resolvable name):
[stock:FooBar]
→ <span class="stocks-ticker stocks-unknown" ...>FooBar</span>
Includes everything from Mode 1, plus $TICKER and #TICKER notation. Useful for finance-focused editorial content.
$AAPL hit a new all-time high today while #TSLA pulled back 3%.
Meanwhile [stock:GOOGL] trades sideways.
Tickers in the Excluded Words list (default: IT, AT, ON, AI, OR, GO) are never matched to avoid false positives with common words.
Includes everything from Modes 1 and 2, plus automatic matching of company names and aliases from your tracked list. The formatter builds a single regex from all tracked names/aliases, sorts by length (longest first to avoid partial matches), and replaces each occurrence once.
Apple reported strong iPhone sales, pushing $AAPL higher.
Tesla meanwhile cut prices again — #TSLA bears are watching closely.
Alphabet's YouTube revenue surprised analysts.
If Apple, iPhone, Tesla, and Alphabet are configured as names/aliases for their respective tickers, all four will render as badges.
Minimum word length (default: 3) prevents very short aliases from triggering false positives.
The formatter never double-replaces a match — once a range in the text is claimed by a replacement, it is locked for all subsequent passes.
Get the module instance anywhere in your templates or hooks:
$stocks = $modules->get('Stocks');Render a badge for a tracked ticker using the configured theme.
string renderBadge(string $ticker, array $options = [])// Basic badge
echo $stocks->renderBadge('AAPL');
// Untracked ticker — rendered per untracked_mode setting
echo $stocks->renderBadge('UNKNOWN');If the ticker is not in the tracked list, the output depends on the Untracked Ticker Mode setting (plain text, hidden, or empty badge shell).
If the API call fails and there is no cache, an error badge is returned:
<span class="stocks-ticker stocks-error" data-ticker="AAPL">AAPL</span>Render a badge with a theme override, ignoring the global ui_theme setting. Useful for sidebars or widgets that use a different framework from the rest of the page.
string renderBadgeAs(string $ticker, string $theme, array $options = [])Valid $theme values: vanilla, tailwind, bootstrap, uikit
// Force Bootstrap badge even if the site uses Tailwind
echo $stocks->renderBadgeAs('TSLA', 'bootstrap');
// Force Vanilla CSS
echo $stocks->renderBadgeAs('MSFT', 'vanilla');Fetch raw normalised stock data for one ticker. Returns an array on success, false on failure.
array|false getStock(string $ticker, bool $forceRefresh = false)$data = $stocks->getStock('AAPL');
if ($data) {
echo $data['ticker']; // 'AAPL'
echo $data['name']; // 'Apple Inc.'
echo $data['price']; // 189.34
echo $data['change']; // 2.15
echo $data['change_percent']; // 1.15 (%)
echo $data['price_high']; // 190.12
echo $data['price_low']; // 186.50
echo $data['price_prev_close']; // 187.19
echo $data['volume']; // 54230000
echo $data['market_cap']; // 2940000000000
echo $data['currency']; // 'USD'
echo $data['exchange']; // 'NMS'
echo $data['market_state']; // 'REGULAR' | 'PRE' | 'POST' | 'CLOSED'
echo $data['fifty_two_week_high']; // 199.62
echo $data['fifty_two_week_low']; // 124.17
echo $data['pe_ratio']; // 29.4 (Finnhub only)
echo $data['eps']; // 6.44 (Finnhub only)
echo $data['beta']; // 1.21 (Finnhub only)
echo $data['provider']; // 'yahoo' | 'finnhub' | 'alphavantage'
echo $data['fetched_at']; // Unix timestamp
echo $data['cached_at']; // Unix timestamp of cache file mtime
echo $data['cache_age']; // Seconds since last fetch
// $data['stale'] === true if served from expired cache
}Force a fresh API call (bypass cache):
$fresh = $stocks->getStock('AAPL', true);Fetch data for multiple tickers in one call. Returns an associative array keyed by uppercase ticker; failed/missing tickers are omitted.
array getStocks(array $tickers)$data = $stocks->getStocks(['AAPL', 'TSLA', 'MSFT', 'GOOGL']);
foreach ($data as $ticker => $quote) {
echo "$ticker: {$quote['price']} ({$quote['change_percent']}%)\n";
}
// AAPL: 189.34 (1.15%)
// TSLA: 242.80 (-2.31%)
// MSFT: 415.60 (0.42%)
// GOOGL: 174.20 (0.88%)Warm the cache for all tracked and enabled companies. Useful in a LazyCron hook or a manual warm-up script.
array prefetchAll() // returns [ 'AAPL' => true, 'TSLA' => false, ... ]// Warm cache on a schedule (requires LazyCron module)
$this->addHook('LazyCron::every30Minutes', function() {
$stocks = $this->modules->get('Stocks');
$result = $stocks->prefetchAll();
$ok = count(array_filter($result));
$fail = count($result) - $ok;
$this->log->save('stocks', "Prefetch: $ok OK, $fail failed");
});Delete one or all ticker cache files.
int clearCache(string|null $ticker = null) // returns number of files deleted// Clear one ticker
$stocks->clearCache('AAPL');
// Clear everything
$stocks->clearCache();The module detects which CSS framework your site uses by inspecting $config->styles and $config->scripts. You can override this with the UI Theme setting in the module config.
| Theme | Rendering approach |
|---|---|
vanilla |
Built-in stocks.css with CSS variables. Injected automatically into <head>. |
tailwind |
Utility classes only. No extra CSS needed — works with Tailwind CDN or CLI. |
bootstrap |
Uses .badge, .text-bg-success, .text-bg-danger, .font-monospace. Bootstrap 5+. |
uikit |
Uses .uk-label, .uk-label-success, .uk-label-danger. UIkit 3. |
Auto-detect (default) inspects loaded assets and falls back to vanilla if no framework is found.
You can also force a specific theme per-badge with renderBadgeAs():
// Render a Bootstrap badge on a Tailwind site
echo $stocks->renderBadgeAs('NVDA', 'bootstrap');When a ticker is referenced (via TextFormatter or renderBadge()) but is not in the tracked company list — or its Active toggle is off — the module applies the configured fallback:
| Mode | Output | API call? |
|---|---|---|
plain (default) |
<span class="stocks-untracked" data-ticker="XYZ">XYZ</span> |
No |
hide |
(empty string) | No |
badge_nodata |
Badge shell with N/A label |
No |
Configure under Modules → Configure → Stocks → Untracked Tickers.
Stock data is cached as JSON files in site/assets/cache/stocks/ (one file per ticker, e.g. AAPL.json).
- Default TTL: 300 seconds (5 minutes)
- Cache files survive server restarts and are independent of ProcessWire's WireCache
- You can view all cached files, their size and age, and clear individual files from Modules → Configure → Stocks → Cache
If the API fails 2 times in a row, the circuit breaker opens for 60 seconds. During this window:
- No new API calls are made
- The module returns stale cache if available (badge displays a
~marker) - After 60 seconds, the circuit resets and API calls resume
This protects your site from latency spikes caused by a downstream provider outage.
The module hooks Page::render to inject assets automatically:
Frontend pages (when stocks-ticker or stocks-untracked is found in the rendered HTML):
<!-- Injected before </head> -->
<link rel="stylesheet" href="/site/modules/Stocks/css/stocks.css">
<script>window.StocksConfig = {"theme":"vanilla","popupStyle":"vanilla","moduleUrl":"..."};</script>
<script src="/site/modules/Stocks/js/stocks.js" defer></script>CSS is only injected when the active theme is vanilla. The StocksConfig global is always injected (needed for popup rendering). To include assets manually, uncheck Inject built-in CSS and/or Inject popup JavaScript in the module config.
Admin pages — stocks-admin.js is injected only on the Stocks and TextFormatter Stocks config pages, powering the Company Manager UI.
To integrate a different data source:
- Create
/site/modules/Stocks/providers/StocksProviderMySource.php - Extend
StocksProviderBaseand implementfetchQuote()andgetProviderName() - Return the normalised array (same keys as the Yahoo example above)
- Add
'mysource' => 'StocksProviderMySource'to thePROVIDERSconstant inStocksAPI.php - Add the option to the
api_providerselect inStocks.module.php
Minimal provider skeleton:
<?php
class StocksProviderMySource extends StocksProviderBase {
public function getProviderName() { return 'mysource'; }
public function requiresKey() { return true; }
public function fetchQuote($ticker) {
if (empty($this->apiKey)) return false;
$url = 'https://api.mysource.com/quote/' . urlencode($ticker)
. '?apikey=' . urlencode($this->apiKey);
$raw = $this->httpGet($url); // cURL/file_get_contents with timeout
$json = $this->decodeJson($raw);
if (!$json) return false;
return [
'ticker' => strtoupper($ticker),
'name' => $json['companyName'] ?? $ticker,
'price' => (float) $json['latestPrice'],
'price_open' => (float) ($json['open'] ?? 0),
'price_high' => (float) ($json['high'] ?? 0),
'price_low' => (float) ($json['low'] ?? 0),
'price_prev_close' => (float) ($json['previousClose'] ?? 0),
'change' => (float) ($json['change'] ?? 0),
'change_percent' => (float) ($json['changePercent'] ?? 0) * 100,
'volume' => (int) ($json['volume'] ?? 0),
'avg_volume' => (int) ($json['avg30Volume'] ?? 0),
'market_cap' => (float) ($json['marketCap'] ?? 0),
'currency' => 'USD',
'exchange' => $json['primaryExchange'] ?? '',
'market_state' => 'REGULAR',
'fifty_two_week_high' => (float) ($json['week52High'] ?? 0),
'fifty_two_week_low' => (float) ($json['week52Low'] ?? 0),
'pe_ratio' => isset($json['peRatio']) ? (float) $json['peRatio'] : null,
'dividend_yield' => null,
'eps' => null,
'beta' => null,
'fetched_at' => time(),
'provider' => $this->getProviderName(),
];
}
}StocksProviderBase provides $this->apiKey, $this->timeout, httpGet($url), and decodeJson($raw).
| Setting | Default | Description |
|---|---|---|
api_provider |
yahoo |
Data provider: yahoo, finnhub, alphavantage |
api_key |
(empty) | API key for providers that require one |
cache_time |
300 |
Cache TTL in seconds (minimum 60) |
request_timeout |
10 |
HTTP timeout for API calls (3–30 s) |
ui_theme |
auto |
CSS framework: auto, vanilla, tailwind, bootstrap, uikit |
inject_css |
1 |
Inject stocks.css automatically (vanilla theme only) |
inject_js |
1 |
Inject stocks.js and StocksConfig automatically |
companies_json |
[] |
JSON blob storing tracked company records |
untracked_mode |
plain |
How untracked tickers render: plain, hide, badge_nodata |
cache_dir |
stocks |
Subfolder name inside site/assets/cache/ |
| Setting | Default | Description |
|---|---|---|
parse_mode |
dollar |
explicit, dollar, or auto |
excluded_words |
IT,AT,ON,AI,OR,GO |
Comma-separated tickers/words never auto-linked |
skip_tags |
pre,code,script,style,a |
HTML tags whose content is never processed |
min_word_len |
3 |
Minimum alias length for auto-detection (mode 3) |
See CHANGELOG.md
MIT — see LICENSE
Maxim Semenov · smnv.org · maxim@smnv.org
GitHub: github.com/mxmsmnv/Stocks