Importing broker CSVs without trusting them
Lessons from building trade imports for a trading journal: detecting the broker, validating every row, and making re-imports safe.
Every broker exports differently
In a trading journal, users import their trades from the broker's CSV export. Every broker has its own columns, date formats, decimal separators and ideas about what a "trade" is. Some split one fill into several rows; some include cancelled orders.
Detect, don't ask
Asking users to pick their broker from a list works badly; they pick the wrong one. Instead, the importer looks at the header row and a few sample rows and scores each known format. Only when no format is confident does it ask.
Validate every row, show every problem
Each format maps rows into one internal shape, validated with a schema. Invalid rows are not silently skipped; the preview shows them with the reason, and the user decides.
const Trade = z.object({
symbol: z.string().min(1),
side: z.enum(["buy", "sell"]),
quantity: z.number().positive(),
price: z.number().positive(),
executedAt: z.coerce.date(),
});Make re-imports safe
Users import the same file twice, or overlapping date ranges. Each trade gets a stable key built from the broker's own identifiers (or a hash of the row when there is none), so a second import updates instead of duplicating.
Preview before commit
Nothing is written until the user has seen the summary: how many trades, which date range, which rows were rejected. Imports that can be undone are imports people trust.