{"id":30345833,"url":"https://github.com/dimdengd/stock-runner","last_synced_at":"2025-08-18T14:38:55.525Z","repository":{"id":308417612,"uuid":"1029916876","full_name":"dimdenGD/stock-runner","owner":"dimdenGD","description":"JS stock algotrading backtester","archived":false,"fork":false,"pushed_at":"2025-08-05T21:26:14.000Z","size":75,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-08-05T21:27:01.859Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dimdenGD.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"patreon":"dimdendev","custom":["https://dimden.dev/donate"]}},"created_at":"2025-07-31T19:31:26.000Z","updated_at":"2025-08-05T21:26:18.000Z","dependencies_parsed_at":"2025-08-05T21:27:03.898Z","dependency_job_id":"b26cb3d6-62be-4a67-9c24-b4df9465d526","html_url":"https://github.com/dimdenGD/stock-runner","commit_stats":null,"previous_names":["dimdengd/stock-runner"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dimdenGD/stock-runner","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimdenGD%2Fstock-runner","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimdenGD%2Fstock-runner/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimdenGD%2Fstock-runner/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimdenGD%2Fstock-runner/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dimdenGD","download_url":"https://codeload.github.com/dimdenGD/stock-runner/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dimdenGD%2Fstock-runner/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":271010908,"owners_count":24684378,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-18T02:00:08.743Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2025-08-18T14:38:51.462Z","updated_at":"2025-08-18T14:38:55.509Z","avatar_url":"https://github.com/dimdenGD.png","language":"JavaScript","funding_links":["https://patreon.com/dimdendev","https://dimden.dev/donate"],"categories":[],"sub_categories":[],"readme":"# Stock Runner\nBecause of lack of good backtesting tools in JavaScript, I've decided to build my own.\nIt uses QuestDB to efficiently store and query the data.\nIt's also quite fast and nice to use. You can run a 5 year backtest on ALL stocks in 1 minute (on daily ticks).\n\n## Installation\n1. Clone the repository\n2. Install dependencies with `npm install`\n3. QuestDB:\n- Install from https://questdb.com/download/\n- Run it with `./questdb.exe` or `./questdb`\n- By default QuestDB uses `admin:quest@localhost:8812/qdb` as credentials. If you just plan on running it locally, you can leave it as is. Otherwise you can set the optional `QUESTDB_USERNAME`, `QUESTDB_PASSWORD`, `QUESTDB_HOST`, `QUESTDB_PORT`, and `QUESTDB_DATABASE` environment variables.\n\n## Getting the data\n\n### Stooq\nFree, adjusted for splits\n1. Go to https://stooq.com/db/h/\n2. Download daily/hourly/5min data and put `nasdaq stocks` etc folders in `data/stooq/`\n3. Run the script to import the data from CSV files to QuestDB:\n```\nnode scripts/stooq_ingest.js \u003c1d|1h|5m\u003e\n```\n  \n4. You can re-run these scripts to update the data.\n\n### Polygon.io\nPaid, not recommended because doesn't adjust for splits\n1. Create an account at https://polygon.io/\n2. Get flat files API key at https://polygon.io/dashboard/keys\n3. Set the `POLYGON_ACCESS_KEY_ID` and `POLYGON_SECRET_ACCESS_KEY` environment variables.\n4. Run the script to download the data from Polygon to CSV files:\n```\nnode scripts/polygon_download.js \u003c1d|1m\u003e\n```\n5. Run the script to import the data from CSV files to QuestDB:\n```\nnode scripts/polygon_ingest.js \u003c1d|1m\u003e\n```\nImporting 1m can take up to 5 hours.  \n  \n6. You can re-run these scripts to update the data.\n\n## Running a backtest\n1. Create a strategy in `strategies/`\n2. Run the backtest:\n```\nnode strategies/sma.js\n```\n\n## Strategies\n\n### Example strategy on single stock - SMA crossover\n```js\nimport Strategy from '../src/backtest/strategy.js';\nimport Backtest from '../src/backtest/index.js';\nimport IBKR from '../src/brokers/ibkr.js';\n\nconst SHORT_LEN = 25;\nconst LONG_LEN = SHORT_LEN * 2;\n\nconst sma = candles =\u003e candles.reduce((sum, c) =\u003e sum + c.close, 0) / candles.length;\n\nconst smaCrossover = new Strategy({\n    intervals: {\n        '1d': { count: LONG_LEN, main: true },\n    },\n    onTick: async ({ candle, getCandles, buy, sell, stockBalance }) =\u003e {\n        const lastLong = getCandles('1d', LONG_LEN);\n        const lastShort = getCandles('1d', SHORT_LEN);\n\n        const longMA = sma(lastLong);\n        const shortMA = sma(lastShort);\n        const price = candle.close;\n\n        if (stockBalance === 0 \u0026\u0026 shortMA \u003e longMA) {\n            buy(3, price);\n        }\n        else if (stockBalance \u003e 0 \u0026\u0026 shortMA \u003c longMA) {\n            sell(stockBalance, price);\n        }\n    }\n});\n\nconst bt = new Backtest({\n    strategy: smaCrossover,\n    startDate: new Date('2020-07-14'),\n    endDate: new Date('2025-07-30'),\n    startCashBalance: 10_000,\n    broker: new IBKR('tiered'),\n    logs: {\n        swaps: false,\n        trades: true\n    }\n});\n\nconst result = await bt.runOnStock('AAPL');\nbt.logMetrics(result);\n```\nResult:  \n  \n![image](https://lune.dimden.dev/9157964b4648.png) \n\n### Example strategy on all stocks - SMA crossover\n```js\nimport Strategy from '../src/backtest/strategy.js';\nimport Backtest from '../src/backtest/index.js';\nimport IBKR from '../src/brokers/ibkr.js';\n\nconst SHORT_LEN = 14;\nconst LONG_LEN = SHORT_LEN * 2;\nconst MAX_POS_PER_STOCK = 50_000; // Maximum position size per stock\n\nconst sma = candles =\u003e candles.reduce((sum, c) =\u003e sum + c.close, 0) / candles.length;\n\nconst smaCrossover = new Strategy({\n    intervals: {\n        '1d': { count: LONG_LEN, main: true },\n    },\n    onTick: async ({ stocks, currentDate, ctx }) =\u003e {\n        // Process each filtered stock\n        for (const s of stocks) {\n            const { stockName, candle, getCandles, buy, sell, stockBalance } = s;\n            \n            try {\n                const lastLong = getCandles('1d', LONG_LEN);\n                const lastShort = getCandles('1d', SHORT_LEN);\n\n                if (!lastLong || !lastShort || lastLong.length \u003c LONG_LEN || lastShort.length \u003c SHORT_LEN) {\n                    continue; // Skip if we don't have enough data\n                }\n\n                const longMA = sma(lastLong);\n                const shortMA = sma(lastShort);\n                const price = candle.close;\n\n                // Buy signal: short MA crosses above long MA and we have no position\n                if (stockBalance === 0 \u0026\u0026 shortMA \u003e longMA) {\n                    const perNameBudget = Math.min(ctx.cashBalance / 10, MAX_POS_PER_STOCK); // Divide cash among up to 10 positions\n                    if (Object.values(ctx.stockBalances).length \u003c 10) {\n                        const qty = Math.floor(perNameBudget / price);\n                        if (qty \u003e 0) {\n                            buy(qty, price);\n                        }\n                    }\n                }\n                // Sell signal: short MA crosses below long MA and we have a position\n                else if (stockBalance \u003e 0 \u0026\u0026 shortMA \u003c longMA) {\n                    sell(stockBalance, price);\n                }\n            } catch (error) {\n                // Skip this stock if there's an error (e.g., insufficient data)\n                continue;\n            }\n        }\n    }\n});\n\nconst bt = new Backtest({\n    strategy: smaCrossover,\n    startDate: new Date('2024-07-14'),\n    endDate: new Date('2025-07-30'),\n    startCashBalance: 100_000,\n    broker: new IBKR('tiered'),\n    logs: {\n        swaps: false,\n        trades: true\n    }\n});\n\nconst result = await bt.runOnAllStocks();\nbt.logMetrics(result);\n```\nResult:  \n  \n![image](https://lune.dimden.dev/c78c2e502eeb.png) ","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdimdengd%2Fstock-runner","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdimdengd%2Fstock-runner","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdimdengd%2Fstock-runner/lists"}