Plotting trades on a price chart looks like the easy part of a trading journal. The user imports a CSV of fills, the app draws a marker per fill on a candlestick chart, and the user sees where each entry and exit landed relative to the price action. The hard part is the bar under the marker. A fill at 14:37:12 UTC needs the 1-minute bar that opened at 14:37, for that instrument, on that date. If the app only has a 15-minute or daily bar, the marker gets pinned next to a price the trade never touched.
Why the marker needs the bar at the fill minute#
Charting libraries place a marker by time and price. Both come from the fill record, so a lone marker is always correct. The problem is what gets drawn around it. If the chart is built from 15-minute bars, the candle that contains the fill spans 14:30 to 14:45. Its open and close can sit well away from the fill price, and a stop that was hit inside that quarter hour is invisible because the bar shows only a range. On a daily chart it's worse. The candle covers the whole session, and a scalp that entered and exited within four minutes collapses into two markers stacked inside one candle, often outside its body.
Users notice this immediately. A journal that shows an entry at 187.20 on a candle whose range is 186.10 to 186.90 looks wrong, and the user stops trusting the P&L the app computes from the same data. More precise markers won't fix it. What fixes it is a 1-minute bar series that actually contains the fill minute, so the chart around each trade is the chart the trader was looking at.
There is a second use for that exact bar. Tax and reporting tools sometimes need a reference price at a timestamp when there was no fill: a crypto transfer between wallets, a corporate action, a token grant. The same lookup that places a marker answers the question of what an instrument traded at during a given minute.
Where free and bundled price feeds fall short#
Most journal apps start on whatever price source is at hand: the free tier of a quote API, the data bundled with a charting widget, or the broker's own history export. These work in the demo. They break on the backfill.
Intraday depth is the first gap. Free feeds commonly offer minute bars for recent weeks only, and daily bars beyond that. A user importing three years of trades gets accurate charts for the last month and daily approximations for everything older. Coverage thins further on less traded instruments, where a minute series may simply be missing for the date in question.
Forex and crypto add their own holes. A feed built around US stocks often carries FX as daily closes only, which is useless for a 1-minute marker on a EURUSD scalp. Crypto history taken from a single venue has gaps wherever that venue had an outage, delisted the pair, or didn't list it yet on the fill date.
Rate limits finish the job. A journal with 4,000 saved trades that fetches one bar per trade makes 4,000 calls. At a ceiling of 60 requests per minute, that backfill takes over an hour while the user watches a spinner. Multiply by every new signup and the import screen becomes the slowest and least reliable page in the app.
Stitching four sources into one chart#
The usual response is to add sources: one for stocks, one for forex, one for crypto, another for gold and oil. Now the app has four JSON shapes. One returns epoch seconds, one ISO strings, one exchange-local time with no offset. One labels a bar by its open time, another by its close. Volume is a float in one, a string in another, absent in the FX feed. Every one of these differences becomes a branch in the import code, and every branch is a place where a marker lands one bar off.
Session conventions are the subtle version of the same problem. Crypto trades on Saturday. Forex doesn't, so a bar series that pads weekends with flat candles will put a marker for a Sunday-evening rollover fill onto a synthetic bar. A US stock fill at 08:15 local time happened in the pre-market session, which some feeds omit from the minute series entirely. A daily bar for gold, for crude oil, and for a US stock each open at a different hour. Those rules are per asset class, and a journal that treats them all as one bar per calendar day draws the wrong day for part of its users. The earlier post on daily bar boundaries covers the exact open and close times per class.
What to design for instead#
Four properties make the lookup reliable across asset classes.
Deep 1-minute history behind one credential. The SiftingIO bars endpoint returns 1-minute to 1-month bars for US stocks and 1-minute to 1-hour bars for forex and crypto, all under the same /v1/hist family with the same response shape. History depth is set by plan: one month on the Free tier, one year on Builder, full history on Pro and above. An app that expects to import years of trades should size the plan to the oldest fill it needs to draw.
One response shape. Every bar carries the same fields regardless of class, and every timestamp is UTC, so the import code has one parser and one time convention.
A fetch pattern that respects the rate limit. Fetch by symbol and UTC day rather than one call per trade. One request for AAPL on 2026-08-14 returns the minute bars covering every AAPL fill that day. A user with 4,000 trades spread over 60 symbol-days needs roughly 60 requests. A page returns up to 2,000 bars and a full day holds at most 1,440 one-minute bars, so a single request usually covers the day; if it ever overflows, continue through the cursor in meta.next_cursor.
Correct session boundaries. Bars exist only where the market traded, so a missing bar at the fill minute carries information. It means the timestamp is in the wrong zone, the fill was outside the session the series covers, or the market was closed. Each of those deserves a visible warning in the import UI, not a marker snapped to the nearest bar.
Here is the lookup in Python. The bars endpoint requires gzip, and the requests library decompresses transparently once the header is set. Field names follow the bars schema in /docs; the time-range parameter names are documented there as well.
import os
from datetime import datetime, timezone
import requests
KEY = os.environ["SIFTING_KEY"]
BASE = "https://api.sifting.io/v1/hist"
HEADERS = {"X-API-Key": KEY, "Accept-Encoding": "gzip"}
def minute_bars(asset_class, symbol, day):
"""Every 1-minute bar for one symbol on one UTC date, keyed by open time (epoch ms)."""
bars = {}
params = {"interval": "1m", "limit": 2000,
"start": f"{day}T00:00:00Z", "end": f"{day}T23:59:59Z"} # range params: see /docs
while True:
r = requests.get(f"{BASE}/{asset_class}/{symbol}/bars", headers=HEADERS, params=params)
r.raise_for_status()
body = r.json()
for bar in body["data"]:
bars[bar["t"]] = bar
cursor = body["meta"]["next_cursor"]
if not cursor:
return bars
params = {"cursor": cursor}
def bar_at_fill(bars, fill_ts):
"""fill_ts must be timezone-aware. Floors to the minute in UTC and returns the bar or None."""
minute = fill_ts.astimezone(timezone.utc).replace(second=0, microsecond=0)
return bars.get(int(minute.timestamp() * 1000))
day_bars = minute_bars("stocks", "AAPL", "2026-08-14")
fill = datetime(2026, 8, 14, 14, 37, 12, tzinfo=timezone.utc)
bar = bar_at_fill(day_bars, fill)
print(bar or "no bar at fill minute: check timezone and session")
For forex and crypto the only change is the path: minute_bars("forex", "EURUSD", day) and minute_bars("crypto", "BTCUSD", day) go through the same parser and the same dictionary key. Commodities such as XAUUSD use the same bars family; the exact path is in /docs. Group the imported trades by asset class, symbol, and UTC date before fetching, cache each day series locally so a re-import costs nothing, and hand the series to the chart. With Lightweight Charts that is one setData call for the candles and one setMarkers call for the fills, with each marker's time set to the bar's open time so it snaps to the right candle.
Common pitfalls#
406 gzip_required on the bars endpoint. Historical bars require gzip. A client that omits Accept-Encoding: gzip gets a 406 with error code gzip_required and no bars. Most HTTP libraries send the header by default, but some minimal fetch wrappers and corporate proxies strip it, and the failure looks like a broken endpoint until the response body is read.
Broker exports in local time. Fill timestamps often arrive without an offset, in the broker's or the user's local time. Convert to UTC first and floor to the minute second. Flooring before conversion shifts every fill on a daylight-saving transition day by an hour, and the whole day's markers land on the wrong candles. If lookups miss for an entire import, the timezone is the first suspect.
403 on the second asset class. Plans are per market. A key entitled to US stocks returns 403 on a crypto bars request until the crypto market is added to the subscription. That is an entitlement error rather than an auth error; a bad key returns 401 unauthorized. During a large backfill, also watch X-RateLimit-Remaining and honor Retry-After on any 429 instead of retrying immediately.
Read the docs for the bars schema, time-range parameters, and the history depth on each plan.



