@zigrivers/scaffold 3.16.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (385) hide show
  1. package/README.md +28 -0
  2. package/content/knowledge/backend/backend-fintech-broker-integration.md +244 -0
  3. package/content/knowledge/backend/backend-fintech-compliance.md +181 -0
  4. package/content/knowledge/backend/backend-fintech-data-modeling.md +210 -0
  5. package/content/knowledge/backend/backend-fintech-ledger.md +226 -0
  6. package/content/knowledge/backend/backend-fintech-observability.md +151 -0
  7. package/content/knowledge/backend/backend-fintech-order-lifecycle.md +213 -0
  8. package/content/knowledge/backend/backend-fintech-risk-management.md +150 -0
  9. package/content/knowledge/backend/backend-fintech-testing.md +197 -0
  10. package/content/knowledge/core/automated-review-tooling.md +10 -0
  11. package/content/knowledge/core/multi-service-api-contracts.md +634 -0
  12. package/content/knowledge/core/multi-service-architecture.md +492 -0
  13. package/content/knowledge/core/multi-service-auth.md +706 -0
  14. package/content/knowledge/core/multi-service-data-ownership.md +539 -0
  15. package/content/knowledge/core/multi-service-observability.md +545 -0
  16. package/content/knowledge/core/multi-service-resilience.md +710 -0
  17. package/content/knowledge/core/multi-service-task-decomposition.md +615 -0
  18. package/content/knowledge/core/multi-service-testing.md +728 -0
  19. package/content/methodology/backend-fintech.yml +46 -0
  20. package/content/methodology/custom-defaults.yml +6 -0
  21. package/content/methodology/deep.yml +6 -0
  22. package/content/methodology/multi-service-overlay.yml +103 -0
  23. package/content/methodology/mvp.yml +6 -0
  24. package/content/pipeline/architecture/service-ownership-map.md +83 -0
  25. package/content/pipeline/quality/cross-service-auth.md +96 -0
  26. package/content/pipeline/quality/cross-service-observability.md +104 -0
  27. package/content/pipeline/quality/integration-test-plan.md +106 -0
  28. package/content/pipeline/specification/inter-service-contracts.md +95 -0
  29. package/dist/cli/commands/adopt.cli-flags.test.js +20 -0
  30. package/dist/cli/commands/adopt.cli-flags.test.js.map +1 -1
  31. package/dist/cli/commands/adopt.d.ts.map +1 -1
  32. package/dist/cli/commands/adopt.js +11 -3
  33. package/dist/cli/commands/adopt.js.map +1 -1
  34. package/dist/cli/commands/complete.d.ts +1 -0
  35. package/dist/cli/commands/complete.d.ts.map +1 -1
  36. package/dist/cli/commands/complete.js +26 -8
  37. package/dist/cli/commands/complete.js.map +1 -1
  38. package/dist/cli/commands/dashboard.d.ts +1 -0
  39. package/dist/cli/commands/dashboard.d.ts.map +1 -1
  40. package/dist/cli/commands/dashboard.js +19 -6
  41. package/dist/cli/commands/dashboard.js.map +1 -1
  42. package/dist/cli/commands/decisions.d.ts +1 -0
  43. package/dist/cli/commands/decisions.d.ts.map +1 -1
  44. package/dist/cli/commands/decisions.js +18 -4
  45. package/dist/cli/commands/decisions.js.map +1 -1
  46. package/dist/cli/commands/info.d.ts +1 -0
  47. package/dist/cli/commands/info.d.ts.map +1 -1
  48. package/dist/cli/commands/info.js +25 -3
  49. package/dist/cli/commands/info.js.map +1 -1
  50. package/dist/cli/commands/init-from.test.d.ts +2 -0
  51. package/dist/cli/commands/init-from.test.d.ts.map +1 -0
  52. package/dist/cli/commands/init-from.test.js +315 -0
  53. package/dist/cli/commands/init-from.test.js.map +1 -0
  54. package/dist/cli/commands/init.d.ts +3 -0
  55. package/dist/cli/commands/init.d.ts.map +1 -1
  56. package/dist/cli/commands/init.js +239 -129
  57. package/dist/cli/commands/init.js.map +1 -1
  58. package/dist/cli/commands/init.test.js +20 -0
  59. package/dist/cli/commands/init.test.js.map +1 -1
  60. package/dist/cli/commands/next.d.ts +1 -0
  61. package/dist/cli/commands/next.d.ts.map +1 -1
  62. package/dist/cli/commands/next.js +40 -4
  63. package/dist/cli/commands/next.js.map +1 -1
  64. package/dist/cli/commands/next.test.js +153 -0
  65. package/dist/cli/commands/next.test.js.map +1 -1
  66. package/dist/cli/commands/reset.d.ts +1 -0
  67. package/dist/cli/commands/reset.d.ts.map +1 -1
  68. package/dist/cli/commands/reset.js +77 -29
  69. package/dist/cli/commands/reset.js.map +1 -1
  70. package/dist/cli/commands/rework.d.ts +1 -0
  71. package/dist/cli/commands/rework.d.ts.map +1 -1
  72. package/dist/cli/commands/rework.js +16 -2
  73. package/dist/cli/commands/rework.js.map +1 -1
  74. package/dist/cli/commands/run.d.ts +1 -0
  75. package/dist/cli/commands/run.d.ts.map +1 -1
  76. package/dist/cli/commands/run.js +65 -13
  77. package/dist/cli/commands/run.js.map +1 -1
  78. package/dist/cli/commands/run.test.js +255 -3
  79. package/dist/cli/commands/run.test.js.map +1 -1
  80. package/dist/cli/commands/skip.d.ts +1 -0
  81. package/dist/cli/commands/skip.d.ts.map +1 -1
  82. package/dist/cli/commands/skip.js +24 -7
  83. package/dist/cli/commands/skip.js.map +1 -1
  84. package/dist/cli/commands/status.d.ts +1 -0
  85. package/dist/cli/commands/status.d.ts.map +1 -1
  86. package/dist/cli/commands/status.js +51 -4
  87. package/dist/cli/commands/status.js.map +1 -1
  88. package/dist/cli/commands/status.test.js +130 -0
  89. package/dist/cli/commands/status.test.js.map +1 -1
  90. package/dist/cli/guards-coverage.test.d.ts +2 -0
  91. package/dist/cli/guards-coverage.test.d.ts.map +1 -0
  92. package/dist/cli/guards-coverage.test.js +26 -0
  93. package/dist/cli/guards-coverage.test.js.map +1 -0
  94. package/dist/cli/guards-integration.test.d.ts +2 -0
  95. package/dist/cli/guards-integration.test.d.ts.map +1 -0
  96. package/dist/cli/guards-integration.test.js +178 -0
  97. package/dist/cli/guards-integration.test.js.map +1 -0
  98. package/dist/cli/guards.d.ts +13 -0
  99. package/dist/cli/guards.d.ts.map +1 -0
  100. package/dist/cli/guards.js +70 -0
  101. package/dist/cli/guards.js.map +1 -0
  102. package/dist/cli/guards.test.d.ts +2 -0
  103. package/dist/cli/guards.test.d.ts.map +1 -0
  104. package/dist/cli/guards.test.js +136 -0
  105. package/dist/cli/guards.test.js.map +1 -0
  106. package/dist/cli/init-flag-families.d.ts +1 -1
  107. package/dist/cli/init-flag-families.d.ts.map +1 -1
  108. package/dist/cli/init-flag-families.js +4 -1
  109. package/dist/cli/init-flag-families.js.map +1 -1
  110. package/dist/cli/init-flag-families.test.js +10 -0
  111. package/dist/cli/init-flag-families.test.js.map +1 -1
  112. package/dist/cli/shutdown.d.ts +2 -3
  113. package/dist/cli/shutdown.d.ts.map +1 -1
  114. package/dist/cli/shutdown.js +14 -11
  115. package/dist/cli/shutdown.js.map +1 -1
  116. package/dist/cli/shutdown.test.js +2 -4
  117. package/dist/cli/shutdown.test.js.map +1 -1
  118. package/dist/config/schema.d.ts +12122 -288
  119. package/dist/config/schema.d.ts.map +1 -1
  120. package/dist/config/schema.js +74 -79
  121. package/dist/config/schema.js.map +1 -1
  122. package/dist/config/schema.test.js +230 -1
  123. package/dist/config/schema.test.js.map +1 -1
  124. package/dist/config/validators/backend.d.ts +4 -0
  125. package/dist/config/validators/backend.d.ts.map +1 -0
  126. package/dist/config/validators/backend.js +14 -0
  127. package/dist/config/validators/backend.js.map +1 -0
  128. package/dist/config/validators/browser-extension.d.ts +4 -0
  129. package/dist/config/validators/browser-extension.d.ts.map +1 -0
  130. package/dist/config/validators/browser-extension.js +24 -0
  131. package/dist/config/validators/browser-extension.js.map +1 -0
  132. package/dist/config/validators/cli.d.ts +4 -0
  133. package/dist/config/validators/cli.d.ts.map +1 -0
  134. package/dist/config/validators/cli.js +14 -0
  135. package/dist/config/validators/cli.js.map +1 -0
  136. package/dist/config/validators/data-pipeline.d.ts +4 -0
  137. package/dist/config/validators/data-pipeline.d.ts.map +1 -0
  138. package/dist/config/validators/data-pipeline.js +14 -0
  139. package/dist/config/validators/data-pipeline.js.map +1 -0
  140. package/dist/config/validators/game.d.ts +4 -0
  141. package/dist/config/validators/game.d.ts.map +1 -0
  142. package/dist/config/validators/game.js +14 -0
  143. package/dist/config/validators/game.js.map +1 -0
  144. package/dist/config/validators/index.d.ts +7 -0
  145. package/dist/config/validators/index.d.ts.map +1 -0
  146. package/dist/config/validators/index.js +27 -0
  147. package/dist/config/validators/index.js.map +1 -0
  148. package/dist/config/validators/library.d.ts +4 -0
  149. package/dist/config/validators/library.d.ts.map +1 -0
  150. package/dist/config/validators/library.js +25 -0
  151. package/dist/config/validators/library.js.map +1 -0
  152. package/dist/config/validators/ml.d.ts +4 -0
  153. package/dist/config/validators/ml.d.ts.map +1 -0
  154. package/dist/config/validators/ml.js +31 -0
  155. package/dist/config/validators/ml.js.map +1 -0
  156. package/dist/config/validators/mobile-app.d.ts +4 -0
  157. package/dist/config/validators/mobile-app.d.ts.map +1 -0
  158. package/dist/config/validators/mobile-app.js +14 -0
  159. package/dist/config/validators/mobile-app.js.map +1 -0
  160. package/dist/config/validators/registry.test.d.ts +2 -0
  161. package/dist/config/validators/registry.test.d.ts.map +1 -0
  162. package/dist/config/validators/registry.test.js +26 -0
  163. package/dist/config/validators/registry.test.js.map +1 -0
  164. package/dist/config/validators/research.d.ts +4 -0
  165. package/dist/config/validators/research.d.ts.map +1 -0
  166. package/dist/config/validators/research.js +24 -0
  167. package/dist/config/validators/research.js.map +1 -0
  168. package/dist/config/validators/research.test.d.ts +2 -0
  169. package/dist/config/validators/research.test.d.ts.map +1 -0
  170. package/dist/config/validators/research.test.js +44 -0
  171. package/dist/config/validators/research.test.js.map +1 -0
  172. package/dist/config/validators/types.d.ts +19 -0
  173. package/dist/config/validators/types.d.ts.map +1 -0
  174. package/dist/config/validators/types.js +2 -0
  175. package/dist/config/validators/types.js.map +1 -0
  176. package/dist/config/validators/validators.test.d.ts +2 -0
  177. package/dist/config/validators/validators.test.d.ts.map +1 -0
  178. package/dist/config/validators/validators.test.js +25 -0
  179. package/dist/config/validators/validators.test.js.map +1 -0
  180. package/dist/config/validators/web-app.d.ts +4 -0
  181. package/dist/config/validators/web-app.d.ts.map +1 -0
  182. package/dist/config/validators/web-app.js +31 -0
  183. package/dist/config/validators/web-app.js.map +1 -0
  184. package/dist/core/assembly/context-gatherer.d.ts.map +1 -1
  185. package/dist/core/assembly/context-gatherer.js +4 -2
  186. package/dist/core/assembly/context-gatherer.js.map +1 -1
  187. package/dist/core/assembly/cross-reads.d.ts +61 -0
  188. package/dist/core/assembly/cross-reads.d.ts.map +1 -0
  189. package/dist/core/assembly/cross-reads.js +190 -0
  190. package/dist/core/assembly/cross-reads.js.map +1 -0
  191. package/dist/core/assembly/cross-reads.test.d.ts +2 -0
  192. package/dist/core/assembly/cross-reads.test.d.ts.map +1 -0
  193. package/dist/core/assembly/cross-reads.test.js +497 -0
  194. package/dist/core/assembly/cross-reads.test.js.map +1 -0
  195. package/dist/core/assembly/overlay-loader-structural.test.d.ts +2 -0
  196. package/dist/core/assembly/overlay-loader-structural.test.d.ts.map +1 -0
  197. package/dist/core/assembly/overlay-loader-structural.test.js +173 -0
  198. package/dist/core/assembly/overlay-loader-structural.test.js.map +1 -0
  199. package/dist/core/assembly/overlay-loader.d.ts +19 -3
  200. package/dist/core/assembly/overlay-loader.d.ts.map +1 -1
  201. package/dist/core/assembly/overlay-loader.js +135 -4
  202. package/dist/core/assembly/overlay-loader.js.map +1 -1
  203. package/dist/core/assembly/overlay-loader.test.js +204 -1
  204. package/dist/core/assembly/overlay-loader.test.js.map +1 -1
  205. package/dist/core/assembly/overlay-resolver.d.ts +9 -2
  206. package/dist/core/assembly/overlay-resolver.d.ts.map +1 -1
  207. package/dist/core/assembly/overlay-resolver.js +32 -1
  208. package/dist/core/assembly/overlay-resolver.js.map +1 -1
  209. package/dist/core/assembly/overlay-resolver.test.js +135 -17
  210. package/dist/core/assembly/overlay-resolver.test.js.map +1 -1
  211. package/dist/core/assembly/overlay-state-resolver.d.ts +9 -0
  212. package/dist/core/assembly/overlay-state-resolver.d.ts.map +1 -1
  213. package/dist/core/assembly/overlay-state-resolver.js +43 -2
  214. package/dist/core/assembly/overlay-state-resolver.js.map +1 -1
  215. package/dist/core/assembly/overlay-state-resolver.test.js +321 -0
  216. package/dist/core/assembly/overlay-state-resolver.test.js.map +1 -1
  217. package/dist/core/assembly/update-mode.d.ts +1 -0
  218. package/dist/core/assembly/update-mode.d.ts.map +1 -1
  219. package/dist/core/assembly/update-mode.js +17 -9
  220. package/dist/core/assembly/update-mode.js.map +1 -1
  221. package/dist/core/dependency/eligibility.d.ts +10 -1
  222. package/dist/core/dependency/eligibility.d.ts.map +1 -1
  223. package/dist/core/dependency/eligibility.js +19 -1
  224. package/dist/core/dependency/eligibility.js.map +1 -1
  225. package/dist/core/dependency/eligibility.test.js +82 -0
  226. package/dist/core/dependency/eligibility.test.js.map +1 -1
  227. package/dist/core/dependency/graph.d.ts +4 -1
  228. package/dist/core/dependency/graph.d.ts.map +1 -1
  229. package/dist/core/dependency/graph.js +7 -1
  230. package/dist/core/dependency/graph.js.map +1 -1
  231. package/dist/core/dependency/graph.test.js +48 -0
  232. package/dist/core/dependency/graph.test.js.map +1 -1
  233. package/dist/core/pipeline/global-steps.d.ts +7 -0
  234. package/dist/core/pipeline/global-steps.d.ts.map +1 -0
  235. package/dist/core/pipeline/global-steps.js +18 -0
  236. package/dist/core/pipeline/global-steps.js.map +1 -0
  237. package/dist/core/pipeline/resolver.d.ts +1 -0
  238. package/dist/core/pipeline/resolver.d.ts.map +1 -1
  239. package/dist/core/pipeline/resolver.js +54 -7
  240. package/dist/core/pipeline/resolver.js.map +1 -1
  241. package/dist/core/pipeline/resolver.test.js +51 -1
  242. package/dist/core/pipeline/resolver.test.js.map +1 -1
  243. package/dist/core/pipeline/types.d.ts +5 -1
  244. package/dist/core/pipeline/types.d.ts.map +1 -1
  245. package/dist/e2e/cross-service-references.test.d.ts +22 -0
  246. package/dist/e2e/cross-service-references.test.d.ts.map +1 -0
  247. package/dist/e2e/cross-service-references.test.js +230 -0
  248. package/dist/e2e/cross-service-references.test.js.map +1 -0
  249. package/dist/e2e/multi-service-pipeline.test.d.ts +10 -0
  250. package/dist/e2e/multi-service-pipeline.test.d.ts.map +1 -0
  251. package/dist/e2e/multi-service-pipeline.test.js +185 -0
  252. package/dist/e2e/multi-service-pipeline.test.js.map +1 -0
  253. package/dist/e2e/project-type-overlays.test.js +68 -0
  254. package/dist/e2e/project-type-overlays.test.js.map +1 -1
  255. package/dist/e2e/service-execution.test.d.ts +15 -0
  256. package/dist/e2e/service-execution.test.d.ts.map +1 -0
  257. package/dist/e2e/service-execution.test.js +219 -0
  258. package/dist/e2e/service-execution.test.js.map +1 -0
  259. package/dist/e2e/service-manifest.test.d.ts +19 -0
  260. package/dist/e2e/service-manifest.test.d.ts.map +1 -0
  261. package/dist/e2e/service-manifest.test.js +166 -0
  262. package/dist/e2e/service-manifest.test.js.map +1 -0
  263. package/dist/project/__frozen-schemas__/schema-v3.9.2.d.ts +224 -224
  264. package/dist/project/frontmatter.d.ts.map +1 -1
  265. package/dist/project/frontmatter.js +11 -0
  266. package/dist/project/frontmatter.js.map +1 -1
  267. package/dist/project/frontmatter.test.js +71 -0
  268. package/dist/project/frontmatter.test.js.map +1 -1
  269. package/dist/state/completion.d.ts +1 -1
  270. package/dist/state/completion.d.ts.map +1 -1
  271. package/dist/state/completion.js +10 -8
  272. package/dist/state/completion.js.map +1 -1
  273. package/dist/state/decision-logger.d.ts +3 -2
  274. package/dist/state/decision-logger.d.ts.map +1 -1
  275. package/dist/state/decision-logger.js +12 -11
  276. package/dist/state/decision-logger.js.map +1 -1
  277. package/dist/state/ensure-v3-migration.d.ts +9 -0
  278. package/dist/state/ensure-v3-migration.d.ts.map +1 -0
  279. package/dist/state/ensure-v3-migration.js +35 -0
  280. package/dist/state/ensure-v3-migration.js.map +1 -0
  281. package/dist/state/lock-manager.d.ts +5 -4
  282. package/dist/state/lock-manager.d.ts.map +1 -1
  283. package/dist/state/lock-manager.js +11 -11
  284. package/dist/state/lock-manager.js.map +1 -1
  285. package/dist/state/rework-manager.d.ts +1 -2
  286. package/dist/state/rework-manager.d.ts.map +1 -1
  287. package/dist/state/rework-manager.js +4 -5
  288. package/dist/state/rework-manager.js.map +1 -1
  289. package/dist/state/state-manager.d.ts +25 -1
  290. package/dist/state/state-manager.d.ts.map +1 -1
  291. package/dist/state/state-manager.js +86 -12
  292. package/dist/state/state-manager.js.map +1 -1
  293. package/dist/state/state-manager.test.js +278 -0
  294. package/dist/state/state-manager.test.js.map +1 -1
  295. package/dist/state/state-migration-v3.d.ts +22 -0
  296. package/dist/state/state-migration-v3.d.ts.map +1 -0
  297. package/dist/state/state-migration-v3.js +82 -0
  298. package/dist/state/state-migration-v3.js.map +1 -0
  299. package/dist/state/state-migration-v3.test.d.ts +2 -0
  300. package/dist/state/state-migration-v3.test.d.ts.map +1 -0
  301. package/dist/state/state-migration-v3.test.js +196 -0
  302. package/dist/state/state-migration-v3.test.js.map +1 -0
  303. package/dist/state/state-migration.d.ts.map +1 -1
  304. package/dist/state/state-migration.js +11 -6
  305. package/dist/state/state-migration.js.map +1 -1
  306. package/dist/state/state-migration.test.js +47 -2
  307. package/dist/state/state-migration.test.js.map +1 -1
  308. package/dist/state/state-path-resolver.d.ts +23 -0
  309. package/dist/state/state-path-resolver.d.ts.map +1 -0
  310. package/dist/state/state-path-resolver.js +36 -0
  311. package/dist/state/state-path-resolver.js.map +1 -0
  312. package/dist/state/state-path-resolver.test.d.ts +2 -0
  313. package/dist/state/state-path-resolver.test.d.ts.map +1 -0
  314. package/dist/state/state-path-resolver.test.js +78 -0
  315. package/dist/state/state-path-resolver.test.js.map +1 -0
  316. package/dist/state/state-version-dispatch.d.ts +17 -0
  317. package/dist/state/state-version-dispatch.d.ts.map +1 -0
  318. package/dist/state/state-version-dispatch.js +27 -0
  319. package/dist/state/state-version-dispatch.js.map +1 -0
  320. package/dist/state/state-version-dispatch.test.d.ts +2 -0
  321. package/dist/state/state-version-dispatch.test.d.ts.map +1 -0
  322. package/dist/state/state-version-dispatch.test.js +40 -0
  323. package/dist/state/state-version-dispatch.test.js.map +1 -0
  324. package/dist/types/config.d.ts +33 -3
  325. package/dist/types/config.d.ts.map +1 -1
  326. package/dist/types/config.test.js +62 -1
  327. package/dist/types/config.test.js.map +1 -1
  328. package/dist/types/dependency.d.ts +9 -0
  329. package/dist/types/dependency.d.ts.map +1 -1
  330. package/dist/types/frontmatter.d.ts +5 -0
  331. package/dist/types/frontmatter.d.ts.map +1 -1
  332. package/dist/types/lock.d.ts +1 -1
  333. package/dist/types/lock.d.ts.map +1 -1
  334. package/dist/types/state.d.ts +1 -1
  335. package/dist/types/state.d.ts.map +1 -1
  336. package/dist/utils/artifact-path.d.ts +19 -0
  337. package/dist/utils/artifact-path.d.ts.map +1 -0
  338. package/dist/utils/artifact-path.js +95 -0
  339. package/dist/utils/artifact-path.js.map +1 -0
  340. package/dist/utils/artifact-path.test.d.ts +2 -0
  341. package/dist/utils/artifact-path.test.d.ts.map +1 -0
  342. package/dist/utils/artifact-path.test.js +138 -0
  343. package/dist/utils/artifact-path.test.js.map +1 -0
  344. package/dist/utils/errors.d.ts +3 -1
  345. package/dist/utils/errors.d.ts.map +1 -1
  346. package/dist/utils/errors.js +21 -2
  347. package/dist/utils/errors.js.map +1 -1
  348. package/dist/utils/errors.test.js +27 -1
  349. package/dist/utils/errors.test.js.map +1 -1
  350. package/dist/utils/user-errors.d.ts +46 -0
  351. package/dist/utils/user-errors.d.ts.map +1 -0
  352. package/dist/utils/user-errors.js +76 -0
  353. package/dist/utils/user-errors.js.map +1 -0
  354. package/dist/utils/user-errors.test.d.ts +2 -0
  355. package/dist/utils/user-errors.test.d.ts.map +1 -0
  356. package/dist/utils/user-errors.test.js +74 -0
  357. package/dist/utils/user-errors.test.js.map +1 -0
  358. package/dist/validation/index.d.ts.map +1 -1
  359. package/dist/validation/index.js +16 -0
  360. package/dist/validation/index.js.map +1 -1
  361. package/dist/validation/index.test.js +48 -0
  362. package/dist/validation/index.test.js.map +1 -1
  363. package/dist/validation/state-validator.d.ts +5 -2
  364. package/dist/validation/state-validator.d.ts.map +1 -1
  365. package/dist/validation/state-validator.js +18 -20
  366. package/dist/validation/state-validator.js.map +1 -1
  367. package/dist/validation/state-validator.test.js +31 -2
  368. package/dist/validation/state-validator.test.js.map +1 -1
  369. package/dist/wizard/copy/backend.d.ts.map +1 -1
  370. package/dist/wizard/copy/backend.js +12 -0
  371. package/dist/wizard/copy/backend.js.map +1 -1
  372. package/dist/wizard/flags.d.ts +1 -0
  373. package/dist/wizard/flags.d.ts.map +1 -1
  374. package/dist/wizard/questions.d.ts.map +1 -1
  375. package/dist/wizard/questions.js +5 -1
  376. package/dist/wizard/questions.js.map +1 -1
  377. package/dist/wizard/questions.test.js +45 -2
  378. package/dist/wizard/questions.test.js.map +1 -1
  379. package/dist/wizard/wizard.d.ts +23 -0
  380. package/dist/wizard/wizard.d.ts.map +1 -1
  381. package/dist/wizard/wizard.js +85 -47
  382. package/dist/wizard/wizard.js.map +1 -1
  383. package/dist/wizard/wizard.test.js +186 -1
  384. package/dist/wizard/wizard.test.js.map +1 -1
  385. package/package.json +1 -1
