TradesNotebook PWA
If you have ever tried to review trades right after a volatile session, you know the pain: spreadsheets scattered across tabs, screenshots buried in folders, and your notes split between apps. Then your internet drops, and the one moment you finally want to reflect is the one moment your tools stop cooperating.
This project solves that exact problem by treating TradesNotebook less like a static log and more like a personal performance system: fast, always available, and smart enough to sync when the network comes back.
The Problem (In Plain Language)
Most traders do not fail because they lack charts. They fail because they cannot consistently learn from their own behavior.
The hardest part is not placing trades. It is answering questions like:
- What setups actually make money for me?
- Which days of the week hurt my performance?
- Am I improving, or just staying busy?
Traditional journaling tools usually break down in one of three ways:
- They are too slow to use right after a trade.
- They depend too heavily on internet connectivity.
- They track numbers, but not context (notes, tags, screenshots, strategy details).
For an active trader, this is the difference between random repetition and deliberate improvement.
What This Project Does
At a high level, TradesNotebook is a trading notebook + analytics dashboard + backup system in one place.
Think of it like this:
- Local notebook first: every trade is saved immediately on your device.
- Cloud backup second: when online, data syncs to Supabase in the background.
- Coach mode on top: dashboard analytics convert raw trades into insight.
You can:
- Log trades with symbols, entry/exit data, position size, P&L, notes, tags, strategy, and screenshots.
- Filter and review trades quickly (including saved filter views).
- See performance metrics like win rate, profit factor, drawdown, and trend charts.
- Use guest mode, then later sign in and claim local data.
How It Works (Layered Explanation)
1) High-Level View (No Code)
The app follows a practical offline-first loop:
- User saves a trade.
- Trade is stored locally right away.
- UI updates instantly.
- If online and authenticated, trade syncs to cloud.
- If offline, the trade stays queued and syncs later.
This gives users the speed of local apps without sacrificing multi-device continuity.
2) Architecture Overview
The system is composed of four clear layers:
- Presentation layer: React pages/components for trade entry, list views, dashboard, and settings.
- Domain and validation layer: Zod schemas + normalization utilities to keep data clean.
- Local storage layer: Dexie over IndexedDB, used as the primary data store.
- Sync layer: Supabase integration and per-entity sync domains for trades, tags, strategies, and screenshots.
3) Data Flow in Practice
When a user creates a trade, the flow looks like this:
- Form data is validated and normalized.
- Trade is inserted into IndexedDB with metadata like
updated_atandlast_synced_at. - UI immediately reflects the new record.
- Sync service attempts cloud write (if online, not guest mode).
- Related entities (like tags and screenshots) are synced.
- Data invalidation event triggers refresh in dependent views.
That separation is important: fast local writes are never blocked by remote latency.
Code Deep Dive (For Developers)
Trade Input Is Normalized Before It Touches Storage
The app avoids dirty data by normalizing user input centrally, not ad hoc in UI components.
export function normalizeTradeForCreate(
input: Omit<Trade, 'id' | 'created_at' | 'updated_at'>
): Omit<Trade, 'id' | 'created_at' | 'updated_at'> {
const normalized = normalizeCommonFields({
...input,
status: input.status ?? deriveTradeStatus(input.exit_date),
pnl: input.pnl,
})
tradeMutationSchema.parse(normalized)
return {
...input,
...normalized,
status: normalized.status ?? deriveTradeStatus(normalized.exit_date),
pnl: normalized.pnl,
}
}Why this matters:
symbolis trimmed and uppercased consistently.- date inputs are normalized to ISO-style values.
- status is derived from exit-date behavior (open vs closed).
- one canonical path reduces validation drift across forms.
Sync Uses a Clear Conflict Rule: Prefer Unsynced Local Edits
The trade sync domain declares policy explicitly:
export const TRADE_SYNC_POLICY = {
remoteFetchPageSize: 200,
localUpsertBatchSize: 200,
maxRemotePages: 100,
conflictResolution: 'prefer-local-unsynced',
retryAttempts: 1
} as constAnd during reconciliation:
const hasUnsyncedLocalChanges = hasLocalChangesAfterLastSync(localUpdatedAt, lastSyncedAtTime)
if (hasUnsyncedLocalChanges) {
return
}
if (!Number.isFinite(localUpdatedAt) || remoteUpdatedAt > localUpdatedAt) {
chunkUpserts.push({ ...remoteTrade, tags: tradeTags })
}Why this is a strong default:
- it protects recent local edits from being overwritten by stale remote state,
- it favors user trust over strict remote authority,
- it keeps offline changes safe until explicitly synced.
Numeric Safety for Cloud Writes Is Handled Up Front
Supabase column constraints are respected before write calls:
const MAX_DECIMAL_15_6 = 999999999.999999
export const sanitizePnlForSupabase = (value: number | undefined): number | undefined => {
if (value === undefined) return undefined
if (!Number.isFinite(value)) return undefined
const rounded = Number(value.toFixed(6))
if (Math.abs(rounded) > MAX_DECIMAL_15_6) return undefined
return rounded
}This is a subtle but practical guardrail. A lot of apps only discover invalid numeric payloads after network errors.
Analytics Is Centralized and Cached
Instead of recalculating metrics in each dashboard component, analytics are computed once and distributed through context.
const analytics = useMemo(() => {
return computeDashboardAnalytics(filteredTrades, strategies.data, tags.data, startingBalance)
}, [filteredTrades, strategies.data, tags.data, startingBalance])There is also cache-aware daily series generation keyed by date range. This improves chart responsiveness and reduces repeated work on unchanged trade arrays.
Reactive Refresh Without Global State Libraries
A simple event-based invalidation bus keeps data views current:
export function invalidateData(domains: DataDomain[]): void {
const event = new CustomEvent(DATA_INVALIDATION_EVENT, {
detail: { domains: Array.from(new Set(domains)) }
})
window.dispatchEvent(event)
}This is lightweight and predictable for a medium-sized app where Redux-like overhead might be unnecessary.
Key Insights and Trade-offs
What Makes This Approach Interesting
- Offline-first as a product decision, not a technical afterthought.
- Guest-mode onboarding lowers friction for first-time users.
- Layered sync domains (
trade,tag,strategy,screenshot) keep responsibilities clean. - Keyboard-driven UX (command palette and shortcuts) makes frequent workflows faster.
Design Compromises
- Conflict strategy is intentionally simple (
prefer-local-unsynced) rather than fully collaborative conflict resolution. - Retry policy is conservative (
retryAttempts: 1) to avoid noisy sync loops, but can leave recovery opportunities on the table. - Analytics rely on entered P&L rather than broker/API import, which keeps scope manageable but increases dependency on accurate user input.
Practical Metrics Layer
The dashboard computes metrics traders care about:
- Win rate: (winning trades ÷ closed trades) × 100
- Profit factor: gross profit ÷ gross loss
- Max drawdown: peak-to-trough equity drop percentage over closed trades.
This is where journaling becomes decision support, not just record keeping.
Real-World Impact
This architecture is useful anywhere users need fast local writes + eventual cloud consistency.
Who benefits immediately:
- Retail and discretionary traders who need discipline and self-review.
- Trading coaches and communities that teach process and post-trade reflection.
- Teams in unstable network environments where offline reliability is mandatory.
Patterns worth borrowing for other domains:
- field service reporting,
- clinical notes,
- mobile inspections,
- logistics logs.
Any workflow where “capture now, sync later” beats “wait for internet” can reuse this model.
Assumptions I Made
Some intent is inferred from implementation patterns:
- The app is optimized for single-user journaling with personal cloud sync, not collaborative multi-user editing.
- P&L is manually entered and treated as source data for analytics.
- Sync priority is reliability and user control over local edits rather than strict server-authoritative merges.
Conclusion
This project shows that good trading software is less about flashy charts and more about trustworthy workflow design:
- capture quickly,
- stay usable offline,
- sync safely,
- turn history into insight.
For non-technical users, that means a journal that does not fail at the worst moment. For developers, it is a strong case study in local-first architecture with practical trade-offs.
Future Ideas
If this project continues evolving, the highest-leverage next steps are:
- Add robust retry/backoff queues and sync diagnostics in UI.
- Introduce test coverage for sync edge cases and conflict scenarios.
- Add optional broker/API import for auto-filled execution data.
- Add explicit conflict resolution UI for rare but important merge disputes.
- Add richer insight tooling (setup quality scoring, behavior tagging, regime analysis).
The core foundation is already strong: I built this app built around how real users behave under real-world constraints.