-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathexecution_upgrade.json
More file actions
14 lines (14 loc) · 11.4 KB
/
Copy pathexecution_upgrade.json
File metadata and controls
14 lines (14 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"tickets/08-execution-guards.md": "# Ticket 08 — Execution guards & marketable-limit routing\n\n## Goal\nReplace market orders with **marketable-limit** orders protected by a **price band**, add auction/holiday awareness, LULD/halts enforcement, child-order slicing, and idempotent order IDs.\n\n## Requirements\n- Default order type: **marketable_limit** (not market)\n- Price band vs last/nbbo mid: limit = side==buy ? min(mid + band, upper_luld) : max(mid - band, lower_luld)\n- Band logic: `band_bps = max(cfg.min_band_bps, cfg.band_spread_multiplier * spread_bps, cfg.band_cap_bps)`\n- Enforce **LULD**: do not send an order outside bands; block entries while halted or during LULD straddle\n- Respect **auction windows**: block entries in opening/closing auction unless `open_close_auction_entries=true`\n- Respect **market hours** + holiday/half-day calendar\n- **Child orders**: if notional > 1% of 1-min ADV, slice with TWAP/POV\n- **Idempotency**: deterministic `client_order_id` hash of (strategy, symbol, ts, side, qty, limit)\n\n## Deliverables\n- `src/execution/OrderRouter.ts` with `buildMarketableLimit()`, `sliceChildOrders()`, `routeOrder()`\n- `src/execution/executionGuards.ts` with `canEnter()` (LULD/halts/auctions/hours/spread)\n- `src/execution/calendars.ts` (US equities holiday/half-day stub + API hook)\n- `src/execution/idempotency.ts` for client order IDs\n- Config section in `data_providers.yaml` → `execution:` knobs\n\n## Tests\n- `tests/execution.spec.ts` covering band calculation, LULD guard, auction block, idempotent ID stability, and slicing triggers.\n",
"tickets/09-adapter-contracts.md": "# Ticket 09 — Adapter contracts for bars & halts\n\n## Goal\nDefine strict interfaces for **bars** and **halts/LULD** across providers and wire them into ProviderRouter.\n\n## Requirements\n- `BarsAdapter` with `getBars(symbol, interval, from, to)` → `NormalizedBar[]`\n- `HaltAdapter` with `getHaltState(symbol)` → `{ halted:boolean, luld?:{upper:number, lower:number} }`\n- Polygon primary for halts/LULD; FMP secondary; Yahoo last-resort (halts unavailable ⇒ null)\n- Normalize timestamps (epoch ms), corporate actions adjustments flag\n- ProviderRouter must expose `getHaltState(symbol)` and pass it to execution guards\n\n## Deliverables\n- `src/data/contracts.ts` for interfaces\n- `src/data/providers/halts.ts` with polygon/fmp/yahoo stubs\n- `src/data/providers/bars.ts` with polygon/tiingo/finnhub stubs\n- Router glue to surface halts/LULD to execution\n\n## Tests\n- `tests/contracts.spec.ts` to ensure all adapters implement required shape and null safety.\n",
"src/data/contracts.ts": "import { NormalizedBar, NormalizedQuote } from \"./types\";\n\nexport interface QuotesAdapter { getQuote(symbol:string): Promise<NormalizedQuote|null>; }\nexport interface BarsAdapter { getBars(symbol:string, interval:\"1m\"|\"5m\"|\"1d\", fromMs:number, toMs:number): Promise<NormalizedBar[]>; }\nexport interface HaltState { halted:boolean; luld?:{ upper:number; lower:number }|null; ts_exchange?:number; }\nexport interface HaltsAdapter { getHaltState(symbol:string): Promise<HaltState|null>; }\n",
"src/data/providers/bars.ts": "import { BarsAdapter } from \"../contracts\"; import { NormalizedBar } from \"../types\";\n\nexport class PolygonBars implements BarsAdapter {\n async getBars(symbol:string, interval:\"1m\"|\"5m\"|\"1d\", fromMs:number, toMs:number): Promise<NormalizedBar[]> {\n // TODO: call Polygon aggregates, normalize, set adjusted=true if back-adjusted\n return [];\n }\n}\n\nexport class TiingoBars implements BarsAdapter {\n async getBars(symbol:string, interval:\"1m\"|\"5m\"|\"1d\", fromMs:number, toMs:number): Promise<NormalizedBar[]> { return []; }\n}\n\nexport class FinnhubBars implements BarsAdapter {\n async getBars(symbol:string, interval:\"1m\"|\"5m\"|\"1d\", fromMs:number, toMs:number): Promise<NormalizedBar[]> { return []; }\n}\n",
"src/data/providers/halts.ts": "import { HaltsAdapter, HaltState } from \"../contracts\";\n\nexport class PolygonHalts implements HaltsAdapter {\n async getHaltState(_symbol:string): Promise<HaltState|null> {\n // TODO: query Polygon status/LULD, map\n return { halted:false, luld:null };\n }\n}\n\nexport class FmpHalts implements HaltsAdapter {\n async getHaltState(_symbol:string): Promise<HaltState|null> { return null; }\n}\n\nexport class YahooHalts implements HaltsAdapter {\n async getHaltState(_symbol:string): Promise<HaltState|null> { return null; }\n}\n",
"src/execution/types.ts": "export type Side = \"buy\"|\"sell\";\nexport interface OrderIntent { symbol:string; side:Side; qty:number; notional:number; mid:number; spread_bps:number; luld?:{upper:number; lower:number}|null; }\nexport interface RoutedOrder { client_order_id:string; symbol:string; side:Side; qty:number; type:\"limit\"; limit:number; timeInForce:\"IOC\"|\"DAY\"; sliceId?:number; totalSlices?:number; }\nexport interface ExecConfig {\n min_band_bps:number; band_spread_multiplier:number; band_cap_bps:number;\n open_close_auction_entries:boolean; participation_target:number; twap_slice_secs:number; adv_slice_threshold: number; // fraction of 1m ADV\n}\n",
"src/execution/executionGuards.ts": "import { HaltState } from \"../data/contracts\";\n\nexport function isMarketOpen(now:Date, tzOffsetMinutes:number, holidays:Set<string>, halfDays:Set<string>): boolean {\n // Simple US RTH stub: 9:30–16:00 ET (adjusted by tzOffsetMinutes)\n // In production, pull official exchange calendar\n const local = new Date(now.getTime() + tzOffsetMinutes*60000);\n const ymd = local.toISOString().slice(0,10);\n if (holidays.has(ymd)) return false;\n const hh = local.getUTCHours(); const mm = local.getUTCMinutes();\n // crude window; replace with proper calendar logic and DST handling\n const mins = hh*60+mm; const open = 14*60+30; const close = 21*60; // UTC equivalents vary; treat tzOffset as input for now\n return mins >= open && mins < close;\n}\n\nexport function canEnter(halt:HaltState|null, spread_bps:number|null, allowAuctions:boolean): { ok:boolean; reasons:string[] } {\n const reasons:string[] = [];\n if (halt?.halted) { reasons.push(\"halted\"); }\n if (halt?.luld) { reasons.push(\"LULD active\"); }\n if (!allowAuctions) { /* router should also check auction windows externally */ }\n if (spread_bps!=null && spread_bps > 25) reasons.push(\"spread too wide\");\n return { ok: reasons.length===0, reasons };\n}\n",
"src/execution/idempotency.ts": "import crypto from \"crypto\";\nexport function clientOrderId(strategy:string, symbol:string, ts:number, side:\"buy\"|\"sell\", qty:number, limit:number){\n const raw = `${strategy}|${symbol}|${ts}|${side}|${qty}|${limit.toFixed(4)}`;\n return crypto.createHash(\"sha256\").update(raw).digest(\"hex\").slice(0, 24);\n}\n",
"src/execution/OrderRouter.ts": "import { OrderIntent, RoutedOrder, ExecConfig } from \"./types\";\nimport { clientOrderId } from \"./idempotency\";\n\nexport function computeBandBps(spread_bps:number|null, cfg:ExecConfig){\n const spreadTerm = (spread_bps ?? 0) * cfg.band_spread_multiplier;\n return Math.min(Math.max(cfg.min_band_bps, spreadTerm), cfg.band_cap_bps);\n}\n\nexport function buildMarketableLimit(intent:OrderIntent, cfg:ExecConfig): RoutedOrder {\n const band_bps = computeBandBps(intent.spread_bps, cfg);\n const band = intent.mid * (band_bps / 10000);\n const upper = intent.luld?.upper ?? Number.POSITIVE_INFINITY;\n const lower = intent.luld?.lower ?? 0;\n const rawLimit = intent.side === \"buy\" ? Math.min(intent.mid + band, upper) : Math.max(intent.mid - band, lower);\n const limit = Number(rawLimit.toFixed(4));\n const id = clientOrderId(\"agent\", intent.symbol, Date.now(), intent.side, intent.qty, limit);\n return { client_order_id: id, symbol:intent.symbol, side:intent.side, qty:intent.qty, type:\"limit\", limit, timeInForce:\"IOC\" };\n}\n\nexport function sliceChildOrders(totalQty:number, slices:number): number[] {\n const base = Math.floor(totalQty / slices); const rem = totalQty % slices;\n return Array.from({length:slices}, (_,i)=> base + (i<rem?1:0));\n}\n\nexport function planSlices(intent:OrderIntent, oneMinADV:number, cfg:ExecConfig): { slices:number; qtys:number[] } {\n const exceed = intent.notional > oneMinADV * cfg.adv_slice_threshold;\n if (!exceed) return { slices:1, qtys:[intent.qty] };\n const slices = Math.max(2, Math.ceil((intent.notional / (oneMinADV * cfg.participation_target))));\n return { slices, qtys: sliceChildOrders(intent.qty, slices) };\n}\n",
"src/config/data_providers.yaml": "data_providers:\n prices:\n order: [polygon_ws, tiingo_iex, finnhub_rest, twelve_data_rapidapi, fmp_rapidapi, yahoo_finance]\n validation:\n floor_bps: 5\n spread_multiplier: 2.0\n cap_bps: 15\n min_quorum: 2\n freshness_ms:\n quotes: 2000\n bars_1m: 60000\n corp_actions:\n order: [polygon, fmp_rapidapi, tiingo]\n fundamentals:\n order: [ycharts, fmp_rapidapi, tiingo, alpha_vantage, yahoo_finance]\n news:\n order: [benzinga, tiingo_news, newsapi]\n sentiment:\n order: [tiingo_news_nlp, stocktwits, reddit]\n macro:\n order: [ycharts, fred]\nrate_limits:\n polygon_ws: { strategy: ws_stream }\n finnhub_rest: { rpm: 150 }\n fmp_rapidapi: { rpm: 60 }\n twelve_data_rapidapi: { rpm: 60 }\nexecution:\n min_band_bps: 5\n band_spread_multiplier: 1.5\n band_cap_bps: 25\n open_close_auction_entries: false\n participation_target: 0.05\n adv_slice_threshold: 0.01\n twap_slice_secs: 20\nexecution_guards:\n respect_luld: true\n block_on_halt: true\n max_spread_bps: 25\nevents:\n block_earnings_window_days: 3\n",
"tests/execution.spec.ts": "import { describe, it, expect } from \"vitest\";\nimport { buildMarketableLimit, computeBandBps, planSlices } from \"../src/execution/OrderRouter\";\n\ndescribe(\"execution\", () => {\n const cfg = { min_band_bps:5, band_spread_multiplier:1.5, band_cap_bps:25, open_close_auction_entries:false, participation_target:0.05, twap_slice_secs:20, adv_slice_threshold:0.01 } as any;\n it(\"computes a reasonable band\", () => {\n expect(computeBandBps(10, cfg)).toBeGreaterThanOrEqual(5);\n expect(computeBandBps(1, cfg)).toBeGreaterThanOrEqual(5);\n });\n it(\"builds marketable limit within band\", () => {\n const intent = { symbol:\"AAPL\", side:\"buy\", qty:100, notional:20000, mid:200, spread_bps:10, luld:{ upper: 205, lower: 195 } } as any;\n const order = buildMarketableLimit(intent, cfg);\n expect(order.type).toBe(\"limit\");\n expect(order.limit).toBeLessThanOrEqual(205);\n });\n it(\"plans slices when notional exceeds ADV threshold\", () => {\n const intent = { symbol:\"AAPL\", side:\"buy\", qty:1000, notional:500000, mid:200, spread_bps:10 } as any;\n const plan = planSlices(intent, 1000000, cfg); // 1m ADV\n expect(plan.slices).toBeGreaterThanOrEqual(2);\n expect(plan.qtys.reduce((a,b)=>a+b,0)).toBe(1000);\n });\n});\n",
"tests/contracts.spec.ts": "import { describe, it, expect } from \"vitest\";\nimport { PolygonBars, TiingoBars, FinnhubBars } from \"../src/data/providers/bars\";\nimport { PolygonHalts, FmpHalts, YahooHalts } from \"../src/data/providers/halts\";\n\ndescribe(\"contracts\", () => {\n it(\"bars adapters exist\", async () => {\n expect(new PolygonBars()).toBeTruthy();\n expect(new TiingoBars()).toBeTruthy();\n expect(new FinnhubBars()).toBeTruthy();\n });\n it(\"halts adapters exist\", async () => {\n expect(new PolygonHalts()).toBeTruthy();\n expect(new FmpHalts()).toBeTruthy();\n expect(new YahooHalts()).toBeTruthy();\n });\n});\n"
}