Compare commits

..

7 Commits

9 changed files with 548 additions and 175 deletions
+2
View File
@@ -1,2 +1,4 @@
.pnpm-store .pnpm-store
node_modules node_modules
.aider*
.env
+1
View File
@@ -1 +1,2 @@
nodejs 20.15.1 nodejs 20.15.1
python 3.12.4
+1
View File
@@ -28,6 +28,7 @@
"@types/node": "^20.10.7", "@types/node": "^20.10.7",
"esbuild": "^0.19.11", "esbuild": "^0.19.11",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"tsx": "^4.17.0",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
} }
+283
View File
@@ -63,6 +63,9 @@ importers:
npm-run-all: npm-run-all:
specifier: ^4.1.5 specifier: ^4.1.5
version: 4.1.5 version: 4.1.5
tsx:
specifier: ^4.17.0
version: 4.17.0
typescript: typescript:
specifier: ^5.3.3 specifier: ^5.3.3
version: 5.3.3 version: 5.3.3
@@ -82,138 +85,282 @@ packages:
cpu: [ppc64] cpu: [ppc64]
os: [aix] 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': '@esbuild/android-arm64@0.19.11':
resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==} resolution: {integrity: sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [android] 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': '@esbuild/android-arm@0.19.11':
resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==} resolution: {integrity: sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm] cpu: [arm]
os: [android] 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': '@esbuild/android-x64@0.19.11':
resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==} resolution: {integrity: sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [android] 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': '@esbuild/darwin-arm64@0.19.11':
resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==} resolution: {integrity: sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [darwin] 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': '@esbuild/darwin-x64@0.19.11':
resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==} resolution: {integrity: sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [darwin] 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': '@esbuild/freebsd-arm64@0.19.11':
resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==} resolution: {integrity: sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [freebsd] 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': '@esbuild/freebsd-x64@0.19.11':
resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==} resolution: {integrity: sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [freebsd] 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': '@esbuild/linux-arm64@0.19.11':
resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==} resolution: {integrity: sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [linux] 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': '@esbuild/linux-arm@0.19.11':
resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==} resolution: {integrity: sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm] cpu: [arm]
os: [linux] 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': '@esbuild/linux-ia32@0.19.11':
resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==} resolution: {integrity: sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [ia32] cpu: [ia32]
os: [linux] 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': '@esbuild/linux-loong64@0.19.11':
resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==} resolution: {integrity: sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [loong64] cpu: [loong64]
os: [linux] 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': '@esbuild/linux-mips64el@0.19.11':
resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==} resolution: {integrity: sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [mips64el] cpu: [mips64el]
os: [linux] 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': '@esbuild/linux-ppc64@0.19.11':
resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==} resolution: {integrity: sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [ppc64] cpu: [ppc64]
os: [linux] 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': '@esbuild/linux-riscv64@0.19.11':
resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==} resolution: {integrity: sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [riscv64] cpu: [riscv64]
os: [linux] 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': '@esbuild/linux-s390x@0.19.11':
resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==} resolution: {integrity: sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [s390x] cpu: [s390x]
os: [linux] 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': '@esbuild/linux-x64@0.19.11':
resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==} resolution: {integrity: sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [linux] 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': '@esbuild/netbsd-x64@0.19.11':
resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==} resolution: {integrity: sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [netbsd] 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': '@esbuild/openbsd-x64@0.19.11':
resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==} resolution: {integrity: sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [openbsd] 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': '@esbuild/sunos-x64@0.19.11':
resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==} resolution: {integrity: sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [sunos] 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': '@esbuild/win32-arm64@0.19.11':
resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==} resolution: {integrity: sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [arm64] cpu: [arm64]
os: [win32] 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': '@esbuild/win32-ia32@0.19.11':
resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==} resolution: {integrity: sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [ia32] cpu: [ia32]
os: [win32] 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': '@esbuild/win32-x64@0.19.11':
resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==} resolution: {integrity: sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==}
engines: {node: '>=12'} engines: {node: '>=12'}
cpu: [x64] cpu: [x64]
os: [win32] 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': '@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==} resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
@@ -462,6 +609,11 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
hasBin: true hasBin: true
esbuild@0.23.0:
resolution: {integrity: sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==}
engines: {node: '>=18'}
hasBin: true
escape-string-regexp@1.0.5: escape-string-regexp@1.0.5:
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
engines: {node: '>=0.8.0'} engines: {node: '>=0.8.0'}
@@ -497,6 +649,11 @@ packages:
fs.realpath@1.0.0: fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 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: function-bind@1.1.2:
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
@@ -523,6 +680,9 @@ packages:
resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==} resolution: {integrity: sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
get-tsconfig@4.7.6:
resolution: {integrity: sha512-ZAqrLlu18NbDdRaHq+AKXzAmqIUPswPWKUchfytdAjiRFnCe5ojG2bstg6mRiZabkKfCoL/e98pbBELIV/YCeA==}
github-from-package@0.0.0: github-from-package@0.0.0:
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
@@ -986,6 +1146,9 @@ packages:
resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==} resolution: {integrity: sha512-sy6TXMN+hnP/wMy+ISxg3krXx7BAtWVO4UouuCN/ziM9UEne0euamVNafDfvC83bRNr95y0V5iijeDQFUNpvrg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
resolve@1.22.8: resolve@1.22.8:
resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==}
hasBin: true hasBin: true
@@ -1164,6 +1327,11 @@ packages:
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
engines: {node: '>=10'} 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: tunnel-agent@0.6.0:
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
@@ -1252,72 +1420,144 @@ snapshots:
'@esbuild/aix-ppc64@0.19.11': '@esbuild/aix-ppc64@0.19.11':
optional: true optional: true
'@esbuild/aix-ppc64@0.23.0':
optional: true
'@esbuild/android-arm64@0.19.11': '@esbuild/android-arm64@0.19.11':
optional: true optional: true
'@esbuild/android-arm64@0.23.0':
optional: true
'@esbuild/android-arm@0.19.11': '@esbuild/android-arm@0.19.11':
optional: true optional: true
'@esbuild/android-arm@0.23.0':
optional: true
'@esbuild/android-x64@0.19.11': '@esbuild/android-x64@0.19.11':
optional: true optional: true
'@esbuild/android-x64@0.23.0':
optional: true
'@esbuild/darwin-arm64@0.19.11': '@esbuild/darwin-arm64@0.19.11':
optional: true optional: true
'@esbuild/darwin-arm64@0.23.0':
optional: true
'@esbuild/darwin-x64@0.19.11': '@esbuild/darwin-x64@0.19.11':
optional: true optional: true
'@esbuild/darwin-x64@0.23.0':
optional: true
'@esbuild/freebsd-arm64@0.19.11': '@esbuild/freebsd-arm64@0.19.11':
optional: true optional: true
'@esbuild/freebsd-arm64@0.23.0':
optional: true
'@esbuild/freebsd-x64@0.19.11': '@esbuild/freebsd-x64@0.19.11':
optional: true optional: true
'@esbuild/freebsd-x64@0.23.0':
optional: true
'@esbuild/linux-arm64@0.19.11': '@esbuild/linux-arm64@0.19.11':
optional: true optional: true
'@esbuild/linux-arm64@0.23.0':
optional: true
'@esbuild/linux-arm@0.19.11': '@esbuild/linux-arm@0.19.11':
optional: true optional: true
'@esbuild/linux-arm@0.23.0':
optional: true
'@esbuild/linux-ia32@0.19.11': '@esbuild/linux-ia32@0.19.11':
optional: true optional: true
'@esbuild/linux-ia32@0.23.0':
optional: true
'@esbuild/linux-loong64@0.19.11': '@esbuild/linux-loong64@0.19.11':
optional: true optional: true
'@esbuild/linux-loong64@0.23.0':
optional: true
'@esbuild/linux-mips64el@0.19.11': '@esbuild/linux-mips64el@0.19.11':
optional: true optional: true
'@esbuild/linux-mips64el@0.23.0':
optional: true
'@esbuild/linux-ppc64@0.19.11': '@esbuild/linux-ppc64@0.19.11':
optional: true optional: true
'@esbuild/linux-ppc64@0.23.0':
optional: true
'@esbuild/linux-riscv64@0.19.11': '@esbuild/linux-riscv64@0.19.11':
optional: true optional: true
'@esbuild/linux-riscv64@0.23.0':
optional: true
'@esbuild/linux-s390x@0.19.11': '@esbuild/linux-s390x@0.19.11':
optional: true optional: true
'@esbuild/linux-s390x@0.23.0':
optional: true
'@esbuild/linux-x64@0.19.11': '@esbuild/linux-x64@0.19.11':
optional: true optional: true
'@esbuild/linux-x64@0.23.0':
optional: true
'@esbuild/netbsd-x64@0.19.11': '@esbuild/netbsd-x64@0.19.11':
optional: true optional: true
'@esbuild/netbsd-x64@0.23.0':
optional: true
'@esbuild/openbsd-arm64@0.23.0':
optional: true
'@esbuild/openbsd-x64@0.19.11': '@esbuild/openbsd-x64@0.19.11':
optional: true optional: true
'@esbuild/openbsd-x64@0.23.0':
optional: true
'@esbuild/sunos-x64@0.19.11': '@esbuild/sunos-x64@0.19.11':
optional: true optional: true
'@esbuild/sunos-x64@0.23.0':
optional: true
'@esbuild/win32-arm64@0.19.11': '@esbuild/win32-arm64@0.19.11':
optional: true optional: true
'@esbuild/win32-arm64@0.23.0':
optional: true
'@esbuild/win32-ia32@0.19.11': '@esbuild/win32-ia32@0.19.11':
optional: true optional: true
'@esbuild/win32-ia32@0.23.0':
optional: true
'@esbuild/win32-x64@0.19.11': '@esbuild/win32-x64@0.19.11':
optional: true optional: true
'@esbuild/win32-x64@0.23.0':
optional: true
'@gar/promisify@1.1.3': '@gar/promisify@1.1.3':
optional: true optional: true
@@ -1658,6 +1898,33 @@ snapshots:
'@esbuild/win32-ia32': 0.19.11 '@esbuild/win32-ia32': 0.19.11
'@esbuild/win32-x64': 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: {} escape-string-regexp@1.0.5: {}
eventemitter3@5.0.1: {} eventemitter3@5.0.1: {}
@@ -1698,6 +1965,9 @@ snapshots:
fs.realpath@1.0.0: fs.realpath@1.0.0:
optional: true optional: true
fsevents@2.3.3:
optional: true
function-bind@1.1.2: {} function-bind@1.1.2: {}
function.prototype.name@1.1.6: function.prototype.name@1.1.6:
@@ -1738,6 +2008,10 @@ snapshots:
call-bind: 1.0.5 call-bind: 1.0.5
get-intrinsic: 1.2.2 get-intrinsic: 1.2.2
get-tsconfig@4.7.6:
dependencies:
resolve-pkg-maps: 1.0.0
github-from-package@0.0.0: {} github-from-package@0.0.0: {}
glob@7.2.3: glob@7.2.3:
@@ -2256,6 +2530,8 @@ snapshots:
define-properties: 1.2.1 define-properties: 1.2.1
set-function-name: 2.0.1 set-function-name: 2.0.1
resolve-pkg-maps@1.0.0: {}
resolve@1.22.8: resolve@1.22.8:
dependencies: dependencies:
is-core-module: 2.13.1 is-core-module: 2.13.1
@@ -2473,6 +2749,13 @@ snapshots:
mkdirp: 1.0.4 mkdirp: 1.0.4
yallist: 4.0.0 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: tunnel-agent@0.6.0:
dependencies: dependencies:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
+24 -23
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 { calendarDatabase } from "./calendardb/optiondb-lmdbx.js";
import type { CalendarKey } from "./calendardb/interfaces.js"; import type { CalendarKey } from "./calendardb/interfaces.js";
import type { Aggregate } from "./interfaces.js";
import { nextDate } from "./lib/utils/nextDate.js"; import { nextDate } from "./lib/utils/nextDate.js";
type BacktestInput = { type BacktestInput = {
@@ -38,49 +37,51 @@ export async function backtest({
// for each minute of that day for which we have a stock candlestick: // for each minute of that day for which we have a stock candlestick:
for (const stockAggregate of stockAggregates) { for (const stockAggregate of stockAggregates) {
// console.log("Current Time:", new Date(stockAggregate.tsStart)); // console.log("Current Time:", new Date(stockAggregate.tsStart));
// filter-out calendars that are far-from-the-money (10%) // filter-out calendars that are far-from-the-money (5%)
console.log("Current Date:", date, stockAggregate.tsStart);
const calendarsNearTheMoney = calendars.filter( const calendarsNearTheMoney = calendars.filter(
({ strike }) => ({ 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 each relevant calendar on that day:
for (const calendar of calendarsNearTheMoney) { for (const calendar of calendarsNearTheMoney) {
const strikePercentageFromTheMoney = Math.abs( const strikePercentageFromTheMoney = Math.abs(
(stockAggregate.open - calendar.strike) / stockAggregate.open, (stockAggregate.open - calendar.strike) / stockAggregate.open
); );
/** In days. */ /** Number of days between the back and front expiration dates. */
const calendarSpan = const calendarSpanInDays =
(new Date(calendar.backExpirationDate).valueOf() - (new Date(calendar.backExpirationDate).valueOf() -
new Date(calendar.frontExpirationDate).valueOf()) / new Date(calendar.frontExpirationDate).valueOf()) /
(1000 * 60 * 60 * 24); (1000 * 60 * 60 * 24);
const targetCalendarPrice = const targetCalendarPrice =
await calendarDatabase.getTargetPriceByProbability({ await calendarDatabase.getTargetPriceByProbability({
symbol, symbol,
calendarSpan, calendarSpan: calendarSpanInDays,
strikePercentageFromTheMoney, strikePercentageFromTheMoney,
historicalProbabilityOfSuccess, historicalProbabilityOfSuccess,
}); });
const calendarAggregates = calendarDatabase.getAggregatesSync({ const calendarAggregateAtCurrentTime =
await calendarDatabase.getAggregate({
key: { key: {
...calendar, ...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 there exists a matching calendar candlestick for the current minute:
if (calendarAggregateAtCurrentTime) { if (calendarAggregateAtCurrentTime) {
// if the current candlestick is a good price (i.e. less than the target price): // if the current candlestick is a good price (i.e. less than the target price):
const minCalendarPriceInCandlestick = Math.min( const minCalendarPriceInCandlestick = Math.min(
calendarAggregateAtCurrentTime.open, calendarAggregateAtCurrentTime.open,
calendarAggregateAtCurrentTime.close, calendarAggregateAtCurrentTime.close
); );
if ( if (
minCalendarPriceInCandlestick < targetCalendarPrice && minCalendarPriceInCandlestick < targetCalendarPrice &&
@@ -99,7 +100,7 @@ export async function backtest({
minCalendarPriceInCandlestick * 100, minCalendarPriceInCandlestick * 100,
"...$", "...$",
buyingPower, buyingPower,
"left", "left"
); );
didBuyCalendar = true; didBuyCalendar = true;
} }
@@ -131,7 +132,7 @@ export async function backtest({
calendarClosingPrice, calendarClosingPrice,
"...$", "...$",
buyingPower, buyingPower,
"left", "left"
); );
} }
} }
+149 -136
View File
@@ -2,152 +2,165 @@ import type { CalendarDatabase } from "./interfaces.js";
import { open } from "lmdbx"; import { open } from "lmdbx";
const calendarAggregatesDb = open({ const calendarAggregatesDb = open({
path: "./calendar-aggregates.db", path: "./calendar-aggregates.db",
// any options go here, we can turn on compression like this: compression: true,
compression: true,
}); });
const calendarExistenceDb = open({ const calendarExistenceDb = open({
path: "./calendar-existence.db", path: "./calendar-existence.db",
// any options go here, we can turn on compression like this: compression: true,
compression: true,
}); });
/** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */ /** Largest possible key according to the `ordered-binary` (used by lmdbx) docs. */
const MAXIMUM_KEY = Buffer.from([0xff]); const MAXIMUM_KEY = Buffer.from([0xff]);
function makeCalendarDatabase(): CalendarDatabase { function makeCalendarDatabase(): CalendarDatabase {
const calendarDatabase: Omit<CalendarDatabase, "getCalendars"> = { const calendarDatabase: Omit<CalendarDatabase, "getCalendars"> = {
getKeys: async ({ key: { symbol }, date }) => { getKeys: async ({ key: { symbol }, date }) => {
return calendarExistenceDb return calendarExistenceDb
.getRange({ .getRange({
start: [date, symbol], start: [date, symbol],
end: [date, symbol, MAXIMUM_KEY], end: [date, symbol, MAXIMUM_KEY],
}) })
.map(({ key }) => ({ .map(({ key }) => ({
symbol, symbol,
frontExpirationDate: key[2], frontExpirationDate: key[2],
backExpirationDate: key[3], backExpirationDate: key[3],
strike: key[4], strike: key[4],
type: key[5], type: key[5],
})).asArray; })).asArray;
}, },
getAggregates: async ({ getAggregates: async ({
key: { symbol, frontExpirationDate, backExpirationDate, strike, type }, key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
date, date,
}) => { }) => {
const startOfDayUnix = new Date(`${date}T00:00:00Z`).valueOf(); const startOfDayUnix = new Date(`${date}T00:00:00Z`).valueOf();
const endOfDayUnix = startOfDayUnix + 3600 * 24 * 1000; const endOfDayUnix = startOfDayUnix + 3600 * 24 * 1000;
return calendarAggregatesDb return calendarAggregatesDb
.getRange({ .getRange({
start: [ start: [
symbol, symbol,
frontExpirationDate, frontExpirationDate,
backExpirationDate, backExpirationDate,
strike, strike,
type, type,
startOfDayUnix, startOfDayUnix,
], ],
end: [ end: [
symbol, symbol,
frontExpirationDate, frontExpirationDate,
backExpirationDate, backExpirationDate,
strike, strike,
type, type,
endOfDayUnix, endOfDayUnix,
], ],
}) })
.map(({ value }) => ({ .map(({ value }) => ({
tsStart: value.tsStart, tsStart: value.tsStart,
open: value.open, open: value.open,
close: value.close, close: value.close,
high: value.high, high: value.high,
low: value.low, low: value.low,
})).asArray; })).asArray;
}, },
insertAggregates: async (aggregates) => { getAggregate: async ({
await calendarExistenceDb.batch(() => { key: { symbol, frontExpirationDate, backExpirationDate, strike, type },
for (const aggregate of aggregates) { tsStart,
calendarExistenceDb.put( }) => {
[ return await calendarAggregatesDb.get([
new Date(aggregate.tsStart).toISOString().substring(0, 10), symbol,
aggregate.key.symbol, frontExpirationDate,
aggregate.key.frontExpirationDate, backExpirationDate,
aggregate.key.backExpirationDate, strike,
aggregate.key.strike, type,
aggregate.key.type, tsStart,
], ]);
null, },
); insertAggregates: async (aggregates) => {
} await calendarExistenceDb.batch(() => {
}); for (const aggregate of aggregates) {
await calendarAggregatesDb.batch(() => { calendarExistenceDb.put(
for (const aggregate of aggregates) { [
calendarAggregatesDb.put( new Date(aggregate.tsStart).toISOString().substring(0, 10),
[ aggregate.key.symbol,
aggregate.key.symbol, aggregate.key.frontExpirationDate,
aggregate.key.frontExpirationDate, aggregate.key.backExpirationDate,
aggregate.key.backExpirationDate, aggregate.key.strike,
aggregate.key.strike, aggregate.key.type,
aggregate.key.type, ],
aggregate.tsStart, null
], );
{ }
open: aggregate.open, });
close: aggregate.close, await calendarAggregatesDb.batch(() => {
high: aggregate.high, for (const aggregate of aggregates) {
low: aggregate.low, calendarAggregatesDb.put(
}, [
); aggregate.key.symbol,
} aggregate.key.frontExpirationDate,
}); aggregate.key.backExpirationDate,
}, aggregate.key.strike,
getClosingPrice: async ({ aggregate.key.type,
key: { symbol, strike, type, frontExpirationDate, backExpirationDate }, aggregate.tsStart,
}) => { ],
const startOfLastHourUnix = new Date( {
`${frontExpirationDate}T00:00:00Z`, open: aggregate.open,
).valueOf(); close: aggregate.close,
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000; high: aggregate.high,
let minPrice = 0; low: aggregate.low,
for (const { value } of calendarAggregatesDb.getRange({ }
start: [ );
symbol, }
frontExpirationDate, });
backExpirationDate, },
strike, getClosingPrice: async ({
type, key: { symbol, strike, type, frontExpirationDate, backExpirationDate },
startOfLastHourUnix, }) => {
], const startOfExpirationDateUnix = new Date(
end: [ `${frontExpirationDate}T23:59:59Z`
symbol, ).valueOf();
frontExpirationDate, const endOfExpirationDateUnix = new Date(
backExpirationDate, `${frontExpirationDate}T00:00:00Z`
strike, ).valueOf();
type, for (const { value } of calendarAggregatesDb.getRange({
endOfLastHourUnix, start: [
], symbol,
})) { frontExpirationDate,
if (value.close < minPrice || minPrice === 0) { backExpirationDate,
minPrice = value.close; strike,
} type,
} startOfExpirationDateUnix,
return minPrice; ],
}, end: [
getTargetPriceByProbability: async ({ symbol,
symbol, frontExpirationDate,
calendarSpan, backExpirationDate,
strikePercentageFromTheMoney, strike,
historicalProbabilityOfSuccess, type,
}) => { endOfExpirationDateUnix,
return 0.24; ],
}, reverse: true,
}; })) {
if (value.close > 0) {
return value.close;
}
}
return 0;
},
getTargetPriceByProbability: async ({
symbol,
calendarSpan,
strikePercentageFromTheMoney,
historicalProbabilityOfSuccess,
}) => {
return 0.24;
},
};
return { return {
...calendarDatabase, ...calendarDatabase,
getCalendars: calendarDatabase.getKeys, getCalendars: calendarDatabase.getKeys,
}; };
} }
export const calendarDatabase: CalendarDatabase = makeCalendarDatabase(); export const calendarDatabase: CalendarDatabase = makeCalendarDatabase();
+50 -7
View File
@@ -68,14 +68,22 @@ function makeCalendarDatabase(): CalendarDatabase {
return optionContracts.flatMap( return optionContracts.flatMap(
(frontOptionContract, i, optionContracts) => (frontOptionContract, i, optionContracts) =>
optionContracts optionContracts
.filter((_, j) => i !== j) .filter(
(potientialBackOptionContract) =>
frontOptionContract.strike ===
potientialBackOptionContract.strike &&
frontOptionContract.type ===
potientialBackOptionContract.type &&
frontOptionContract.expirationDate <
potientialBackOptionContract.expirationDate
)
.map((backOptionContract) => ({ .map((backOptionContract) => ({
symbol, symbol,
frontExpirationDate: frontOptionContract.expirationDate, frontExpirationDate: frontOptionContract.expirationDate,
backExpirationDate: backOptionContract.expirationDate, backExpirationDate: backOptionContract.expirationDate,
strike: frontOptionContract.strike, strike: frontOptionContract.strike,
type: frontOptionContract.type, type: frontOptionContract.type,
})), }))
); );
}, },
getAggregates: async ({ getAggregates: async ({
@@ -93,9 +101,10 @@ function makeCalendarDatabase(): CalendarDatabase {
getClosingPrice: async ({ getClosingPrice: async ({
key: { symbol, strike, type, frontExpirationDate, backExpirationDate }, 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( const startOfLastHourUnix = new Date(
`${frontExpirationDate}T00:00:00Z`, `${frontExpirationDate}T19:30:00Z`
).valueOf(); ).getTime();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000; const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
const frontOptionContractAggregates = ( const frontOptionContractAggregates = (
await optionContractDatabase.getAggregates({ await optionContractDatabase.getAggregates({
@@ -104,7 +113,7 @@ function makeCalendarDatabase(): CalendarDatabase {
}) })
).filter( ).filter(
({ tsStart }) => ({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix, tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix
); );
const backOptionContractAggregates = ( const backOptionContractAggregates = (
await optionContractDatabase.getAggregates({ await optionContractDatabase.getAggregates({
@@ -113,7 +122,7 @@ function makeCalendarDatabase(): CalendarDatabase {
}) })
).filter( ).filter(
({ tsStart }) => ({ tsStart }) =>
tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix, tsStart >= startOfLastHourUnix && tsStart < endOfLastHourUnix
); );
let i = 0; let i = 0;
let j = 0; let j = 0;
@@ -128,7 +137,7 @@ function makeCalendarDatabase(): CalendarDatabase {
) { ) {
const calendarClosePrice = const calendarClosePrice =
backOptionContractAggregates[j].close - backOptionContractAggregates[j].close -
frontOptionContractAggregates[j].close; frontOptionContractAggregates[i].close;
if (calendarClosePrice < minPrice || minPrice === 0) { if (calendarClosePrice < minPrice || minPrice === 0) {
minPrice = calendarClosePrice; minPrice = calendarClosePrice;
} }
@@ -145,6 +154,40 @@ function makeCalendarDatabase(): CalendarDatabase {
} }
return minPrice; 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 ({ getTargetPriceByProbability: async ({
symbol, symbol,
calendarSpan, calendarSpan,
+20 -3
View File
@@ -15,15 +15,32 @@ export type AggregateDatabase<T> = {
getKeys: ({ getKeys: ({
key, key,
date, date,
}: { key?: T | Partial<T>; date?: string }) => Promise<Array<T>>; }: {
key?: T | Partial<T>;
date?: string;
}) => Promise<Array<T>>;
getAggregates: ({ getAggregates: ({
key, key,
date, 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?: ({ getAggregatesSync?: ({
key, key,
date, 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>; insertAggregates: (aggregates: Array<Aggregate<T>>) => Promise<void>;
getClosingPrice: ({ key }: { key: T }) => Promise<number>; getClosingPrice: ({ key }: { key: T }) => Promise<number>;
}; };
+17 -5
View File
@@ -28,8 +28,8 @@ function makeOptionContractDatabase(): OptionContractDatabase {
start: [symbol, expirationDate, strike, type, startOfDayUnix], start: [symbol, expirationDate, strike, type, startOfDayUnix],
end: [symbol, expirationDate, strike, type, endOfDayUnix], end: [symbol, expirationDate, strike, type, endOfDayUnix],
}) })
.map(({ value }) => ({ .map(({ key, value }) => ({
tsStart: value.tsStart, tsStart: key[4],
open: value.open, open: value.open,
close: value.close, close: value.close,
high: value.high, high: value.high,
@@ -73,7 +73,7 @@ function makeOptionContractDatabase(): OptionContractDatabase {
aggregate.key.strike, aggregate.key.strike,
aggregate.key.type, aggregate.key.type,
], ],
null, null
); );
} }
}); });
@@ -92,7 +92,7 @@ function makeOptionContractDatabase(): OptionContractDatabase {
close: aggregate.close, close: aggregate.close,
high: aggregate.high, high: aggregate.high,
low: aggregate.low, low: aggregate.low,
}, }
); );
} }
}); });
@@ -101,7 +101,7 @@ function makeOptionContractDatabase(): OptionContractDatabase {
key: { symbol, strike, type, expirationDate }, key: { symbol, strike, type, expirationDate },
}) => { }) => {
const startOfLastHourUnix = new Date( const startOfLastHourUnix = new Date(
`${expirationDate}T00:00:00Z`, `${expirationDate}T00:00:00Z`
).valueOf(); ).valueOf();
const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000; const endOfLastHourUnix = startOfLastHourUnix + 3600 * 1000;
let minPrice = 0; let minPrice = 0;
@@ -115,6 +115,18 @@ function makeOptionContractDatabase(): OptionContractDatabase {
} }
return minPrice; return minPrice;
}, },
getAggregate: async ({
key: { symbol, expirationDate, strike, type },
tsStart,
}) => {
return await optionContractAggregatesDb.get([
symbol,
expirationDate,
strike,
type,
tsStart,
]);
},
}; };
return { return {