@@ -0,0 +1,210 @@
1
+ ---
2
+ name: backend-fintech-data-modeling
3
+ description: Financial data models; currency handling; decimal precision; positions, trades, prices; time-series designs.
4
+ topics: [backend, fintech, data-modeling, decimal, currency, time-series, positions, trades]
5
+ ---
6
+
7
+ Financial data modeling is where most fintech bugs are born: a float creeps into a money field, a currency is implied instead of stored, a tick table grows unbounded, or a `current_position` column drifts from the journal. This doc covers the non-negotiable shapes of money, quantity, and price data, and the time-series and derived-view patterns that keep a trading or banking system honest at scale.
8
+
9
+ ## Summary
10
+
11
+ **Money is never a float.** Every stack has a correct type and it is never IEEE-754 binary floating point. Use `NUMERIC(precision, scale)` or integer minor units in Postgres; `Decimal` with an explicit context in Python; `BigDecimal` with `setScale` in Java; `decimal.js`, `big.js`, or `bignumber.js` in JavaScript/TypeScript (with `string` on the wire, not `number`); `rust_decimal` in Rust; `shopspring/decimal` in Go. A `double` anywhere in the money path is a latent bug, not a performance optimization.
12
+
13
+ **Currencies are not interchangeable.** Every money field is a `(amount, currency)` tuple, not a bare scalar. Store the currency as an ISO 4217 alpha-3 code (`USD`, `EUR`, `JPY`, `BTC`, `USDT`) alongside the amount, and reject any arithmetic that mixes currencies without an explicit FX conversion. Aggregations (`SUM(amount)`) across mixed currencies are always wrong — group by currency or convert at a snapshotted rate first.
14
+
15
+ **Quantities have their own precision, separate from price.** Equity shares are typically integers (fractional-share brokers use 6–9 decimals); Bitcoin quantities need 8 decimals (satoshis); Ethereum and most ERC-20 tokens need 18 decimals (wei); stablecoins like USDT and USDC use 6 decimals; FX quantities are usually 2 decimals for major pairs but pip size varies per pair. Store the instrument's quantity scale in the instrument master and validate every fill against it.
16
+
17
+ **Prices change constantly and the storage design must account for it.** Tick data (every quote, every trade) at scale means billions of rows per instrument per year — OLTP Postgres will not hold it. Use a columnar or purpose-built time-series store (TimescaleDB hypertables, ClickHouse with `ReplacingMergeTree`, InfluxDB, or Parquet-on-object-storage queried via DuckDB) with explicit retention policies. Bars (1-minute, 1-hour, 1-day OHLCV) live alongside ticks and are the common read path for charts and analytics.
18
+
19
+ **Positions are a derived view, not a primary table.** The authoritative record is the journal of fills (and, for cash, the ledger — see `backend-fintech-ledger.md`). A position is `SUM(signed_quantity) GROUP BY account, instrument`, optionally as of a point in time. Maintain a materialized view or cache for read performance, but always be able to rebuild it from the journal — any system that cannot do this has lost its audit story.
20
+
21
+ **Corporate actions require keeping both raw and adjusted prices.** Splits, dividends, and mergers retroactively change historical price series. Keep raw prices (what actually printed on the tape) immutable, and maintain a parallel `adjusted_price` column or adjustment-factor table so charting and backtesting get continuous series without losing the original record.
22
+
23
+ ## Deep Guidance
24
+
25
+ ### Decimal Types Across the Stack
26
+
27
+ Floats fail on money because `0.1 + 0.2 !== 0.3` in every IEEE-754 language, and because the error compounds across additions in a way you cannot bound. The canonical replacements:
28
+
29
+ - **Postgres**: `NUMERIC(precision, scale)` — e.g., `NUMERIC(20, 8)` for crypto amounts, `NUMERIC(18, 2)` for fiat. Arbitrary precision, exact arithmetic, slower than `BIGINT`. For high-throughput hot paths, store integer **minor units** (cents, satoshis, wei) in `BIGINT` or `NUMERIC(38, 0)` and keep the scale in the instrument master.
30
+ - **Python**: `decimal.Decimal` with `getcontext().prec = 28` and an explicit rounding mode set at application startup. Never mix `Decimal` and `float` in arithmetic — Python will not error, it will coerce and you will silently lose precision.
31
+ - **Java / Kotlin**: `BigDecimal`, always with `setScale(n, RoundingMode.HALF_EVEN)` or `HALF_UP` explicitly chosen per context. Never call `new BigDecimal(double)` — use `BigDecimal.valueOf(double)` or, better, pass strings.
32
+ - **JavaScript / TypeScript**: JavaScript `Number` is a `double`; `BigInt` handles integer minor units but not fractional amounts. Use `decimal.js`, `big.js`, or `bignumber.js` for fractional money; serialize as strings on the wire (`"123.45"`, never `123.45`). GraphQL and JSON Schema both support string-encoded decimals.
33
+ - **Rust**: `rust_decimal::Decimal` for fixed-scale, `num-bigint::BigInt` + scale for arbitrary precision.
34
+ - **Go**: `shopspring/decimal` is the de facto standard.
35
+
36
+ On the wire: JSON numbers are IEEE-754 doubles in most parsers, so serialize money as strings. Protobuf has no decimal type — use a message like `{ string value; int32 scale; string currency; }` or send the minor-unit integer with an out-of-band scale from the instrument master.
37
+
38
+ ### Currency Representation
39
+
40
+ Use ISO 4217 alpha-3 codes: `USD`, `EUR`, `JPY`, `CHF`, `GBP`, plus crypto conventions that extend the space: `BTC`, `ETH`, `USDT`, `USDC`, `SOL`. Keep a reference `currencies` table with `code`, `name`, `minor_unit_exponent` (2 for USD, 0 for JPY, 8 for BTC, 18 for ETH, 6 for USDT), `is_fiat`, `is_active`.
41
+
42
+ The "store money as cents" advice is only correct for fiat with 2 decimal places. JPY has no sub-unit (exponent 0); KWD has 3; BTC has 8; ETH has 18; USDT has 6. Hard-coding `*100` anywhere in the codebase is a bug waiting for a non-USD customer. Always read the scale from the currencies table or instrument master, or use a decimal type that carries scale explicitly.
43
+
44
+ ```sql
45
+ CREATE TABLE currencies (
46
+ code CHAR(3) PRIMARY KEY, -- or VARCHAR(10) for longer crypto tickers
47
+ name TEXT NOT NULL,
48
+ minor_unit_exponent SMALLINT NOT NULL, -- 2=USD, 0=JPY, 8=BTC, 18=ETH
49
+ is_fiat BOOLEAN NOT NULL,
50
+ is_active BOOLEAN NOT NULL DEFAULT true
51
+ );
52
+
53
+ CREATE TABLE cash_movements (
54
+ id BIGSERIAL PRIMARY KEY,
55
+ account_id UUID NOT NULL,
56
+ amount NUMERIC(38, 18) NOT NULL, -- scale wide enough for ETH
57
+ currency CHAR(3) NOT NULL REFERENCES currencies(code),
58
+ occurred_at TIMESTAMPTZ NOT NULL,
59
+ CHECK (amount <> 0)
60
+ );
61
+ ```
62
+
63
+ ### Rounding Rules
64
+
65
+ Rounding must be explicit, contextual, and documented. Defaults:
66
+
67
+ - **Banker's rounding (HALF_EVEN)** for settlement and regulatory reporting — statistically unbiased across many rounding events.
68
+ - **HALF_UP** for consumer-facing invoice totals where "round half away from zero" matches user expectations.
69
+ - **Truncation (ROUND_DOWN)** for display of quantities where you must not overstate (e.g., a balance-available readout must never round up).
70
+ - **HALF_UP or ceiling** for fees charged to the customer in your favor — so you do not under-collect — with regulatory review.
71
+
72
+ The footgun case: `1.005` rendered to 2 decimals. In binary float it is `1.00499999...`, which HALF_UP rounds to `1.00`, not `1.01`. Use a decimal library and explicit context, and write a test for exactly this case.
73
+
74
+ ### Multi-Currency Positions
75
+
76
+ Two patterns, pick one and be explicit:
77
+
78
+ 1. **Native-currency storage + periodic revaluation.** Each position and each cash balance is stored in its native currency. At report time, convert to the reporting currency using a snapshotted FX rate (end-of-day close, or an intraday snapshot for intraday P&L). Preserves accuracy and audit trail, simplifies settlement.
79
+ 2. **Functional-currency storage + memo native amounts.** Everything is denominated in the reporting currency (e.g., USD); foreign-currency fills are converted at execution time. Simpler to aggregate but loses information — revaluing an FX exposure later requires the memo column.
80
+
81
+ For broker/exchange integration, pattern 1 is nearly always correct. For internal accounting that feeds a single GAAP/IFRS reporting currency, pattern 2 is common. See `backend-fintech-ledger.md` for the journal-entry mechanics of FX conversion.
82
+
83
+ ### Time-Series Storage: Ticks, Bars, and Retention
84
+
85
+ Decide early what granularity you need to keep, and for how long. A naive "store every tick forever in Postgres" plan breaks at the first liquid instrument. Options:
86
+
87
+ - **TimescaleDB hypertables** — Postgres extension, partitions by time transparently, supports continuous aggregates (materialized bars), native compression (10–20x) and retention policies. Good choice when you already have Postgres expertise.
88
+ - **ClickHouse `ReplacingMergeTree` / `AggregatingMergeTree`** — columnar, extreme compression, fast time-range scans, scales to trillions of rows. Best for high-volume tick capture and analytics.
89
+ - **InfluxDB** — time-series native, good for metrics-shaped data, less common for financial tick capture.
90
+ - **Parquet on object storage + DuckDB / Athena / BigQuery** — effectively free cold storage, pay per query; excellent for long-term archive and backtesting over years of history.
91
+
92
+ Tick schema: `(instrument_id, exchange_id, ts, price, size, side, seq)`. Always store the exchange timestamp *and* your ingest timestamp — clock skew matters. Bar schema: `(instrument_id, resolution, open_ts, open, high, low, close, volume, vwap, tick_count)`.
93
+
94
+ ```sql
95
+ -- TimescaleDB: hypertable + retention + continuous aggregate
96
+ CREATE TABLE ticks (
97
+ instrument_id BIGINT NOT NULL,
98
+ exchange_id SMALLINT NOT NULL,
99
+ ts TIMESTAMPTZ NOT NULL,
100
+ price NUMERIC(20, 8) NOT NULL,
101
+ size NUMERIC(28, 8) NOT NULL,
102
+ side CHAR(1) NOT NULL CHECK (side IN ('B','S','U')),
103
+ seq BIGINT NOT NULL
104
+ );
105
+ SELECT create_hypertable('ticks', 'ts', chunk_time_interval => INTERVAL '1 day');
106
+
107
+ -- Keep raw ticks for 30 days, then drop; bars are retained separately
108
+ SELECT add_retention_policy('ticks', INTERVAL '30 days');
109
+
110
+ -- 1-minute bars as a continuous aggregate, retained 10 years
111
+ CREATE MATERIALIZED VIEW bars_1m
112
+ WITH (timescaledb.continuous) AS
113
+ SELECT instrument_id,
114
+ time_bucket('1 minute', ts) AS bucket,
115
+ first(price, ts) AS open,
116
+ max(price) AS high,
117
+ min(price) AS low,
118
+ last(price, ts) AS close,
119
+ sum(size) AS volume
120
+ FROM ticks
121
+ GROUP BY instrument_id, bucket;
122
+ SELECT add_continuous_aggregate_policy('bars_1m',
123
+ start_offset => INTERVAL '2 days',
124
+ end_offset => INTERVAL '1 minute',
125
+ schedule_interval => INTERVAL '1 minute');
126
+ ```
127
+
128
+ ### Corporate Actions and Price Adjustment
129
+
130
+ A 2:1 stock split doubles the share count and halves the price. Historical charts must be adjusted so the price series is continuous, but *settlement* still used the unadjusted prices. Keep both:
131
+
132
+ - `prices_raw(instrument_id, ts, price)` — immutable, what actually printed.
133
+ - `corporate_actions(instrument_id, effective_date, action_type, ratio_numerator, ratio_denominator, cash_amount, currency)` — authoritative record of splits, dividends, mergers.
134
+ - `adjusted_price` — either a materialized column computed from raw + the product of all adjustment factors effective after the tick date, or computed on read via a view.
135
+
136
+ Never overwrite the raw price. If a data vendor re-emits history post-split and you overwrite, you cannot reconcile past trade tickets. The same principle applies to reference data generally: store the vendor's snapshot with a `received_at`, and derive any "current" projection.
137
+
138
+ ### Position Model
139
+
140
+ A position is `SUM(signed_quantity) GROUP BY (account_id, instrument_id)` over the fills journal, where `signed_quantity` is positive for buys and negative for sells (and the reverse for shorts). Cost basis is `SUM(signed_quantity * price) / SUM(signed_quantity)` under average-cost, or a FIFO lot walk for tax-lot accounting.
141
+
142
+ Implementation options:
143
+
144
+ - **Read-time aggregation** — correct and simple, fine up to ~10M fills per account; use an index on `(account_id, instrument_id, executed_at)`.
145
+ - **Materialized view, refreshed nightly** — for dashboards; accept T-1 staleness in exchange for cheap reads.
146
+ - **Rolling snapshot table** — `positions(account_id, instrument_id, as_of_ts, quantity, avg_cost)` updated by trigger on fill insert. Fastest reads, most complex invariants; regenerate from the fills journal on any suspicion of drift.
147
+ - **Event-sourced with periodic checkpoints** — for point-in-time queries ("what was my position at 14:32:17 on 2026-03-14?"), keep fills + periodic snapshots and replay from nearest snapshot.
148
+
149
+ Whichever you pick, maintain a `rebuild_positions(account_id)` job that recomputes from the fills journal; run it nightly against a sample of accounts and alarm on any drift.
150
+
151
+ ### Trade Identifiers: Internal, Broker, Client
152
+
153
+ Every executed trade has at least three identifiers, and you should store all of them:
154
+
155
+ - **Internal trade id (UUID)** — your primary key; generated at ingest; never changes; used in all downstream references (ledger `external_id`, position rebuilds, tax lots).
156
+ - **Broker execution id** — the exchange or broker's identifier for the fill. Used for reconciliation against the broker's clearing feed (see `backend-fintech-ledger.md` reconciliation). May arrive after the ack — treat as nullable initially and backfill.
157
+ - **Client order id (`clOrdID`)** — generated by your order-management system before placing the order. Round-trips through the broker; used to match acks and fills back to the originating intent. Must be globally unique per client-session per the FIX spec (and per broker rules — some require monotonically increasing).
158
+
159
+ Store venue, symbol (broker's symbology *and* your canonical instrument id), side, quantity, price, fees in their own currency, liquidity flag (maker/taker), and timestamps (client-submitted, broker-ack, exchange-executed, ingest). See `backend-fintech-order-lifecycle.md` for the state-machine that connects these.
160
+
161
+ ### Cross-Currency Arithmetic: Reject at the Boundary
162
+
163
+ The single highest-leverage fintech habit: refuse to add two money values in different currencies. Write it as a typed function; make the compiler or runtime enforce it.
164
+
165
+ ```typescript
166
+ import Decimal from 'decimal.js';
167
+
168
+ type Money = { amount: Decimal; currency: string };
169
+
170
+ function add(a: Money, b: Money): Money {
171
+ if (a.currency !== b.currency) {
172
+ throw new Error(
173
+ `cannot add ${a.currency} and ${b.currency} without explicit conversion`,
174
+ );
175
+ }
176
+ return { amount: a.amount.plus(b.amount), currency: a.currency };
177
+ }
178
+
179
+ function convert(m: Money, toCurrency: string, rate: Decimal, rateAsOf: Date): Money {
180
+ // rate is units of toCurrency per unit of m.currency, snapshotted at rateAsOf
181
+ return { amount: m.amount.times(rate), currency: toCurrency };
182
+ }
183
+
184
+ // Aggregations over mixed currencies must group explicitly
185
+ function sumByCurrency(movements: Money[]): Map<string, Decimal> {
186
+ const totals = new Map<string, Decimal>();
187
+ for (const m of movements) {
188
+ const prev = totals.get(m.currency) ?? new Decimal(0);
189
+ totals.set(m.currency, prev.plus(m.amount));
190
+ }
191
+ return totals;
192
+ }
193
+ ```
194
+
195
+ In Python, subclass `Decimal` or wrap in a `Money` dataclass with `__add__` raising on currency mismatch; same pattern in Kotlin with a `Money` value class. Every SQL aggregation over money must `GROUP BY currency` or restrict to a single currency in the `WHERE` clause.
196
+
197
+ ### Common Pitfalls
198
+
199
+ - **`0.1 + 0.2`-class bugs in money math.** JavaScript `Number`, Python `float`, `double` in JVM/.NET, `REAL`/`DOUBLE PRECISION` in Postgres — all unsafe for money. Use decimal types end to end, including the JSON wire format (strings, not numbers).
200
+ - **Banker's rounding vs HALF_UP confusion.** `Decimal('0.5').quantize(Decimal('1'))` in Python rounds to 0 by default (banker's), not 1. Explicitly pass the rounding mode matching the business rule per call site.
201
+ - **Equality comparison on floats or even decimals.** `amount == 0` is fine on integer minor units or exact decimals; `amount < epsilon` is a code smell that usually means a float slipped in. Money is exact.
202
+ - **Mixing currencies in `SUM()`.** Running `SELECT SUM(amount) FROM cash_movements` without grouping by currency yields meaningless numbers; forbid it at the query-review layer.
203
+ - **Hardcoding `*100` for cents conversion.** Works for USD, breaks for JPY (no sub-unit), BTC (8 decimals), ETH (18), USDT (6). Always read the minor-unit exponent from the currencies table.
204
+ - **Unbounded tick retention in OLTP.** Every liquid instrument floods the table. Pick a columnar or TSDB backend, set a retention policy, materialize bars, and move old raw ticks to cold storage.
205
+ - **Overwriting raw prices after corporate actions.** Split-adjust historical bars at read time, not by mutating the raw series. Keep the corporate-actions table as the authoritative record.
206
+ - **`current_position` columns in user tables.** Drift from the fills journal, get clobbered by races, silently wrong. Derive, materialize, and nightly-reconcile.
207
+ - **Mixing transaction time and system time.** Exchange timestamp, broker ack timestamp, and your ingest timestamp are three different clocks. Store all three on fills and ticks; alarm on skew above threshold.
208
+ - **Implicit currency from account context.** "This account is a USD account, so amounts are in USD" — until it isn't, or until a crypto product ships. Every money field carries its currency.
209
+
210
+ See also `backend-fintech-ledger.md` for double-entry posting over these same primitives, `backend-fintech-testing.md` for property-based money-math tests and time-series fixture patterns, and `backend-fintech-compliance.md` for the audit-trail and retention constraints that shape historical data storage.
@@ -0,0 +1,226 @@
1
+ ---
2
+ name: backend-fintech-ledger
3
+ description: Double-entry accounting for fintech ledgers; journal vs ledger tables; idempotent posting; reconciliation patterns; balance invariants.
4
+ topics: [backend, fintech, ledger, double-entry, accounting, reconciliation, idempotency, invariants]
5
+ ---
6
+
7
+ A fintech ledger is the authoritative record of money movement; if it is wrong, nothing else in the system can be trusted. The discipline is borrowed intact from 700 years of double-entry bookkeeping — not a new invention, and not negotiable. This doc covers the invariants, schema shape, idempotent posting mechanics, and reconciliation patterns that keep a ledger survivable at production scale.
8
+
9
+ ## Summary
10
+
11
+ The single most important invariant in any fintech system: **for every credit there is an equal-amount debit, and the sum of debits across a journal entry equals the sum of credits**. This is double-entry accounting. Enforce it in the database, not just the application — a balanced journal entry is the atomic unit of money movement. A single journal entry may have two lines (simple transfer) or dozens (payroll run, fee split, multi-leg trade settlement), but the sum-to-zero constraint holds for every entry.
12
+
13
+ You **cannot** derive balances from `current_balance` columns updated in application tables. Those columns drift, get clobbered by race conditions, and are unauditable. The balance of an account at any point in time is defined as the sum of all posted ledger lines against that account up to that instant. The journal is the system of record; balances are a projection.
14
+
15
+ The canonical schema is three layers: **journal** (immutable events — one row per business event, with an external idempotency key and a posting timestamp), **ledger lines** (the double-entry rows — two or more per journal entry, each referencing an account and signed amount), and **account balances** (either a materialized view refreshed periodically, a rolling aggregation maintained by trigger, or computed on demand for low-volume accounts). Write-once to journal and ledger-lines; balances are derived and can always be rebuilt from the journal.
16
+
17
+ Every journal insert MUST carry an external idempotency key — usually a UUID derived from the upstream event (webhook event ID, Stripe payment intent ID, broker execution ID, client-supplied `Idempotency-Key` header). Retries are a fact of life: payment processors retry webhooks, queues redeliver, operators click twice. The key is enforced by a unique index on `(source, external_id)` so the second attempt fails cleanly and the caller receives the original posting. Libraries like `ledgers.db` (Mercury's pattern), `tigerbeetle`, Square's `subzero`, and `medici` (Node) codify this. Most fintech outages can be traced to a missing or mis-scoped idempotency key.
18
+
19
+ Reconciliation is a daily, non-optional process. For each counterparty (bank, broker, card processor, payment rail), the ledger's expected cash movement is matched against the counterparty's settlement file or API feed. Matched items clear; unmatched items go into a "breaks queue" with aging (1 day, 3 days, 7 days, escalate). A break is either a timing difference that will resolve on the next feed, a fee the counterparty applied that you didn't book, or a bug. Break quarantine prevents a single unmatched item from blocking close; root cause must be found and booked before period close. See `backend-fintech-compliance.md` for audit-trail requirements and `backend-fintech-order-lifecycle.md` for trade-specific settlement flows.
20
+
21
+ ## Deep Guidance
22
+
23
+ ### Chart of Accounts Design
24
+
25
+ The chart of accounts (COA) is the taxonomy every ledger line must pick from. Classic classes: **assets** (cash, receivables, inventory), **liabilities** (customer deposits, payables), **equity** (retained earnings, owner's capital), **revenue** (fees, interest earned), **expenses** (processing fees, bad debt). Customer deposits in a consumer fintech are a *liability* on your balance sheet — the money belongs to the customer, you're holding it.
26
+
27
+ Granularity matters more than people expect. Per-customer wallet balances require a distinct account per customer (or per customer-currency pair) — millions of rows of accounts is fine, this is what modern ledgers are designed for. Operational accounts (cash-at-bank, processing-fee-expense, interchange-revenue) are few and coarse. A typical pattern: customer accounts identified by `customer_id`, operational accounts identified by a stable account code (`1000-CASH-USD`, `4000-FEE-REVENUE`, `5000-PROCESSOR-COST`).
28
+
29
+ Avoid "merge" accounts that combine classes — "customer cash and fees" is wrong; split them. Every account has exactly one normal balance (debit for assets/expenses, credit for liabilities/equity/revenue) and one class.
30
+
31
+ ```sql
32
+ CREATE TABLE accounts (
33
+ id UUID PRIMARY KEY,
34
+ code TEXT UNIQUE, -- '1000-CASH-USD' or NULL for customer accounts
35
+ name TEXT NOT NULL,
36
+ class TEXT NOT NULL CHECK (class IN ('asset','liability','equity','revenue','expense')),
37
+ normal_side TEXT NOT NULL CHECK (normal_side IN ('debit','credit')),
38
+ currency CHAR(3) NOT NULL, -- ISO 4217
39
+ customer_id UUID REFERENCES customers(id),
40
+ is_active BOOLEAN NOT NULL DEFAULT true,
41
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
42
+ );
43
+ ```
44
+
45
+ ### Journal Entry and Ledger Line Structure
46
+
47
+ A journal entry represents one business event. Ledger lines represent the debits and credits that settle that event. Every ledger line belongs to exactly one journal entry. Every journal entry must sum to zero per currency.
48
+
49
+ ```sql
50
+ CREATE TABLE journal_entries (
51
+ id UUID PRIMARY KEY,
52
+ source TEXT NOT NULL, -- 'stripe' | 'broker' | 'manual' | 'internal'
53
+ external_id TEXT NOT NULL, -- idempotency key (e.g. Stripe event id)
54
+ counterparty_id UUID REFERENCES counterparties(id),
55
+ memo TEXT,
56
+ transaction_date DATE NOT NULL, -- when economically occurred
57
+ posting_date DATE NOT NULL, -- when booked to the ledger
58
+ posted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
59
+ posted_by UUID NOT NULL, -- actor id
60
+ UNIQUE (source, external_id)
61
+ );
62
+
63
+ CREATE TABLE ledger_lines (
64
+ id BIGSERIAL PRIMARY KEY,
65
+ journal_entry_id UUID NOT NULL REFERENCES journal_entries(id),
66
+ account_id UUID NOT NULL REFERENCES accounts(id),
67
+ direction TEXT NOT NULL CHECK (direction IN ('debit','credit')),
68
+ amount_minor_units BIGINT NOT NULL CHECK (amount_minor_units > 0),
69
+ currency CHAR(3) NOT NULL,
70
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
71
+ );
72
+
73
+ CREATE INDEX ON ledger_lines (account_id, created_at);
74
+ CREATE INDEX ON ledger_lines (journal_entry_id);
75
+ ```
76
+
77
+ Distinguish **transaction date** (when the economic event occurred — the customer's card was charged at 23:59 Dec 31) from **posting date** (when you booked it — Jan 2, after the batch settled). Financial statements group by posting date; period-end cutoffs use transaction date for accruals. Both are needed.
78
+
79
+ ### Double-Entry Invariants in the Database
80
+
81
+ The sum-to-zero rule must be enforced by the database, not just by application code. Application bugs happen; a CHECK constraint or deferred trigger does not regress silently.
82
+
83
+ ```sql
84
+ -- Deferred constraint: check at end of transaction, after all lines inserted
85
+ CREATE OR REPLACE FUNCTION assert_journal_balanced()
86
+ RETURNS trigger LANGUAGE plpgsql AS $$
87
+ DECLARE imbalance RECORD;
88
+ BEGIN
89
+ FOR imbalance IN
90
+ SELECT currency,
91
+ SUM(CASE WHEN direction = 'debit' THEN amount_minor_units ELSE 0 END)
92
+ - SUM(CASE WHEN direction = 'credit' THEN amount_minor_units ELSE 0 END) AS delta
93
+ FROM ledger_lines
94
+ WHERE journal_entry_id = NEW.journal_entry_id
95
+ GROUP BY currency
96
+ HAVING SUM(CASE WHEN direction='debit' THEN amount_minor_units ELSE 0 END)
97
+ <> SUM(CASE WHEN direction='credit' THEN amount_minor_units ELSE 0 END)
98
+ LOOP
99
+ RAISE EXCEPTION 'journal entry % unbalanced in %: delta=%',
100
+ NEW.journal_entry_id, imbalance.currency, imbalance.delta;
101
+ END LOOP;
102
+ RETURN NEW;
103
+ END;
104
+ $$;
105
+
106
+ CREATE CONSTRAINT TRIGGER journal_balance_check
107
+ AFTER INSERT ON ledger_lines
108
+ DEFERRABLE INITIALLY DEFERRED
109
+ FOR EACH ROW EXECUTE FUNCTION assert_journal_balanced();
110
+ ```
111
+
112
+ Also block UPDATE and DELETE on `ledger_lines` and `journal_entries` via triggers — corrections are *reversing* journal entries, not edits. This is the same append-only posture described in `backend-fintech-compliance.md`.
113
+
114
+ ### Idempotent Posting
115
+
116
+ Every write path into the ledger goes through a single posting function. The function takes a journal-entry spec plus an idempotency key; on conflict it returns the existing journal-entry id without side effects.
117
+
118
+ ```typescript
119
+ type PostingLine = {
120
+ accountId: string;
121
+ direction: 'debit' | 'credit';
122
+ amountMinorUnits: bigint;
123
+ currency: string;
124
+ };
125
+
126
+ type PostingRequest = {
127
+ source: string; // 'stripe' | 'broker' | ...
128
+ externalId: string; // caller-supplied idempotency key
129
+ transactionDate: string; // YYYY-MM-DD
130
+ postingDate: string;
131
+ counterpartyId?: string;
132
+ memo?: string;
133
+ lines: PostingLine[]; // must sum to zero per currency, length >= 2
134
+ postedBy: string;
135
+ };
136
+
137
+ // Returns existing journal_entry_id on duplicate (source, externalId) without
138
+ // reinserting lines. Raises on imbalanced entry, unknown account, inactive
139
+ // account, negative amounts, or currency mismatch between line and account.
140
+ async function postJournalEntry(req: PostingRequest): Promise<string>;
141
+ ```
142
+
143
+ Implementation notes: start a transaction, `INSERT ... ON CONFLICT (source, external_id) DO NOTHING RETURNING id` on `journal_entries`; if nothing was returned, `SELECT id` the existing row and short-circuit. Otherwise insert all ledger lines in the same transaction. The deferred constraint trigger fires on commit.
144
+
145
+ For admin corrections, *require* an idempotency key — it is the common audit-gap in fintech. Admins produce a key like `correction-2026-04-14-ticket-12345` or the UUID of a ticketing-system record.
146
+
147
+ ### Balance Queries and Materialization
148
+
149
+ Balances are derived. The simplest correct query:
150
+
151
+ ```sql
152
+ -- Balance of an account at a point in time (pseudo-SQL)
153
+ SELECT account_id,
154
+ currency,
155
+ COALESCE(SUM(CASE WHEN direction = 'debit' THEN amount_minor_units ELSE 0 END)
156
+ - SUM(CASE WHEN direction = 'credit' THEN amount_minor_units ELSE 0 END), 0)
157
+ AS debit_minus_credit_minor_units
158
+ FROM ledger_lines
159
+ WHERE account_id = $1
160
+ AND created_at <= $2 -- 'as-of' timestamp
161
+ GROUP BY account_id, currency;
162
+ ```
163
+
164
+ Interpret the sign by the account's `normal_side`: for an asset account the balance is debits minus credits; for a liability the balance is credits minus debits. Always compute in minor units (cents, satoshis) and format for display only — never use floating point. Use `BIGINT` in Postgres, `bigint` / `BigInt` in TS, `decimal.Decimal` in Python, never `double`.
165
+
166
+ For high-volume accounts (customer wallets), maintain rolling balances via a trigger or via TigerBeetle's built-in running balances. Refresh materialized views nightly for reporting; read-time aggregation is fine up to roughly 10M lines per account.
167
+
168
+ ### Multi-Currency and FX
169
+
170
+ A multi-currency ledger must either (a) denominate every account in a single currency and use separate FX accounts for conversions, or (b) denominate lines in their native currency and revalue to a reporting currency at close. Mixing currencies within a single account is an anti-pattern.
171
+
172
+ Every FX conversion is a three-leg journal entry: debit source-currency account, credit source-currency FX clearing, debit target-currency FX clearing, credit target-currency account — with the rate snapshot stored alongside the entry. Snapshot the rate at booking (use a feed like OpenExchangeRates, ECB, or Fixer with a timestamp); do not re-read it at settlement, or you introduce drift. At period end, revalue open foreign-currency balances to the reporting currency using the closing rate and book the realized/unrealized FX gain or loss as a separate journal entry.
173
+
174
+ Precision: never `double`. Minor units (ints) for fiat. For crypto, use a fixed-scale decimal library (`decimal.js`, Go `shopspring/decimal`, Python `decimal.Decimal`) and store the scale explicitly; satoshi-level BIGINTs work for Bitcoin but not for many Ethereum tokens.
175
+
176
+ ### Reconciliation Patterns
177
+
178
+ Reconciliation matches internal ledger entries against external sources of truth: bank statements (BAI2, MT940, CAMT.053), card processor settlement files (Visa Base II, Stripe balance transactions), broker clearing files (DTCC CNS). For each counterparty, on each business day, every external line must match an internal journal entry (or vice versa).
179
+
180
+ Event sourcing from counterparty feeds works best: ingest the feed into a `counterparty_events` table, then run a match job that joins against journal entries via a natural key (broker execution id, card network auth code, ACH trace number). Unmatched rows on either side go to a **breaks queue**.
181
+
182
+ ```sql
183
+ -- Unmatched broker executions (external) with no matching journal entry (internal)
184
+ SELECT bx.execution_id,
185
+ bx.trade_date,
186
+ bx.symbol,
187
+ bx.amount_minor_units,
188
+ bx.received_at
189
+ FROM broker_executions bx
190
+ LEFT JOIN journal_entries je
191
+ ON je.source = 'broker'
192
+ AND je.external_id = bx.execution_id
193
+ WHERE je.id IS NULL
194
+ AND bx.trade_date >= current_date - INTERVAL '7 days'
195
+ ORDER BY bx.trade_date, bx.received_at;
196
+ ```
197
+
198
+ Break items age with SLAs: T+1 auto-retry, T+3 operator review, T+7 escalation. Auto-matching should use counterparty id plus amount plus date within a small tolerance; never match on amount alone. Manual-review UI exposes side-by-side diffs and lets an operator post a reversing or adjusting entry — with mandatory idempotency key and reason code.
199
+
200
+ ### Period-Close
201
+
202
+ At month-end, quarter-end, and year-end, the books are **closed**: a cutoff timestamp is recorded, no journal entries with a posting date on or before the cutoff may be inserted, and an opening balance sheet is materialized for the next period. Late-arriving events (a broker settlement that arrives 3 days after trade date) post to the *next* period with the prior period's transaction date — so accruals remain correct but the closed period's reported balances do not move.
203
+
204
+ Implementation: a `periods` table with `status IN ('open','closing','closed')`; a trigger on `journal_entries` that rejects inserts into closed periods; a nightly job that materializes the opening balance sheet into an `account_period_balances` table. Auditors will ask for the close runbook; keep it in-repo alongside the code.
205
+
206
+ ### Performance, Partitioning, Archiving
207
+
208
+ Journal and ledger-line tables grow forever — a mid-size consumer fintech posts tens of millions of lines per year. Strategies:
209
+
210
+ - **Partition by posting month** (native Postgres declarative partitioning, or `pg_partman`). Old partitions become read-mostly and can be moved to cheaper storage.
211
+ - **Archive closed periods** to columnar storage (ClickHouse, Snowflake, S3+Parquet) for analytics; keep a queryable summary in the primary DB.
212
+ - **Index carefully:** `(account_id, created_at)` is the hot path for balance queries; `(journal_entry_id)` for entry lookup; `(source, external_id)` unique index for idempotency. Do not over-index — every index costs on every insert and the write path is hot.
213
+ - **Purpose-built ledger DBs:** TigerBeetle is designed specifically for double-entry workloads with built-in idempotency and running balances; consider it for greenfield high-volume systems.
214
+
215
+ ### Common Pitfalls
216
+
217
+ - **Negative balances silently accepted.** A wallet debit that overdraws should either be rejected at the posting function or post to an overdraft-receivable account — never silently go negative in a customer wallet. Enforce with a CHECK on the rolling balance trigger or pre-flight check in the posting function.
218
+ - **FX rate drift between booking and settlement.** If you read the FX rate at posting time and again at settlement, small differences accumulate. Snapshot once at booking and book any settlement-day delta as a realized FX gain/loss entry.
219
+ - **Double-posting from webhook retries.** Stripe and similar providers retry webhooks aggressively. Without a unique index on `(source, external_id)`, the second delivery books a duplicate journal entry. Always key on the provider event id, never on your own generated UUID at receive time.
220
+ - **Missing idempotency keys on manual admin corrections.** A support engineer clicks "post adjustment" twice and double-credits the customer. The admin UI must require and persist an idempotency key on every posting action.
221
+ - **`current_balance` columns in application tables.** These drift, lie about history, and cannot be audited. The ledger is the source; balances are derived. If a dashboard needs a fast balance read, materialize it from the ledger, not alongside it.
222
+ - **Using floats anywhere in the money path.** JavaScript `Number`, Python `float`, Java `double` — all unsafe. Minor-unit integers or fixed-scale decimals only, end to end, including JSON wire formats (send as strings).
223
+ - **Mixing posting-date and transaction-date reports.** A balance-sheet-as-of report uses posting date; an accrual-based P&L uses transaction date. Label every report and use the right column.
224
+ - **Correcting entries by UPDATE.** Never. A correction is a *reversing* journal entry plus a corrected entry, both with idempotency keys and memos linking to the original.
225
+
226
+ See also `backend-fintech-data-modeling.md` for the broader schema-design patterns, `backend-fintech-compliance.md` for audit-trail and retention requirements, and `backend-fintech-order-lifecycle.md` for trade-settlement flows that terminate in ledger postings.
@@ -0,0 +1,151 @@
1
+ ---
2
+ name: backend-fintech-observability
3
+ description: Trade event correlation; market-hours aware scheduling; SLOs for fintech systems; compliance logging; alerting strategy.
4
+ topics: [backend, fintech, observability, tracing, slos, alerting, correlation-id, market-hours]
5
+ ---
6
+
7
+ Observability for a trading system is not generic APM with a finance skin — it is the ability to reconstruct any single order, end to end, across six or more services, on demand, years later, with timezone-correct timestamps and a stable correlation identifier. It is also the early-warning system that catches a sudden drop in fill rate at 09:31 ET before the desk calls. Done well it overlaps with — but does not replace — the immutable audit trail (`backend-fintech-compliance.md`).
8
+
9
+ ## Summary
10
+
11
+ Every trade flow is distributed. A single order crosses the wizard (UI), order-management service, risk-check service, broker adapter, fill-processing worker, ledger, and balance projection — typically six to ten hops across HTTP and message queues. A correlation ID must be minted at the edge, propagated across every hop (HTTP header, MQ header, database row, log line, span attribute), and indexed in both logs and tracing. Without this, a "my order is stuck" ticket becomes an archaeology project.
12
+
13
+ SLOs for fintech are tighter than typical SaaS and are defined per flow, not per service. Order-submission (client click → broker ack) runs in the low hundreds of milliseconds at p99. Fill-processing (broker fill event → ledger write) targets single-digit seconds. Ledger-to-balance propagation (fill persisted → UI-visible balance update) targets under a minute — users refresh aggressively after a trade. Error budgets are monthly, tracked per flow, and breached budgets freeze risky deploys.
14
+
15
+ Market-hours-aware alerting is non-negotiable. A stale-price alert at 03:00 ET on a US equity feed is noise; at 09:35 ET it is a P1. A prolonged broker outage at any hour is a P1 — after-hours orders still queue, and overnight index futures trade nearly 24 hours. The alerting layer must consult a trading-calendar service (per venue, per asset class, with half-days and holidays) and route accordingly. Maintenance windows are encoded as first-class suppression rules, not tribal knowledge.
16
+
17
+ Regulatory logging is additive to operational observability. The audit trail (WORM storage, 7-year retention for most US regimes) is written in parallel with — never replaced by — ops logs. Ops logs rotate at 30–90 days. Conflating the two produces either cripplingly expensive storage or a compliance breach; keep the pipelines separate with different retention classes.
18
+
19
+ Alert on anomalies, not just errors. A zero-error service that suddenly stops filling orders is worse than one throwing 500s. Track fill-rate baselines (by symbol, venue, time-of-day bucket), risk-check reject rates, P&L velocity, and queue depths; alert on deviation from baseline, not just on thresholds. Cross-ref: `backend-fintech-order-lifecycle.md` for the flow being instrumented, `backend-fintech-risk-management.md` for risk-specific signals, `backend-fintech-broker-integration.md` for per-broker health, `backend-fintech-compliance.md` for the audit-log boundary.
20
+
21
+ ## Deep Guidance
22
+
23
+ ### Correlation IDs Across Every Hop
24
+
25
+ Use a two-tier identifier scheme. The outer tier is the `client_order_id` — minted in the UI (or API client), echoed on every user-facing artifact (order ticket, confirmation email, support ticket), and stable for the lifetime of the order. The inner tier is a W3C Trace Context `trace-id` per service interaction, standard across OpenTelemetry instrumentations. The `client_order_id` is the noun a human searches for ("why is order ABC-123 stuck?"); the `trace-id` is the edge-traversal graph a tool renders.
26
+
27
+ Propagation rules are absolute. HTTP boundaries carry `traceparent` and `tracestate` per W3C; add an `X-Client-Order-Id` header for the outer ID. Message queues (SQS, Kafka, RabbitMQ) put both in message attributes/headers — never only in the payload, because DLQ tools and retry wrappers often strip payload context. Database rows for orders, fills, and ledger entries store the `client_order_id` column indexed. Every structured log line and every span carries both. If a queue hop drops the correlation ID, that is a P2 bug to be fixed, not tolerated.
28
+
29
+ ### Structured Logging Schema
30
+
31
+ Logs are JSON, single-line, with a fixed minimum set of fields plus event-type-specific extensions. Emit through a shared library so the schema is enforced, not hoped for.
32
+
33
+ ```json
34
+ {
35
+ "timestamp": "2026-04-15T13:31:04.128Z",
36
+ "timestamp_local": "2026-04-15T09:31:04.128-04:00",
37
+ "level": "info",
38
+ "service": "order-management",
39
+ "event_type": "order.submitted",
40
+ "client_order_id": "ABC-123",
41
+ "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
42
+ "span_id": "00f067aa0ba902b7",
43
+ "account_id": "acct_9f3c",
44
+ "symbol": "AAPL",
45
+ "side": "buy",
46
+ "quantity": 100,
47
+ "limit_price": { "amount": "182.50", "currency": "USD" },
48
+ "broker_id": "alpaca",
49
+ "venue": "NASDAQ",
50
+ "latency_ms": 42
51
+ }
52
+ ```
53
+
54
+ Monetary amounts are objects with `amount` (string decimal, never float) and `currency`. Timestamps are ISO-8601 with explicit UTC *and* the exchange-local time for operator sanity — "09:31 ET" is how a trader thinks, "13:31 UTC" is how storage indexes. `event_type` uses dotted nouns (`order.submitted`, `fill.received`, `risk.blocked`, `ledger.posted`) so alerting and dashboards can route on prefix.
55
+
56
+ ### SLOs Per Flow
57
+
58
+ Define SLOs on user-visible flows, not on internal microservice latency. Typical targets:
59
+
60
+ | Flow | Metric | Target | Window |
61
+ |------|--------|--------|--------|
62
+ | Order submission (click → broker ack) | p99 latency | 400 ms | 30 days |
63
+ | Fill processing (broker fill → ledger) | p99 latency | 5 s | 30 days |
64
+ | Ledger-to-balance propagation | p95 lag | 45 s | 30 days |
65
+ | Balance query (API read) | p99 latency | 200 ms | 30 days |
66
+ | Order-submission availability | success rate | 99.9% | 30 days during market hours |
67
+
68
+ Error budgets are consumed only against market-hours traffic; off-hours degradation is tracked separately. When a budget is below 25% remaining, ship freeze applies to risk-touching services until the window resets or a postmortem authorizes deploy.
69
+
70
+ ```yaml
71
+ # slo/order-submission.yaml
72
+ slo:
73
+ name: order-submission-latency
74
+ service: order-management
75
+ flow: order.submitted -> order.broker_ack
76
+ objective: 99.0
77
+ indicator:
78
+ type: latency
79
+ percentile: 99
80
+ threshold_ms: 400
81
+ window:
82
+ duration: 30d
83
+ market_hours_only: true
84
+ calendar: us-equities
85
+ alerting:
86
+ burn_rate:
87
+ - window: 1h
88
+ factor: 14.4 # fast burn
89
+ severity: page
90
+ - window: 6h
91
+ factor: 6
92
+ severity: ticket
93
+ ```
94
+
95
+ ### Market-Hours-Aware Alerting
96
+
97
+ A trading-calendar service (backed by `pandas-market-calendars`, `iex-cloud`, or Polygon reference data) exposes `is_market_open(venue, timestamp)` and `next_open/close(venue)`. Alert routing rules consult it. Stale-price alerts are suppressed outside regular session; broker-outage alerts escalate regardless; ledger-lag alerts during after-hours page only if open positions exist. Encode the routing in the alert platform (PagerDuty event rules, Grafana OnCall) rather than in per-alert bespoke logic, so maintenance is tractable.
98
+
99
+ Half-days (1 pm ET close around Thanksgiving and July 3rd) and early-close holidays are where this fails in practice — test the calendar service against a full year of real sessions, not a regex on weekdays.
100
+
101
+ ### Trade Anomaly Detection
102
+
103
+ Error rates alone miss the most dangerous incidents. Track baselines and deviation. Fill rate per minute per symbol, compared against a trailing 20-session median for the same minute-bucket, alerting on >3σ deviation. Risk-check reject rate per account, alerting on sudden jumps (often a config deploy gone wrong). P&L velocity (dollars-per-minute realized loss) with tiered alerts — warn, page, and auto-trigger the kill switch at escalating thresholds. Queue depth on the fill-processing worker, alerting when backlog grows faster than drain rate.
104
+
105
+ Honeycomb BubbleUp or Datadog Watchdog handle the baseline math adequately for most teams; rolling homegrown detectors is rarely worth it until scale forces it.
106
+
107
+ ### Multi-Broker Observability
108
+
109
+ Every broker integration (`backend-fintech-broker-integration.md`) exports the same metric taxonomy — `broker.submit.latency`, `broker.submit.errors`, `broker.fill.lag`, `broker.reconnect.count` — tagged with `broker_id`. Dashboards show aggregate health on top (all brokers combined) with per-broker drill-downs below. When one broker degrades, failover logic (if implemented) must emit its own events (`broker.failover.triggered`) with both old and new broker IDs and the failover reason.
110
+
111
+ ### Distributed Tracing with OpenTelemetry
112
+
113
+ OpenTelemetry SDKs in every service, exporter to a backend (Honeycomb, Datadog APM, Grafana Tempo, or a Grafana LGTM stack). Span attributes carry financial context — `trading.symbol`, `trading.side`, `trading.quantity`, `trading.account_id`, `trading.broker_id`, `trading.client_order_id`. Sample tail-based: keep 100% of traces that contain an error or a latency outlier, throttle successes to 5–10%. Head-based sampling (decide at root) loses the ability to keep all errors and is the wrong default for fintech.
114
+
115
+ ```typescript
116
+ // correlation-middleware.ts (Express + OpenTelemetry)
117
+ import { trace, context } from '@opentelemetry/api';
118
+ import { randomUUID } from 'node:crypto';
119
+ import type { Request, Response, NextFunction } from 'express';
120
+
121
+ export function correlationMiddleware(req: Request, res: Response, next: NextFunction) {
122
+ const clientOrderId = req.header('x-client-order-id') ?? `srv-${randomUUID()}`;
123
+ const span = trace.getActiveSpan();
124
+ span?.setAttribute('trading.client_order_id', clientOrderId);
125
+
126
+ res.setHeader('x-client-order-id', clientOrderId);
127
+ (req as any).clientOrderId = clientOrderId;
128
+
129
+ // Attach to async context so downstream logger picks it up.
130
+ const ctx = context.active().setValue(Symbol.for('client_order_id'), clientOrderId);
131
+ context.with(ctx, () => next());
132
+ }
133
+ ```
134
+
135
+ ### Log Retention vs Audit Retention
136
+
137
+ Two pipelines, two retention classes. Ops logs ship to Loki or a hot Elasticsearch tier with 30–90 day retention, indexed for high-cardinality ad-hoc querying. Audit events ship to append-only WORM storage (S3 Object Lock, Glacier with compliance-mode retention, or a dedicated vendor like DataBP) for the regulatory retention period — typically 7 years for SEC 17a-4 style records, sometimes longer. The two pipelines share schema where possible but diverge in transport, storage, and access controls. Never satisfy audit requirements by pointing at your Datadog log archive — it is not WORM unless explicitly configured, and even then access controls are wrong.
138
+
139
+ ### Common Pitfalls
140
+
141
+ Correlation IDs dropped at queue hops because a retry wrapper reconstructed the message from payload only. Fix: make propagation a library concern, test explicitly.
142
+
143
+ Timezones not logged. A 14:30 timestamp is ambiguous; a 14:30Z plus 09:30-05:00 is not. Log both.
144
+
145
+ Alerts firing during scheduled broker maintenance windows because the suppression was a shared Google Doc. Fix: encode maintenance in the alerting platform with a defined owner and a review cadence.
146
+
147
+ No dashboard for the first-minute-of-open latency spike. The open is where queues are fullest, baselines are least representative, and incidents cluster. A dedicated "09:30–09:35 ET" dashboard saves incidents.
148
+
149
+ Tracing without financial attributes. Spans that show HTTP method and path but not symbol, side, or account are useless at 2 am. Every span on a trade path must carry the trading tuple.
150
+
151
+ Sampling that throws away errors. Head-based 1% sampling loses 99% of failure traces. Use tail-based sampling or keep 100% of errors regardless of sample rate.