Compare commits

...

7 Commits

9 changed files with 548 additions and 175 deletions
+2
View File
@@ -1,2 +1,4 @@
.pnpm-store
node_modules
.aider*
.env
+1
View File
@@ -1 +1,2 @@
nodejs 20.15.1
python 3.12.4
+1
View File
@@ -28,6 +28,7 @@
"@types/node": "^20.10.7",
"esbuild": "^0.19.11",
"npm-run-all": "^4.1.5",
"tsx": "^4.17.0",
"typescript": "^5.3.3"
}
}
+283
View File
@@ -63,6 +63,9 @@ importers:
npm-run-all:
specifier: ^4.1.5
version: 4.1.5
tsx:
specifier: ^4.17.0
version: 4.17.0
typescript:
specifier: ^5.3.3
version: 5.3.3
@@ -82,138 +85,282 @@ packages:
cpu: [ppc64]
os: [aix]
'@esbuild/aix-ppc64@0.23.0':
resolution: {integrity: sha512-3sG8Zwa5fMcA9bgqB8AfWPQ+HFke6uD3h1s3RIwUNK8EG7a4buxvuFTs3j1IMs2NXAk9F30C/FF4vxRgQCcmoQ==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [aix]
'@esbuild/android-arm64@0.19.11':
resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==}
engines: {node: '>=12'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm64@0.23.0':
resolution: {integrity: sha512-EuHFUYkAVfU4qBdyivULuu03FhJO4IJN9PGuABGrFy4vUuzk91P2d+npxHcFdpUnfYKy0PuV+n6bKIpHOB3prQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [android]
'@esbuild/android-arm@0.19.11':
resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==}
engines: {node: '>=12'}
cpu: [arm]
os: [android]
'@esbuild/android-arm@0.23.0':
resolution: {integrity: sha512-+KuOHTKKyIKgEEqKbGTK8W7mPp+hKinbMBeEnNzjJGyFcWsfrXjSTNluJHCY1RqhxFurdD8uNXQDei7qDlR6+g==}
engines: {node: '>=18'}
cpu: [arm]
os: [android]
'@esbuild/android-x64@0.19.11':
resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==}
engines: {node: '>=12'}
cpu: [x64]
os: [android]
'@esbuild/android-x64@0.23.0':
resolution: {integrity: sha512-WRrmKidLoKDl56LsbBMhzTTBxrsVwTKdNbKDalbEZr0tcsBgCLbEtoNthOW6PX942YiYq8HzEnb4yWQMLQuipQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [android]
'@esbuild/darwin-arm64@0.19.11':
resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-arm64@0.23.0':
resolution: {integrity: sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==}
engines: {node: '>=18'}
cpu: [arm64]
os: [darwin]
'@esbuild/darwin-x64@0.19.11':
resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==}
engines: {node: '>=12'}
cpu: [x64]
os: [darwin]
'@esbuild/darwin-x64@0.23.0':
resolution: {integrity: sha512-IMQ6eme4AfznElesHUPDZ+teuGwoRmVuuixu7sv92ZkdQcPbsNHzutd+rAfaBKo8YK3IrBEi9SLLKWJdEvJniQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [darwin]
'@esbuild/freebsd-arm64@0.19.11':
resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==}
engines: {node: '>=12'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-arm64@0.23.0':
resolution: {integrity: sha512-0muYWCng5vqaxobq6LB3YNtevDFSAZGlgtLoAc81PjUfiFz36n4KMpwhtAd4he8ToSI3TGyuhyx5xmiWNYZFyw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [freebsd]
'@esbuild/freebsd-x64@0.19.11':
resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==}
engines: {node: '>=12'}
cpu: [x64]
os: [freebsd]
'@esbuild/freebsd-x64@0.23.0':
resolution: {integrity: sha512-XKDVu8IsD0/q3foBzsXGt/KjD/yTKBCIwOHE1XwiXmrRwrX6Hbnd5Eqn/WvDekddK21tfszBSrE/WMaZh+1buQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [freebsd]
'@esbuild/linux-arm64@0.19.11':
resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==}
engines: {node: '>=12'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm64@0.23.0':
resolution: {integrity: sha512-j1t5iG8jE7BhonbsEg5d9qOYcVZv/Rv6tghaXM/Ug9xahM0nX/H2gfu6X6z11QRTMT6+aywOMA8TDkhPo8aCGw==}
engines: {node: '>=18'}
cpu: [arm64]
os: [linux]
'@esbuild/linux-arm@0.19.11':
resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==}
engines: {node: '>=12'}
cpu: [arm]
os: [linux]
'@esbuild/linux-arm@0.23.0':
resolution: {integrity: sha512-SEELSTEtOFu5LPykzA395Mc+54RMg1EUgXP+iw2SJ72+ooMwVsgfuwXo5Fn0wXNgWZsTVHwY2cg4Vi/bOD88qw==}
engines: {node: '>=18'}
cpu: [arm]
os: [linux]
'@esbuild/linux-ia32@0.19.11':
resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==}
engines: {node: '>=12'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-ia32@0.23.0':
resolution: {integrity: sha512-P7O5Tkh2NbgIm2R6x1zGJJsnacDzTFcRWZyTTMgFdVit6E98LTxO+v8LCCLWRvPrjdzXHx9FEOA8oAZPyApWUA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [linux]
'@esbuild/linux-loong64@0.19.11':
resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==}
engines: {node: '>=12'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-loong64@0.23.0':
resolution: {integrity: sha512-InQwepswq6urikQiIC/kkx412fqUZudBO4SYKu0N+tGhXRWUqAx+Q+341tFV6QdBifpjYgUndV1hhMq3WeJi7A==}
engines: {node: '>=18'}
cpu: [loong64]
os: [linux]
'@esbuild/linux-mips64el@0.19.11':
resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==}
engines: {node: '>=12'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-mips64el@0.23.0':
resolution: {integrity: sha512-J9rflLtqdYrxHv2FqXE2i1ELgNjT+JFURt/uDMoPQLcjWQA5wDKgQA4t/dTqGa88ZVECKaD0TctwsUfHbVoi4w==}
engines: {node: '>=18'}
cpu: [mips64el]
os: [linux]
'@esbuild/linux-ppc64@0.19.11':
resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==}
engines: {node: '>=12'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-ppc64@0.23.0':
resolution: {integrity: sha512-cShCXtEOVc5GxU0fM+dsFD10qZ5UpcQ8AM22bYj0u/yaAykWnqXJDpd77ublcX6vdDsWLuweeuSNZk4yUxZwtw==}
engines: {node: '>=18'}
cpu: [ppc64]
os: [linux]
'@esbuild/linux-riscv64@0.19.11':
resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==}
engines: {node: '>=12'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-riscv64@0.23.0':
resolution: {integrity: sha512-HEtaN7Y5UB4tZPeQmgz/UhzoEyYftbMXrBCUjINGjh3uil+rB/QzzpMshz3cNUxqXN7Vr93zzVtpIDL99t9aRw==}
engines: {node: '>=18'}
cpu: [riscv64]
os: [linux]
'@esbuild/linux-s390x@0.19.11':
resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==}
engines: {node: '>=12'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-s390x@0.23.0':
resolution: {integrity: sha512-WDi3+NVAuyjg/Wxi+o5KPqRbZY0QhI9TjrEEm+8dmpY9Xir8+HE/HNx2JoLckhKbFopW0RdO2D72w8trZOV+Wg==}
engines: {node: '>=18'}
cpu: [s390x]
os: [linux]
'@esbuild/linux-x64@0.19.11':
resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==}
engines: {node: '>=12'}
cpu: [x64]
os: [linux]
'@esbuild/linux-x64@0.23.0':
resolution: {integrity: sha512-a3pMQhUEJkITgAw6e0bWA+F+vFtCciMjW/LPtoj99MhVt+Mfb6bbL9hu2wmTZgNd994qTAEw+U/r6k3qHWWaOQ==}
engines: {node: '>=18'}
cpu: [x64]
os: [linux]
'@esbuild/netbsd-x64@0.19.11':
resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [netbsd]
'@esbuild/netbsd-x64@0.23.0':
resolution: {integrity: sha512-cRK+YDem7lFTs2Q5nEv/HHc4LnrfBCbH5+JHu6wm2eP+d8OZNoSMYgPZJq78vqQ9g+9+nMuIsAO7skzphRXHyw==}
engines: {node: '>=18'}
cpu: [x64]
os: [netbsd]
'@esbuild/openbsd-arm64@0.23.0':
resolution: {integrity: sha512-suXjq53gERueVWu0OKxzWqk7NxiUWSUlrxoZK7usiF50C6ipColGR5qie2496iKGYNLhDZkPxBI3erbnYkU0rQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [openbsd]
'@esbuild/openbsd-x64@0.19.11':
resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==}
engines: {node: '>=12'}
cpu: [x64]
os: [openbsd]
'@esbuild/openbsd-x64@0.23.0':
resolution: {integrity: sha512-6p3nHpby0DM/v15IFKMjAaayFhqnXV52aEmv1whZHX56pdkK+MEaLoQWj+H42ssFarP1PcomVhbsR4pkz09qBg==}
engines: {node: '>=18'}
cpu: [x64]
os: [openbsd]
'@esbuild/sunos-x64@0.19.11':
resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==}
engines: {node: '>=12'}
cpu: [x64]
os: [sunos]
'@esbuild/sunos-x64@0.23.0':
resolution: {integrity: sha512-BFelBGfrBwk6LVrmFzCq1u1dZbG4zy/Kp93w2+y83Q5UGYF1d8sCzeLI9NXjKyujjBBniQa8R8PzLFAUrSM9OA==}
engines: {node: '>=18'}
cpu: [x64]
os: [sunos]
'@esbuild/win32-arm64@0.19.11':
resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==}
engines: {node: '>=12'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-arm64@0.23.0':
resolution: {integrity: sha512-lY6AC8p4Cnb7xYHuIxQ6iYPe6MfO2CC43XXKo9nBXDb35krYt7KGhQnOkRGar5psxYkircpCqfbNDB4uJbS2jQ==}
engines: {node: '>=18'}
cpu: [arm64]
os: [win32]
'@esbuild/win32-ia32@0.19.11':
resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==}
engines: {node: '>=12'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-ia32@0.23.0':
resolution: {integrity: sha512-7L1bHlOTcO4ByvI7OXVI5pNN6HSu6pUQq9yodga8izeuB1KcT2UkHaH6118QJwopExPn0rMHIseCTx1CRo/uNA==}
engines: {node: '>=18'}
cpu: [ia32]
os: [win32]
'@esbuild/win32-x64@0.19.11':
resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==}
engines: {node: '>=12'}
cpu: [x64]
os: [win32]
'@esbuild/win32-x64@0.23.0':
resolution: {integrity: sha512-Arm+WgUFLUATuoxCJcahGuk6Yj9Pzxd6l11Zb/2aAuv5kWWvvfhLFo2fni4uSK5vzlUdCGZ/BdV5tH8klj8p8g==}
engines: {node: '>=18'}
cpu: [x64]
os: [win32]
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@@ -462,6 +609,11 @@ packages:
engines: {node: '>=12'}
hasBin: true
esbuild@0.23.0:
resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==}
engines: {node: '>=18'}
hasBin: true
escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'}
@@ -497,6 +649,11 @@ packages:
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
os: [darwin]
function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -523,6 +680,9 @@ packages:
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
engines: {node: '>= 0.4'}
get-tsconfig@4.7.6:
resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==}
github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
@@ -986,6 +1146,9 @@ packages:
resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==}
engines: {node: '>= 0.4'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true
@@ -1164,6 +1327,11 @@ packages:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'}
tsx@4.17.0:
resolution: {integrity: sha512-eN4mnDA5UMKDt4YZixo9tBioibaMBpoxBkD+rIPAjVmYERSG0/dWEY1CEFuV89CgASlKL499q8AhmkMnnjtOJg==}
engines: {node: '>=18.0.0'}
hasBin: true
tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@@ -1252,72 +1420,144 @@ snapshots:
'@esbuild/aix-ppc64@0.19.11':
optional: true
'@esbuild/aix-ppc64@0.23.0':
optional: true
'@esbuild/android-arm64@0.19.11':
optional: true
'@esbuild/android-arm64@0.23.0':
optional: true
'@esbuild/android-arm@0.19.11':
optional: true
'@esbuild/android-arm@0.23.0':
optional: true
'@esbuild/android-x64@0.19.11':
optional: true
'@esbuild/android-x64@0.23.0':
optional: true
'@esbuild/darwin-arm64@0.19.11':
optional: true
'@esbuild/darwin-arm64@0.23.0':
optional: true
'@esbuild/darwin-x64@0.19.11':
optional: true
'@esbuild/darwin-x64@0.23.0':
optional: true
'@esbuild/freebsd-arm64@0.19.11':
optional: true
'@esbuild/freebsd-arm64@0.23.0':
optional: true
'@esbuild/freebsd-x64@0.19.11':
optional: true
'@esbuild/freebsd-x64@0.23.0':
optional: true
'@esbuild/linux-arm64@0.19.11':
optional: true
'@esbuild/linux-arm64@0.23.0':
optional: true
'@esbuild/linux-arm@0.19.11':
optional: true
'@esbuild/linux-arm@0.23.0':
optional: true
'@esbuild/linux-ia32@0.19.11':
optional: true
'@esbuild/linux-ia32@0.23.0':
optional: true
'@esbuild/linux-loong64@0.19.11':
optional: true
'@esbuild/linux-loong64@0.23.0':
optional: true
'@esbuild/linux-mips64el@0.19.11':
optional: true
'@esbuild/linux-mips64el@0.23.0':
optional: true
'@esbuild/linux-ppc64@0.19.11':
optional: true
'@esbuild/linux-ppc64@0.23.0':
optional: true
'@esbuild/linux-riscv64@0.19.11':
optional: true
'@esbuild/linux-riscv64@0.23.0':
optional: true
'@esbuild/linux-s390x@0.19.11':
optional: true
'@esbuild/linux-s390x@0.23.0':
optional: true
'@esbuild/linux-x64@0.19.11':
optional: true
'@esbuild/linux-x64@0.23.0':
optional: true
'@esbuild/netbsd-x64@0.19.11':
optional: true
'@esbuild/netbsd-x64@0.23.0':
optional: true
'@esbuild/openbsd-arm64@0.23.0':
optional: true
'@esbuild/openbsd-x64@0.19.11':
optional: true
'@esbuild/openbsd-x64@0.23.0':
optional: true
'@esbuild/sunos-x64@0.19.11':
optional: true
'@esbuild/sunos-x64@0.23.0':
optional: true
'@esbuild/win32-arm64@0.19.11':
optional: true
'@esbuild/win32-arm64@0.23.0':
optional: true
'@esbuild/win32-ia32@0.19.11':
optional: true
'@esbuild/win32-ia32@0.23.0':
optional: true
'@esbuild/win32-x64@0.19.11':
optional: true
'@esbuild/win32-x64@0.23.0':
optional: true
'@gar/promisify@1.1.3':
optional: true
@@ -1658,6 +1898,33 @@ snapshots:
'@esbuild/win32-ia32': 0.19.11
'@esbuild/win32-x64': 0.19.11
esbuild@0.23.0:
optionalDependencies:
'@esbuild/aix-ppc64': 0.23.0
'@esbuild/android-arm': 0.23.0
'@esbuild/android-arm64': 0.23.0
'@esbuild/android-x64': 0.23.0
'@esbuild/darwin-arm64': 0.23.0
'@esbuild/darwin-x64': 0.23.0
'@esbuild/freebsd-arm64': 0.23.0
'@esbuild/freebsd-x64': 0.23.0
'@esbuild/linux-arm': 0.23.0
'@esbuild/linux-arm64': 0.23.0
'@esbuild/linux-ia32': 0.23.0
'@esbuild/linux-loong64': 0.23.0
'@esbuild/linux-mips64el': 0.23.0
'@esbuild/linux-ppc64': 0.23.0
'@esbuild/linux-riscv64': 0.23.0
'@esbuild/linux-s390x': 0.23.0
'@esbuild/linux-x64': 0.23.0
'@esbuild/netbsd-x64': 0.23.0
'@esbuild/openbsd-arm64': 0.23.0
'@esbuild/openbsd-x64': 0.23.0
'@esbuild/sunos-x64': 0.23.0
'@esbuild/win32-arm64': 0.23.0
'@esbuild/win32-ia32': 0.23.0
'@esbuild/win32-x64': 0.23.0
escape-string-regexp@1.0.5: {}
eventemitter3@5.0.1: {}
@@ -1698,6 +1965,9 @@ snapshots:
fs.realpath@1.0.0:
optional: true
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {}
function.prototype.name@1.1.6:
@@ -1738,6 +2008,10 @@ snapshots:
call-bind: 1.0.5
get-intrinsic: 1.2.2
get-tsconfig@4.7.6:
dependencies:
resolve-pkg-maps: 1.0.0
github-from-package@0.0.0: {}
glob@7.2.3:
@@ -2256,6 +2530,8 @@ snapshots:
define-properties: 1.2.1
set-function-name: 2.0.1
resolve-pkg-maps@1.0.0: {}
resolve@1.22.8:
dependencies:
is-core-module: 2.13.1
@@ -2473,6 +2749,13 @@ snapshots:
mkdirp: 1.0.4
yallist: 4.0.0
tsx@4.17.0:
dependencies:
esbuild: 0.23.0
get-tsconfig: 4.7.6
optionalDependencies:
fsevents: 2.3.3
tunnel-agent@0.6.0:
dependencies:
safe-buffer: 5.2.1
+23 -22
View File
@@ -1,7 +1,6 @@
import { stockDatabase } from "./stockdb/clickhouse.js";
import { stockDatabase } from "./stockdb/lmdbx.js";
import { calendarDatabase } from "./calendardb/optiondb-lmdbx.js";
import type { CalendarKey } from "./calendardb/interfaces.js";
import type { Aggregate } from "./interfaces.js";
import { nextDate } from "./lib/utils/nextDate.js";
type BacktestInput = {
@@ -38,49 +37,51 @@ export async function backtest({
// for each minute of that day for which we have a stock candlestick:
for (const stockAggregate of stockAggregates) {
// console.log("Current Time:", new Date(stockAggregate.tsStart));
// filter-out calendars that are far-from-the-money (10%)
console.log("Current Date:", date, stockAggregate.tsStart);
// filter-out calendars that are far-from-the-money (5%)
const calendarsNearTheMoney = calendars.filter(
({ strike }) =>
Math.abs((stockAggregate.open - strike) / stockAggregate.open) < 0.1,
Math.abs((stockAggregate.open - strike) / stockAggregate.open) < 0.05
);
console.log(
"Current Date:",
new Intl.DateTimeFormat("en-US", {
timeZone: "America/New_York",
dateStyle: "full",
timeStyle: "long",
}).format(new Date(stockAggregate.tsStart)),
";",
`${calendarsNearTheMoney.length} Calendars Near The Money`
);
// for each relevant calendar on that day:
for (const calendar of calendarsNearTheMoney) {
const strikePercentageFromTheMoney = Math.abs(
(stockAggregate.open - calendar.strike) / stockAggregate.open,
(stockAggregate.open - calendar.strike) / stockAggregate.open
);
/** In days. */
const calendarSpan =
/** Number of days between the back and front expiration dates. */
const calendarSpanInDays =
(new Date(calendar.backExpirationDate).valueOf() -
new Date(calendar.frontExpirationDate).valueOf()) /
(1000 * 60 * 60 * 24);
const targetCalendarPrice =
await calendarDatabase.getTargetPriceByProbability({
symbol,
calendarSpan,
calendarSpan: calendarSpanInDays,
strikePercentageFromTheMoney,
historicalProbabilityOfSuccess,
});
const calendarAggregates = calendarDatabase.getAggregatesSync({
const calendarAggregateAtCurrentTime =
await calendarDatabase.getAggregate({
key: {
...calendar,
},
date,
tsStart: stockAggregate.tsStart,
});
// console.log(
// "Calendar Aggregates:",
// calendar,
// calendarAggregates.length,
// );
const calendarAggregateAtCurrentTime = calendarAggregates.find(
({ tsStart }) => tsStart === stockAggregate.tsStart,
);
// if there exists a matching calendar candlestick for the current minute:
if (calendarAggregateAtCurrentTime) {
// if the current candlestick is a good price (i.e. less than the target price):
const minCalendarPriceInCandlestick = Math.min(
calendarAggregateAtCurrentTime.open,
calendarAggregateAtCurrentTime.close,
calendarAggregateAtCurrentTime.close
);
if (
minCalendarPriceInCandlestick < targetCalendarPrice &&
@@ -99,7 +100,7 @@ export async function backtest({
minCalendarPriceInCandlestick * 100,
"...$",
buyingPower,
"left",
"left"
);
didBuyCalendar = true;
}
@@ -131,7 +132,7 @@ export async function backtest({
calendarClosingPrice,
"...$",
buyingPower,
"left",
"left"
);
}
}
+26 -13
View File
@@ -3,13 +3,11 @@ import { open } from "lmdbx";
const calendarAggregatesDb = open({
path: "./calendar-aggregates.db",
// any options go here, we can turn on compression like this:
compression: true,
});
const calendarExistenceDb = open({
path: "./calendar-existence.db",
// any options go here, we can turn on compression like this:
compression: true,
});
@@ -65,6 +63,19 @@ function makeCalendarDatabase(): CalendarDatabase {
low: value.low,
})).asArray;
},
getAggregate: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
tsStart,
}) => {
return await calendarAggregatesDb.get([
symbol,
frontExpirationDate,
backExpirationDate,
strike,
type,
tsStart,
]);
},
insertAggregates: async (aggregates) => {
await calendarExistenceDb.batch(() => {
for (const aggregate of aggregates) {
@@ -77,7 +88,7 @@ function makeCalendarDatabase(): CalendarDatabase {
aggregate.key.strike,
aggregate.key.type,
],
null,
null
);
}
});
@@ -97,7 +108,7 @@ function makeCalendarDatabase(): CalendarDatabase {
close: aggregate.close,
high: aggregate.high,
low: aggregate.low,
},
}
);
}
});
@@ -105,11 +116,12 @@ function makeCalendarDatabase(): CalendarDatabase {
getClosingPrice: async ({
key: { symbol, strike, type, frontExpirationDate, backExpirationDate },
}) => {
const startOfLastHourUnix = new Date(
`${frontExpirationDate}T00:00:00Z`,
const startOfExpirationDateUnix = new Date(
`${frontExpirationDate}T23:59:59Z`
).valueOf();
const endOfExpirationDateUnix = new Date(
`${frontExpirationDate}T00:00:00Z`
).valueOf();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
let minPrice = 0;
for (const { value } of calendarAggregatesDb.getRange({
start: [
symbol,
@@ -117,7 +129,7 @@ function makeCalendarDatabase(): CalendarDatabase {
backExpirationDate,
strike,
type,
startOfLastHourUnix,
startOfExpirationDateUnix,
],
end: [
symbol,
@@ -125,14 +137,15 @@ function makeCalendarDatabase(): CalendarDatabase {
backExpirationDate,
strike,
type,
endOfLastHourUnix,
endOfExpirationDateUnix,
],
reverse: true,
})) {
if (value.close < minPrice || minPrice === 0) {
minPrice = value.close;
if (value.close > 0) {
return value.close;
}
}
return minPrice;
return 0;
},
getTargetPriceByProbability: async ({
symbol,
+50 -7
View File
@@ -68,14 +68,22 @@ function makeCalendarDatabase(): CalendarDatabase {
return optionContracts.flatMap(
(frontOptionContract, i, optionContracts) =>
optionContracts
.filter((_, j) => i !== j)
.filter(
(potientialBackOptionContract) =>
frontOptionContract.strike ===
potientialBackOptionContract.strike &&
frontOptionContract.type ===
potientialBackOptionContract.type &&
frontOptionContract.expirationDate <
potientialBackOptionContract.expirationDate
)
.map((backOptionContract) => ({
symbol,
frontExpirationDate: frontOptionContract.expirationDate,
backExpirationDate: backOptionContract.expirationDate,
strike: frontOptionContract.strike,
type: frontOptionContract.type,
})),
}))
);
},
getAggregates: async ({
@@ -93,9 +101,10 @@ function makeCalendarDatabase(): CalendarDatabase {
getClosingPrice: async ({
key: { symbol, strike, type, frontExpirationDate, backExpirationDate },
}) => {
// get unix timestamp, in milliseconds, of the start of the last hour, which is 03:30PM in the `America/New_York` timezone on the front expiration date:
const startOfLastHourUnix = new Date(
`${frontExpirationDate}T00:00:00Z`,
).valueOf();
`${frontExpirationDate}T19:30:00Z`
).getTime();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
const frontOptionContractAggregates = (
await optionContractDatabase.getAggregates({
@@ -104,7 +113,7 @@ function makeCalendarDatabase(): CalendarDatabase {
})
).filter(
({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix,
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix
);
const backOptionContractAggregates = (
await optionContractDatabase.getAggregates({
@@ -113,7 +122,7 @@ function makeCalendarDatabase(): CalendarDatabase {
})
).filter(
({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix,
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix
);
let i = 0;
let j = 0;
@@ -128,7 +137,7 @@ function makeCalendarDatabase(): CalendarDatabase {
) {
const calendarClosePrice =
backOptionContractAggregates[j].close -
frontOptionContractAggregates[j].close;
frontOptionContractAggregates[i].close;
if (calendarClosePrice < minPrice || minPrice === 0) {
minPrice = calendarClosePrice;
}
@@ -145,6 +154,40 @@ function makeCalendarDatabase(): CalendarDatabase {
}
return minPrice;
},
getAggregate: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
tsStart,
}) => {
const [frontOptionContractAggregate, backOptionContractAggregate] =
await Promise.all([
optionContractDatabase.getAggregate({
key: { symbol, expirationDate: frontExpirationDate, strike, type },
tsStart,
}),
optionContractDatabase.getAggregate({
key: { symbol, expirationDate: backExpirationDate, strike, type },
tsStart,
}),
]);
// only return the calendar aggregate if its constituent front and back option contract aggregates exist:
if (frontOptionContractAggregate && backOptionContractAggregate) {
return {
tsStart,
open:
backOptionContractAggregate.open -
frontOptionContractAggregate.open,
close:
backOptionContractAggregate.close -
frontOptionContractAggregate.close,
high:
backOptionContractAggregate.high -
frontOptionContractAggregate.high,
low:
backOptionContractAggregate.low - frontOptionContractAggregate.low,
};
}
return undefined;
},
getTargetPriceByProbability: async ({
symbol,
calendarSpan,
+20 -3
View File
@@ -15,15 +15,32 @@ export type AggregateDatabase<T> = {
getKeys: ({
key,
date,
}: { key?: T | Partial<T>; date?: string }) => Promise<Array<T>>;
}: {
key?: T | Partial<T>;
date?: string;
}) => Promise<Array<T>>;
getAggregates: ({
key,
date,
}: { key: T; date: string }) => Promise<Array<Omit<Aggregate<T>, "key">>>;
}: {
key: T;
date: string;
}) => Promise<Array<Omit<Aggregate<T>, "key">>>;
/** Since an aggregate may not exist at the specified `tsStart`, return `undefined` if it doesn't exist. */
getAggregate: ({
key,
tsStart,
}: {
key: T;
tsStart: number;
}) => Promise<Omit<Aggregate<T>, "key"> | undefined>;
getAggregatesSync?: ({
key,
date,
}: { key: T; date: string }) => Array<Omit<Aggregate<T>, "key">>;
}: {
key: T;
date: string;
}) => Array<Omit<Aggregate<T>, "key">>;
insertAggregates: (aggregates: Array<Aggregate<T>>) => Promise<void>;
getClosingPrice: ({ key }: { key: T }) => Promise<number>;
};
+17 -5
View File
@@ -28,8 +28,8 @@ function makeOptionContractDatabase(): OptionContractDatabase {
start: [symbol, expirationDate, strike, type, startOfDayUnix],
end: [symbol, expirationDate, strike, type, endOfDayUnix],
})
.map(({ value }) => ({
tsStart: value.tsStart,
.map(({ key, value }) => ({
tsStart: key[4],
open: value.open,
close: value.close,
high: value.high,
@@ -73,7 +73,7 @@ function makeOptionContractDatabase(): OptionContractDatabase {
aggregate.key.strike,
aggregate.key.type,
],
null,
null
);
}
});
@@ -92,7 +92,7 @@ function makeOptionContractDatabase(): OptionContractDatabase {
close: aggregate.close,
high: aggregate.high,
low: aggregate.low,
},
}
);
}
});
@@ -101,7 +101,7 @@ function makeOptionContractDatabase(): OptionContractDatabase {
key: { symbol, strike, type, expirationDate },
}) => {
const startOfLastHourUnix = new Date(
`${expirationDate}T00:00:00Z`,
`${expirationDate}T00:00:00Z`
).valueOf();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
let minPrice = 0;
@@ -115,6 +115,18 @@ function makeOptionContractDatabase(): OptionContractDatabase {
}
return minPrice;
},
getAggregate: async ({
key: { symbol, expirationDate, strike, type },
tsStart,
}) => {
return await optionContractAggregatesDb.get([
symbol,
expirationDate,
strike,
type,
tsStart,
]);
},
};
return {