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/.
|
node_modules/
|
||||||
# Feedstock configuration goes in `conda-forge.yml`
|
dist/
|
||||||
# Everything else is managed by the conda-smithy rerender process.
|
.env
|
||||||
# Please do not modify
|
*.log
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|||||||
@@ -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
|
## My Polymarket account
|
||||||
|
<img width="815" height="291" alt="image" src="https://github.com/user-attachments/assets/0971eb6d-de7e-4f9a-82a1-14a2163db209" />
|
||||||
Summary: Gdptools
|
|
||||||
|
|
||||||
Current build status
|
|
||||||
====================
|
|
||||||
|
|
||||||
|
|
||||||
<table><tr><td>All platforms:</td>
|
| | |
|
||||||
<td>
|
|---|---|
|
||||||
<a href="https://dev.azure.com/conda-forge/feedstock-builds/_build/latest?definitionId=17168&branchName=main">
|
| **Profile** | [@odahoa](https://polymarket.com/@odahoa?tab=activity) |
|
||||||
<img src="https://dev.azure.com/conda-forge/feedstock-builds/_apis/build/status/gdptools-feedstock?branchName=main">
|
| **Username** | `odahoa` |
|
||||||
</a>
|
| **Proxy wallet** | `0xe2511c9e41c5e762887e538b1d6e7221807aa237` |
|
||||||
</td>
|
| **Markets** | `btc-updown-15m`, `eth-updown-15m` |
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
Current release info
|
All activity, positions, and PnL live on my profile:
|
||||||
====================
|
**https://polymarket.com/@odahoa?tab=activity**
|
||||||
|
|
||||||
| Name | Downloads | Version | Platforms |
|
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.
|
||||||
| --- | --- | --- | --- |
|
|
||||||
| [](https://anaconda.org/conda-forge/gdptools) | [](https://anaconda.org/conda-forge/gdptools) | [](https://anaconda.org/conda-forge/gdptools) | [](https://anaconda.org/conda-forge/gdptools) |
|
|
||||||
|
|
||||||
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
|
BTC pumps in first 10 minutes
|
||||||
conda config --set channel_priority strict
|
→ 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:
|
52 shares × 95¢ = $49.40 in
|
||||||
mamba repoquery search gdptools --channel conda-forge
|
52 shares × $1 = $52.00 out → +$2.60 (~5%)
|
||||||
|
|
||||||
# List packages depending on `gdptools`:
|
|
||||||
mamba repoquery whoneeds gdptools --channel conda-forge
|
|
||||||
|
|
||||||
# List dependencies of `gdptools`:
|
|
||||||
mamba repoquery depends gdptools --channel conda-forge
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
About conda-forge
|
## Example window
|
||||||
=================
|
|
||||||
|
|
||||||
[](https://numfocus.org)
|
**BTC pumped early** → Up favored, Down cheap
|
||||||
|
|
||||||
conda-forge is a community-led conda channel of installable packages.
|
| Token | Book | Bot posts |
|
||||||
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
|
| Up (favorite) | ask 97¢ | BUY limits @ 90–95¢ |
|
||||||
for each of the installable packages. Such a repository is known as a *feedstock*.
|
| 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
|
| Result | Cheap leg | Hedge leg |
|
||||||
the package) and the necessary configurations for automatic building using freely
|
|--------|-----------|-----------|
|
||||||
available continuous integration services. Thanks to the awesome service provided by
|
| Up wins | Down → $0 | Up → small profit |
|
||||||
[Azure](https://azure.microsoft.com/en-us/services/devops/), [GitHub](https://github.com/),
|
| Down reverses | Down → big profit | Up → $0 |
|
||||||
[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.
|
|
||||||
|
|
||||||
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
|
## Quick start
|
||||||
produce the finished article (built conda distributions)
|
|
||||||
|
|
||||||
|
```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
|
```env
|
||||||
package version, please fork this repository and submit a PR. Upon submission,
|
DRY_RUN=false
|
||||||
your changes will be run on the appropriate platforms to give the reviewer an
|
PRIVATE_KEY=0x... # account signing key
|
||||||
opportunity to confirm that the changes result in a successful build. Once
|
FUNDER_ADDRESS=0xe2511c9e41c5e762887e538b1d6e7221807aa237 # @odahoa proxy wallet
|
||||||
merged, the recipe will be re-built and uploaded automatically to the
|
SIGNATURE_TYPE=2
|
||||||
`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.
|
|
||||||
|
|
||||||
In order to produce a uniquely identifiable distribution:
|
| `SIGNATURE_TYPE` | Use for |
|
||||||
* 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).
|
| `0` | EOA / MetaMask |
|
||||||
* If the version of a package **is** being increased, please remember to return
|
| `2` | Gnosis Safe proxy *(typical Polymarket account)* |
|
||||||
the [``build/number``](https://docs.conda.io/projects/conda-build/en/latest/resources/define-metadata.html#build-number-and-string)
|
| `3` | POLY_1271 deposit wallet |
|
||||||
back to 0.
|
|
||||||
|
|
||||||
Feedstock Maintainers
|
---
|
||||||
=====================
|
|
||||||
|
|
||||||
* [@ocefpaf](https://github.com/ocefpaf/)
|
## Config
|
||||||
* [@rmcd-mscb](https://github.com/rmcd-mscb/)
|
|
||||||
|
|
||||||
|
### 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