<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Maciek Lamberski</title>
    <description>Writing about the web and building software.</description>
    <language>en</language>
    <lastBuildDate>Mon, 02 Feb 2026 00:00:00 GMT</lastBuildDate>
    <generator>Feedsmith</generator>
    <atom:link href="https://lamberski.com/feed" rel="self" type="application/rss+xml"/>
    <atom:link href="https://lamberski.com" rel="alternate" type="text/html"/>
    <item>
      <title>Minimalist API for stock prices in Google Sheets #2</title>
      <link>https://lamberski.com/blog/googlefinance-alternative-2</link>
      <guid isPermaLink="true">https://lamberski.com/blog/googlefinance-alternative-2</guid>
      <pubDate>Mon, 02 Feb 2026 00:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>When I initially <a href="/blog/googlefinance-alternative">came up with Stonkista</a>, I was naive to think that Google Sheets would happily welcome my idea with open arms. It didn’t take long to realize I had only replaced <code>#N/A</code> errors with the equally useless <code>Loading…</code> state in cells.</p>
<p>My spreadsheet includes a page that tracks a portfolio in various currencies over the last few years with weekly data points. That&#39;s roughly 270 rows × 5 columns. 1350 cells. And every single one of those cells called the API via <code>IMPORTDATA()</code>. That couldn&#39;t work.</p>
<p>After a bit of research, I found that <b>Google Sheets seems to cap external data calls at around 50 per sheet</b>. So the only viable solution was to load data in batches.</p>
<h2>Batching endpoints</h2>
<p>To make that possible, I added a new type of endpoints that return prices for a given date range.</p>
<pre><code class="language-text">/forex/usd/pln/2021-01-01..2025-12-31
</code></pre>
<p>The response is a list of prices separated by a new line, which Google Sheets conveniently renders as separate rows.</p>
<pre><code class="language-text">3.719628473
3.742906631
3.742885424
3.742885424
3.742885424
3.762350796
3.73821299
3.745310305
3.724625761
…
</code></pre>
<p>That means I can now pull the full dataset with a single formula:</p>
<pre><code class="language-text">=IMPORTDATA(&quot;https://stonkista.com/forex/chf/pln/2021-01-01..&quot; &amp; TEXT(TODAY(),&quot;yyyy-mm-dd&quot;))
</code></pre>
<p>I’ve been testing this for the last few weeks, and so far it has been very reliable. Instead of hundreds of individual requests, I can load a large chunk of historical data at once and chart the portfolio over time.</p>
<p>The main downside is that the sheet still takes a few seconds to fetch everything when the document opens. But that seems to be a general trade-off when importing external data into Google Sheets.</p>
<hr>
<p>For now, I’m calling this version done. It’s stable, it solves the problem I built it for, and it has held up well in my use. The only thing it’s really missing is a small website with documentation and examples so it’s easier for others to use too.</p>]]>
      </content:encoded>
    </item>
    <item>
      <title>Minimalist API for stock prices in Google Sheets</title>
      <link>https://lamberski.com/blog/googlefinance-alternative</link>
      <guid isPermaLink="true">https://lamberski.com/blog/googlefinance-alternative</guid>
      <pubDate>Mon, 15 Dec 2025 00:00:00 GMT</pubDate>
      <content:encoded>
        <![CDATA[<p>For a few years now, I&#39;ve been using Google Sheets to manage my investment portfolio. It shows the current state of the investments as well as the historical data on charts, allowing me to see how the internet-informed decisions impacted it over time.</p>
<p>To get the prices of various instruments, I use the built-in <code>GOOGLEFINANCE()</code> function. However, this is not without its cons. First, it does not have all the tickers I need, and is known for <a href="https://issuetracker.google.com/issues/182524129">causing</a> <a href="https://www.bogleheads.org/forum/viewtopic.php?t=415943">problems</a> <a href="https://support.google.com/docs/thread/320455428/googlefinance-not-working">with frequent</a> <a href="https://stackoverflow.com/q/59860596">#N/A errors</a>. This issue is especially annoying when many prices depend on each other. One invalid cell is enough to disrupt the entire calculation or chart. More than once I opened my spreadsheet only to find many of the numbers replaced with errors.</p>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="972.76 59.845 727.442 413.386" aria-label="Google Finance #N/A error">
  <style>
    svg { --bg: #fff; --fg: #222; --line: rgba(0, 0, 0, 0.075); }
    @media (prefers-color-scheme: dark) {
      svg { --bg: #222; --fg: #eee; --line: rgba(255, 255, 255, 0.075); }
    }
  </style>
  <rect fill="var(--bg)" x="972.76" y="59.845" width="727.442" height="413.386"/>
  <path fill="var(--fg)" d="m 1266.0896,275.55315 q -2.0424,0 -3.2095,-0.63219 -1.1185,-0.63219 -1.5562,-1.60479 -0.4377,-1.02123 -0.3404,-2.09109 0.097,-1.31301 1.0212,-2.09109 0.9726,-0.77808 2.4315,-0.77808 1.3617,0 2.7719,0 1.4103,-0.0486 2.8206,-0.0486 0.8267,-2.33424 1.7506,-4.71711 0.924,-2.4315 1.7994,-4.81437 -1.2644,-0.0973 -2.4315,-0.19452 -2.0911,-0.14589 -3.2096,-0.72945 -1.0699,-0.63219 -1.4589,-1.4589 -0.3404,-0.87534 -0.3404,-1.75068 0.049,-1.26438 0.8267,-2.04246 0.8267,-0.82671 2.5287,-0.82671 1.313,0 2.6747,0.19452 1.3616,0.14589 3.7445,0.38904 0.8753,-2.57739 1.4103,-4.71711 0.5835,-2.13972 0.6322,-3.69588 0.1945,-3.45273 3.015,-3.50136 1.7993,0 2.8206,1.16712 1.0212,1.11849 0.9239,3.16095 -0.049,1.4589 -0.5835,3.54999 -0.535,2.04246 -1.3617,4.52259 2.0911,0.0973 4.6685,0.19452 2.5774,0.0486 5.8356,0.14589 0.6808,-2.04246 1.1185,-3.84177 0.4377,-1.79931 0.4863,-3.25821 0.097,-1.41027 0.7781,-2.23698 0.7294,-0.87534 2.1883,-0.87534 1.7021,-0.0486 2.7719,1.02123 1.0699,1.06986 0.9726,3.45273 -0.049,1.21575 -0.389,2.67465 -0.3404,1.4589 -0.8753,3.11232 1.3616,0 2.7719,0 2.8205,0 2.8205,2.52876 0,1.36164 -1.1671,2.48013 -1.1185,1.06986 -3.1123,1.06986 -0.924,0 -1.7993,0 -0.8754,0 -1.7994,0 -0.778,1.9452 -1.6047,4.03629 -0.8268,2.04246 -1.6535,4.18218 1.7507,-0.0486 3.4528,-0.0486 1.702,-0.0486 3.3068,-0.0486 1.6534,0 2.3342,0.68082 0.7295,0.68082 0.7295,1.9452 0,1.4589 -1.2644,2.67465 -1.2644,1.21575 -4.0363,1.31301 -1.6048,0 -3.3554,0.0973 -1.7021,0.0486 -3.55,0.14589 -0.8267,2.82054 -1.4103,5.64108 -0.5836,2.77191 -0.7295,5.49519 -0.049,1.50753 -1.0212,2.33424 -0.924,0.87534 -2.1883,0.82671 -1.7507,-0.0486 -2.7719,-1.16712 -0.9726,-1.11849 -0.8268,-3.30684 0.097,-2.23698 0.5836,-4.61985 0.4863,-2.38287 1.2158,-4.81437 -2.6261,0.14589 -5.3007,0.34041 -2.626,0.14589 -5.1548,0.24315 -0.6322,2.23698 -1.0699,4.18218 -0.389,1.89657 -0.4863,3.45273 -0.049,1.4589 -0.9239,2.33424 -0.8754,0.87534 -2.3343,0.87534 -1.5561,-0.0486 -2.5774,-1.16712 -1.0212,-1.06986 -0.9239,-3.20958 0.049,-1.26438 0.3404,-2.82054 0.3404,-1.60479 0.8267,-3.35547 -0.8267,0 -1.5562,0 z m 14.4431,-16.43694 q -0.8267,2.18835 -1.702,4.47396 -0.8267,2.28561 -1.6048,4.52259 2.5774,-0.0973 5.2034,-0.19452 2.626,-0.0973 5.2034,-0.19452 0.8267,-2.13972 1.6534,-4.27944 0.8754,-2.13972 1.7021,-4.13355 -2.7233,-0.0486 -5.3493,-0.0486 -2.5774,-0.0486 -5.1062,-0.14589 z m 32.6307,19.59789 q -1.5561,0 -2.2856,-0.82671 -0.6808,-0.82671 -0.5835,-2.04246 0,-0.82671 0.2431,-1.60479 0.2918,-0.82671 0.6322,-1.99383 0.3404,-1.21575 0.6322,-3.11232 0.3404,-1.89657 0.4863,-4.863 0.1459,-2.18835 0.049,-4.47396 -0.049,-2.28561 -0.1459,-4.03629 -0.097,-1.79931 -0.049,-2.62602 0.097,-1.65342 0.9726,-2.67465 0.924,-1.06986 2.7719,-1.06986 2.4801,0 4.4253,1.36164 1.9939,1.31301 3.55,3.50136 1.6048,2.18835 2.9178,4.81437 1.3617,2.57739 2.5774,5.15478 1.2644,2.52876 2.5288,4.57122 1.313,2.04246 2.7719,3.11232 0.3404,-0.77808 0.5349,-1.84794 0.2432,-1.06986 0.3404,-2.57739 0.1459,-2.9178 0.097,-5.49519 0,-2.57739 0.1459,-5.00889 0.1945,-2.4315 0.8753,-4.81437 0.8754,-2.86917 3.2096,-2.86917 1.4589,0 2.1884,0.77808 0.778,0.72945 0.9239,1.84794 0.1945,1.11849 -0.049,2.13972 -0.6322,2.48013 -0.8267,4.61985 -0.1459,2.09109 -0.1459,4.27944 0,2.13972 -0.1459,4.91163 -0.1945,3.11232 -0.7781,5.59245 -0.5349,2.4315 -1.9452,3.84177 -1.4102,1.36164 -4.1335,1.36164 -2.4802,0 -4.3281,-1.36164 -1.8479,-1.41027 -3.3068,-3.64725 -1.4103,-2.28561 -2.7233,-4.96026 -1.2644,-2.72328 -2.6747,-5.39793 -1.4102,-2.72328 -3.2095,-4.863 0.1945,3.20958 0,6.12738 -0.1946,3.50136 -0.7295,6.46779 -0.4863,2.96643 -1.313,5.15478 -0.4377,1.11849 -1.313,1.84794 -0.8267,0.68082 -2.1884,0.68082 z m 34.6732,12.20613 q -1.8479,0 -2.7233,-1.31301 -0.8753,-1.26438 -0.4863,-3.25821 1.1185,-5.64108 2.7233,-11.42805 1.6534,-5.8356 3.6959,-11.42805 2.0424,-5.59245 4.328,-10.60134 2.3343,-5.05752 4.863,-9.2397 1.9939,-3.35547 4.3281,-3.35547 1.1671,0 1.9938,0.87534 0.8754,0.87534 0.9726,2.38287 0.097,1.4589 -0.9239,3.25821 -3.8904,6.56505 -6.7596,13.1301 -2.8692,6.51642 -4.9116,13.37325 -2.0425,6.8082 -3.5014,14.15133 -0.6322,3.45273 -3.5986,3.45273 z m 44.2047,-10.55271 q -1.3616,0 -2.3342,-0.82671 -0.9726,-0.82671 -1.9452,-2.77191 -0.7294,-1.36164 -1.2157,-3.20958 -0.4863,-1.89657 -0.8268,-3.98766 -4.2794,0.24315 -7.3431,0.72945 -3.0637,0.43767 -5.3979,0.82671 -0.7781,2.9178 -1.313,5.98149 -0.4863,2.57739 -2.7233,2.57739 -1.7993,0 -2.7233,-1.21575 -0.8753,-1.26438 -0.6808,-4.13355 0.049,-1.21575 0.2431,-2.52876 -1.5075,-0.19452 -2.1883,-0.92397 -0.6322,-0.77808 -0.5349,-1.84794 0.097,-1.75068 2.1397,-2.38287 0.8753,-0.29178 1.8479,-0.53493 0.7295,-2.72328 1.4589,-5.25204 0.7781,-2.52876 0.924,-5.10615 0.1945,-2.62602 2.2856,-2.62602 0.9726,0 1.8479,0.63219 2.0425,-3.11232 4.474,-4.863 2.4315,-1.75068 5.0575,-1.75068 3.2096,0 4.9603,1.60479 1.7993,1.55616 2.5774,4.96026 0.7781,3.35547 1.0698,8.7534 0,0.38904 0.049,0.77808 0.3404,0 0.7294,0 2.5774,0 3.4527,0.68082 0.8754,0.63219 0.8268,1.9452 -0.097,1.31301 -1.2158,2.28561 -1.1185,0.9726 -2.9664,1.21575 0.4863,2.28561 1.0212,3.8904 0.5349,1.60479 0.8753,2.72328 0.3891,1.11849 0.3405,2.04246 -0.1459,2.33424 -2.772,2.33424 z m -9.4342,-26.79513 q -1.5561,0 -3.5986,2.9178 -2.0425,2.86917 -3.939,7.92669 4.6198,-0.72945 10.0178,-1.02123 -0.049,-0.53493 -0.097,-1.11849 -0.1945,-3.59862 -0.4377,-5.49519 -0.2431,-1.89657 -0.7294,-2.52876 -0.4377,-0.68082 -1.2158,-0.68082 z" aria-label="#N/A"/>
  <path fill="var(--line)" d="M1747.758 217.698 1743.447 217.535 1728.219 217.248 1697.889 217.029 1651.343 216.865 1589.116 216.743 1512.764 216.654 1422.148 216.589 1331.672 216.541 1255.362 216.506 1190.339 216.481 1132.643 216.463 1082.121 216.449 1041.791 216.439 1010.463 216.433 986.563 216.427 967.944 216.424 953.249 216.421 942.327 216.419 934.472 216.418 928.892 216.417 923.79 216.416L921.014 216.416A0 0 0 0 1 921.014 212.968L928.892 212.968L934.472 212.966 942.327 212.965 953.249 212.963 967.944 212.96 986.564 212.956 1010.463 212.951 1041.791 212.944 1082.121 212.934 1132.643 212.921 1190.339 212.903 1255.362 212.878 1331.672 212.843 1422.148 212.795 1512.764 212.73 1589.116 212.64 1651.343 212.52 1697.889 212.355 1728.219 212.135 1743.447 211.849L1747.758 211.686A0 0 0 0 1 1747.758 217.698"/>
  <path fill="var(--line)" d="M1807.232 319.978 1803.569 319.815 1791.863 319.528 1769.248 319.309 1733.4 319.145 1683.4 319.023 1618.37 318.826 1539.507 318.253 1451.254 317.129 1365.37 315.818 1289.71 314.763 1218.29 313.972 1153.25 313.432 1103.464 313.136L1066.728 312.972L1037.006 312.882 1010.666 312.832 987.794 312.805 969.527 312.789 954.901 312.78 943.256 312.775 934.286 312.772 927.601 312.77 922.858 312.82 918.558 312.855 916.218 312.841A0 0 0 0 1 916.219 309.25L918.559 309.236 922.859 309.273 927.602 309.323 934.287 309.324 943.257 309.325 954.903 309.328 969.529 309.334 987.797 309.346 1010.672 309.369 1037.016 309.411 1066.743 309.491 1103.483 309.64 1153.277 309.916 1218.329 310.43 1289.759 311.184 1365.424 312.189 1451.299 313.43 1539.529 314.459 1618.377 314.901 1683.4 314.919 1733.4 314.799 1769.248 314.634 1791.863 314.414 1803.569 314.128L1807.232 313.966A0 0 0 0 1 1807.232 319.978"/>
  <path fill="var(--line)" d="M1500.33 545.882 1500.406 544.014 1500.614 539.559 1500.844 533.216 1501.013 523.264 1501.136 509.466 1501.226 493.583 1501.291 475.413 1501.338 454.203 1501.372 429.851 1501.397 402.747 1501.415 375.766 1501.428 350.963 1501.438 326.393 1501.415 301.087 1501.345 276.5 1501.275 253.16 1501.233 231.078 1501.21 210.432 1501.196 191.242 1501.188 174.376 1501.184 160.35 1501.182 145.338 1501.164 130.196 1501.097 117.788 1500.997 106.572 1500.893 96.386 1500.788 87.07 1500.69 78.844 1500.6 71.984 1500.532 65.933 1500.456 59.505 1500.379 53.053 1500.289 48.114 1500.109 44.752 1499.802 40.716 1499.609 38.077A2.3 2.3 0 0 1 1504.206 37.992L1504.111 40.646 1503.941 44.71 1503.831 48.085 1503.823 53.013 1503.9 59.465 1503.976 65.894 1504.045 71.939 1504.134 78.803 1504.233 87.031 1504.337 96.351 1504.442 106.541 1504.542 117.769 1504.61 130.191 1504.63 145.337 1504.632 160.348 1504.638 174.374 1504.648 191.239 1504.664 210.428 1504.691 231.071 1504.737 253.149 1504.815 276.489 1504.895 301.083 1504.931 326.393 1504.941 350.963 1504.954 375.766 1504.971 402.748 1504.996 429.852 1505.029 454.202 1505.075 475.414 1505.137 493.584 1505.221 509.468 1505.335 523.268 1505.485 533.229 1505.683 539.597 1505.831 544.07 1505.868 545.94A2.769 2.769 0 0 1 1500.329 545.881"/>
  <path fill="var(--line)" d="M1174.603 511.8 1174.712 509.637L1174.923 504.55L1175.103 497.761 1175.237 488.936 1175.335 478.919 1175.407 468.947 1175.46 458.556 1175.498 446.965 1175.526 434.283 1175.546 421.039 1175.561 407.436 1175.571 393.556 1175.579 379.633 1175.585 365.862 1175.589 352.558 1175.592 339.571 1175.594 326.557 1175.596 313.714L1175.596 301.1L1175.598 288.198L1175.598 258.247L1175.599 240.31L1175.599 132.305L1175.611 116.137 1175.641 99.113 1175.676 85.149 1175.715 73.041 1175.759 62.491 1175.799 53.551 1175.83 46.626 1175.858 41.234 1175.885 36.354 1175.908 31.724 1175.878 27.531 1175.774 23.889 1175.635 20.808 1175.425 17.412 1175.152 13.809 1174.892 10.407 1174.769 8.609A2.964 2.964 0 0 1 1180.697 8.614L1180.569 10.416 1180.294 13.827 1179.988 17.435 1179.74 20.827 1179.573 23.907 1179.433 27.55 1179.357 31.743 1179.329 36.373 1179.302 41.252 1179.275 46.642 1179.244 53.567 1179.203 62.506 1179.159 73.053 1179.12 85.158 1179.085 99.119 1179.055 116.139 1179.045 132.305L1179.045 274.825L1179.046 288.197 1179.047 301.099 1179.048 313.713 1179.05 326.556 1179.052 339.57 1179.055 352.557 1179.059 365.861 1179.065 379.631 1179.072 393.555 1179.083 407.435 1179.098 421.038 1179.118 434.282 1179.146 446.964 1179.184 458.554 1179.237 468.946 1179.309 478.918 1179.407 488.935 1179.541 497.76 1179.721 504.549 1179.933 509.637L1180.044 511.8A2.72 2.72 0 0 1 1174.602 511.8"/>
  <path fill="var(--line)" d="M862.471 393.745 866.134 393.908 877.84 394.195 900.455 394.414 936.303 394.578 986.303 394.7 1051.333 394.897 1130.196 395.47 1218.449 396.594 1304.333 397.905 1379.993 398.96 1451.413 399.751 1516.453 400.291 1566.239 400.587L1602.975 400.751L1632.697 400.841 1659.037 400.891 1681.909 400.918 1700.176 400.934 1714.802 400.943 1726.447 400.948 1735.417 400.951 1742.102 400.953 1746.845 400.903 1751.145 400.868 1753.485 400.882A0 0 0 0 1 1753.484 404.473L1751.144 404.487 1746.844 404.45 1742.101 404.4 1735.416 404.399 1726.446 404.398 1714.8 404.395 1700.174 404.389 1681.906 404.377 1659.031 404.354 1632.687 404.312 1602.96 404.232 1566.22 404.083 1516.426 403.807 1451.374 403.293 1379.944 402.539 1304.279 401.534 1218.404 400.293 1130.174 399.264 1051.326 398.822 986.303 398.804 936.303 398.924 900.455 399.089 877.84 399.309 866.134 399.595L862.471 399.757A0 0 0 0 1 862.471 393.745"/>
  <path fill="var(--line)" d="M918.047 126.692 921.854 126.842 930.946 127.108 943.889 127.306 964.169 127.452 992.278 127.559L1024.632 127.638L1061.643 127.695 1104.848 127.736 1154.451 127.766 1209.661 127.788 1264.621 127.804 1315.144 127.816 1365.191 127.824 1416.743 127.82 1466.833 127.798 1514.376 127.774 1559.353 127.761 1601.406 127.754 1640.493 127.75 1674.847 127.748 1703.419 127.747L1733.996 127.747L1764.844 127.74 1790.132 127.716 1812.988 127.681 1833.741 127.644 1852.721 127.607 1869.479 127.572 1883.457 127.54 1895.777 127.516 1908.871 127.489 1922.015 127.462 1932.065 127.447 1938.925 127.435 1943.686 127.362 1947.18 127.212 1950.667 127.096 1952.563 127.065A0 0 0 0 1 1952.574 131.165L1950.678 131.145 1947.193 131.048 1943.698 130.922 1938.931 130.879 1932.069 130.891 1922.022 130.906 1908.878 130.933 1895.784 130.961 1883.464 130.985 1869.487 131.016 1852.728 131.052 1833.748 131.089 1812.994 131.126 1790.134 131.162 1764.844 131.186 1733.996 131.193 1703.419 131.194 1674.848 131.197 1640.493 131.201 1601.407 131.207 1559.354 131.217 1514.378 131.235 1466.836 131.265 1416.744 131.296 1365.191 131.312 1315.144 131.321 1264.622 131.332 1209.662 131.348 1154.452 131.37 1104.848 131.4 1061.643 131.44 1024.633 131.497 992.277 131.573 964.169 131.677 943.886 131.817 930.939 132.003 921.845 132.247 918.037 132.384A0 0 0 0 1 918.047 126.692"/>
</svg>

<h2>Looking for an alternative</h2>
<p>I started looking for an alternative which would cover all my needs:</p>
<ul>
<li>support for ETFs, commodities, forex, and crypto,</li>
<li>the ability to get historical data of each instrument.</li>
</ul>
<p>Ideally, the replacement would also be as simple as the solution built into Google Sheets. One function you pass an argument to and get the value in return. No explicit casting to number, no plucking for nested JSON values.</p>
<p>After some research, I found <a href="https://cryptoprices.cc">Crypto Prices</a>. It checks the box for simplicity, but is limited to cryptocurrencies and only current prices. Though not ideal, I really liked the idea of using <code>IMPORTDATA()</code> with API endpoints returning text with the price. Inspired by this simplicity, I decided to build the solution myself.</p>
<h2>Custom solution</h2>
<p>This solution is <strong>Stonkista</strong>. A minimalist API service inspired by Crypto Prices, but designed for broad markets and providing access to historical data. The name is kinda dorky, but I like it! </p>
<p>Getting the prices is a breeze and endpoints pretty self-explanatory. Want the current price of Apple stock? Put this into the cell and that&#39;s it.</p>
<pre><code class="language-text">=IMPORTDATA(&quot;https://stonkista.com/AAPL&quot;)
</code></pre>
<p>Stonkista has no usage limits, no sign ups, no API keys. It&#39;s open source and <a href="https://github.com/macieklamberski/stonkista">available on GitHub</a>.</p>
<h2>Implementation</h2>
<p>First, I needed to find data sources. I wanted free sources with current and historical data, and ideally accessible without any API keys. I ended up choosing:</p>
<ul>
<li><a href="https://www.coingecko.com/api">Coin Gecko API</a> for cryptocurrency prices (also used by Crypto Prices). Can be used either with or without an API key.</li>
<li><a href="https://frankfurter.dev">Frankfurter</a> for currency rates. It provides the official data published by the European Central Bank and includes ~30 commonly traded currencies.</li>
<li><a href="https://finance.yahoo.com">Unofficial Yahoo Finance API</a> for all the other instruments. Though unofficial, it has worked reliably for a long time. There are even Python/JS wrappers for it. I decided to use the API directly.</li>
</ul>
<p>I used my usual go-to stack: Bun, Hono, Drizzle, BullMQ, Redis and Postgres, stitched together with a Docker Compose. Created background jobs to call the APIs periodically and store current prices. I also wrote a script to backfill the database with historical data.</p>
<h3>API design</h3>
<p>I wanted the URLs to be obvious at a glance. Ticker first, then optional modifiers like currency or date:</p>
<pre><code class="language-text">/AAPL              → current price of Apple
/AAPL/EUR          → converted to EUR
/AAPL/2024-01-15   → historical price
/crypto/BTC/PLN    → Bitcoin in PLN
/forex/USD/PLN     → exchange rate
</code></pre>
<p>One quirk I ran into was that many crypto symbols conflict with stock tickers. <code>ETH</code> is both Ethereum and Ethan Allen Interiors, <code>LUNA</code> could be Terra or Luna Innovations. I prefixed crypto endpoints with <code>/crypto/</code> to avoid ambiguity.</p>
<p>Another thing to handle was price holes. Markets don&#39;t trade on weekends and holidays, so instead of returning errors for those dates, the API falls back to the last available price.</p>
<h3>Lazy fetching</h3>
<p>Yahoo Finance has thousands of tickers, so pre-populating the database with all of them wasn&#39;t practical. Instead, when someone requests a ticker that&#39;s not in the database, it gets fetched from Yahoo on demand, including full price history, and cached for future requests. The first request is slower, but all following ones are instant.</p>
<h2>Wrapping up</h2>
<p>My own spreadsheet pulls ~20 tickers with history going back to 2021. No more #N/A cascades. There are limitations, though, where Google Sheets struggles to load many values at once, sometimes taking a long time to populate. I might dig into those in a future post.</p>]]>
      </content:encoded>
    </item>
  </channel>
</rss>
