From 14a89a3c7f2be9f90bc0f254f4d85057e63be2a8 Mon Sep 17 00:00:00 2001 From: Andrew <39019063+manhinhang@users.noreply.github.com> Date: Mon, 22 Apr 2024 01:07:11 +0800 Subject: [PATCH] Merge develop inito master (#92) * feat: remove ib-insync (#77) * Update to `10.19.2l` (#81) Co-authored-by: github-actions * ci: detect new ib ver (#79) * ci: detect ib gateway version * ci: detect ibc version * ci: export version to github output * ci: fix unable read cur ver * ci: fix unable pass has_update flag * ci: update workflow * ci: update permission * fix; typo * ci: update base branch * ci: read cur ver from .env * ci: PR branch include ibc version * ci: add pull request permission * ci: skip other branch build test * ci: replace with latest version * ci: sed replace action fail * Upgrade action version (#82) * ci: upgrade deployment action dependencies version * ci: add manual trigger deployment * fix: install ib gateway with dynamic ver (#83) * Add heathcheck (#84) * add simple java app for connection check * chore: rename target to heathcheck * embed heathcheck tool to docker image * ci: update workflow * make heathcheck tool executable and update tests * ci: handle deprecation warnings * ci: setup java version * ci: fix deployment error (#85) * fix: healthcheck word typo (#86) * fix: run HealthCheck fail (#87) * refactor: docker build with builder (#89) * refactor: docker build with builder * fix: missed ibc ini file in docker build * update dockerfile * fix: ibgw running error * update Dockerfile * update starting script * feat: support ibgw java heap size (#90) * feat: impl http method for healthcheck (#91) * start rest api in background * add test case for healthcheck rest api * fix: unable start rest server * test: update waiting time * doc: update README --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions --- .env | 2 + .envrc | 2 +- .gitattributes | 10 + .github/workflows/build-test.yml | 29 +- .github/workflows/deploy-dockerhub.yml | 36 +- .github/workflows/detect-new-ver.yml | 77 +++ .gitignore | 1 + Dockerfile | 98 ++-- Dockerfile.template | 92 ++++ README.md | 247 +++++---- README.template | 80 +++ cmd.sh | 35 -- doc/Debugging.md | 4 +- docker-compose.yaml | 20 +- .../google_cloud_secret_manager/README.md | 35 -- examples/ib_insync/README.md | 6 +- healthcheck/.gitattributes | 9 + healthcheck/.gitignore | 5 + healthcheck/gradle.properties | 6 + healthcheck/gradle/libs.versions.toml | 11 + healthcheck/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43453 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + healthcheck/gradlew | 249 +++++++++ healthcheck/gradlew.bat | 92 ++++ healthcheck/healthcheck-rest/build.gradle.kts | 64 +++ .../healthcheck/rest/HttpControllers.kt | 27 + .../ibgatewaydocker/healthcheck/rest/Main.kt | 11 + healthcheck/healthcheck/build.gradle.kts | 71 +++ .../ibgatewaydocker/healthcheck/App.kt | 12 + .../healthcheck/IBGatewayClient.kt | 48 ++ .../ibgatewaydocker/healthcheck/Wrapper.kt | 509 ++++++++++++++++++ .../ibgatewaydocker/healthcheck/AppTest.kt | 13 + healthcheck/settings.gradle.kts | 16 + install_ibgw.exp | 4 +- scripts/detect_ib_gateway_ver.py | 14 + scripts/detect_ibc_ver.py | 16 + scripts/extract_ib_gateway_major_minor.sh | 4 + src/bootstrap.py | 64 --- src/ib_account.py | 44 -- start.sh | 57 ++ test/test_docker_interactive.py | 15 +- test/test_ib_gateway.py | 57 +- test/test_ib_gateway_fail.py | 62 ++- 43 files changed, 1852 insertions(+), 409 deletions(-) create mode 100644 .env create mode 100644 .gitattributes create mode 100644 .github/workflows/detect-new-ver.yml create mode 100644 Dockerfile.template create mode 100644 README.template delete mode 100644 cmd.sh delete mode 100644 examples/google_cloud_secret_manager/README.md create mode 100644 healthcheck/.gitattributes create mode 100644 healthcheck/.gitignore create mode 100644 healthcheck/gradle.properties create mode 100644 healthcheck/gradle/libs.versions.toml create mode 100644 healthcheck/gradle/wrapper/gradle-wrapper.jar create mode 100644 healthcheck/gradle/wrapper/gradle-wrapper.properties create mode 100755 healthcheck/gradlew create mode 100644 healthcheck/gradlew.bat create mode 100644 healthcheck/healthcheck-rest/build.gradle.kts create mode 100644 healthcheck/healthcheck-rest/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/rest/HttpControllers.kt create mode 100644 healthcheck/healthcheck-rest/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/rest/Main.kt create mode 100644 healthcheck/healthcheck/build.gradle.kts create mode 100644 healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/App.kt create mode 100644 healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/IBGatewayClient.kt create mode 100644 healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/Wrapper.kt create mode 100644 healthcheck/healthcheck/src/test/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/AppTest.kt create mode 100644 healthcheck/settings.gradle.kts create mode 100644 scripts/detect_ib_gateway_ver.py create mode 100644 scripts/detect_ibc_ver.py create mode 100644 scripts/extract_ib_gateway_major_minor.sh delete mode 100644 src/bootstrap.py delete mode 100644 src/ib_account.py create mode 100755 start.sh diff --git a/.env b/.env new file mode 100644 index 0000000..d6b889c --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +CUR_IB_GATEWAY_VER=10.19.2l +CUR_IBC_VER=3.18.0 diff --git a/.envrc b/.envrc index 8bccf1f..b88be13 100644 --- a/.envrc +++ b/.envrc @@ -1,5 +1,5 @@ export IMAGE_NAME=ib-gateway-docker -export TRADE_MODE=paper +export TRADING_MODE=paper if [ -f ".secrets" ] ; then source ./.secrets fi diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..44c0836 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +*.sh text eol=lf +*.exp text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 56bdabe..344eca2 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,6 +1,9 @@ name: Build test on: push: + branches: + - develop + - master paths-ignore: - 'README.md' - 'LICENSE' @@ -17,35 +20,51 @@ jobs: - uses: actions/checkout@master - name: Setup python uses: actions/setup-python@v5 + with: + python-version: '3.11' + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: 17 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Build healthcheck tool + working-directory: healthcheck + run: ./gradlew build - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest if [ -f requirements-test.txt ]; then pip install -r requirements-test.txt; fi - name: Build Docker image - run: docker build -t $IMAGE_NAME . + uses: docker/build-push-action@v5 + with: + context: . + push: false + tags: ${{ env.IMAGE_NAME }} - name: Smoke tests container image run: pytest -x env: IB_ACCOUNT: ${{ secrets.IB_ACCOUNT }} IB_PASSWORD: ${{ secrets.IB_PASSWORD }} - TRADE_MODE: paper + TRADING_MODE: paper - name: Run ib_insync example run: | docker run --rm \ -e IB_ACCOUNT=$IB_ACCOUNT \ -e IB_PASSWORD=$IB_PASSWORD \ - -e TRADE_MODE=paper \ + -e TRADING_MODE=paper \ -p 4001:4002 \ -d \ - $IMAGE_NAME tail -f /dev/null; + $IMAGE_NAME; sleep 30; pip install ib_insync pandas; python examples/ib_insync/scripts/connect_gateway.py; docker stop $(docker ps -a -q) env: - TRADE_MODE: paper IB_ACCOUNT: ${{ secrets.IB_ACCOUNT }} IB_PASSWORD: ${{ secrets.IB_PASSWORD }} + TRADING_MODE: paper diff --git a/.github/workflows/deploy-dockerhub.yml b/.github/workflows/deploy-dockerhub.yml index a6561cf..fbf7946 100644 --- a/.github/workflows/deploy-dockerhub.yml +++ b/.github/workflows/deploy-dockerhub.yml @@ -8,7 +8,8 @@ on: - '!hotfix/**' - '!bugfix/**' tags: - - 'v*' + - '*' + workflow_dispatch: jobs: build: @@ -16,11 +17,30 @@ jobs: timeout-minutes: 20 steps: - uses: actions/checkout@master - - name: build-push - uses: docker/build-push-action@v1 + - name: Setup Java + uses: actions/setup-java@v4 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_PASSWORD }} - repository: manhinhang/ib-gateway-docker - tag_with_ref: true - + distribution: 'temurin' + java-version: 17 + - name: Setup Gradle + uses: gradle/actions/setup-gradle@v3 + - name: Build healthcheck tool + working-directory: healthcheck + run: ./gradlew build + - name: Docker meta + id: meta + uses: docker/metadata-action@v5 + with: + images: manhinhang/ib-gateway-docker + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASSWORD }} + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/detect-new-ver.yml b/.github/workflows/detect-new-ver.yml new file mode 100644 index 0000000..1aadcde --- /dev/null +++ b/.github/workflows/detect-new-ver.yml @@ -0,0 +1,77 @@ +name: Detect new version +on: + schedule: + - cron: '0 0 * * *' + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: write + env: + IMAGE_NAME: ib-gateway-docker + steps: + - uses: actions/checkout@master + - name: Setup python + uses: actions/setup-python@v5 + - name: detect new ib gateway version + id: check-update + run: | + IB_GATEWAY_VER=$(python scripts/detect_ib_gateway_ver.py) + echo ib-gateway-ver=${IB_GATEWAY_VER} >> "$GITHUB_OUTPUT" + python scripts/detect_ibc_ver.py + source .env + git restore .env # restore .env + echo ibc-ver=${IBC_VER} >> "$GITHUB_OUTPUT" + echo ibc-asset-url=${IBC_ASSET_URL} >> "$GITHUB_OUTPUT" + + if [ "$IB_GATEWAY_VER" = "$CUR_IB_GATEWAY_VER" ]; then + echo "No dated IB gateway version" + echo has_update=false >> "$GITHUB_OUTPUT" + else + echo "New IB gateway version($IB_GATEWAY_VER)" + echo has_update=true >> "$GITHUB_OUTPUT" + fi + + if [ "$IBC_VER" = "$CUR_IBC_VER" ]; then + echo "No dated IBC version" + echo has_update=false >> "$GITHUB_OUTPUT" + else + echo "New IBC version($IBC_VER)" + echo has_update=true >> "$GITHUB_OUTPUT" + fi + - name: Update files with new version + if: steps.check-update.outputs.has_update == 'true' + run: | + . scripts/extract_ib_gateway_major_minor.sh ${{steps.check-update.outputs.ib-gateway-ver}} + sed -e 's/###IB_GATEWAY_VER###/${{steps.check-update.outputs.ib-gateway-ver}}/g' -e 's/###IBC_VER###/${{steps.check-update.outputs.ibc-ver}}/g' README.template > README.update + sed -e 's/###IBC_VER###/${{steps.check-update.outputs.ibc-ver}}/g' \ + -e 's,###IBC_ASSET_URL###,${{steps.check-update.outputs.ibc-asset-url}},g' \ + -e 's,###IB_GATEWAY_MAJOR###,${IB_GATEWAY_MAJOR},g' \ + -e 's,###IB_GATEWAY_MINOR###,${IB_GATEWAY_MINOR},g' \ + Dockerfile.template > Dockerfile.update + - name: Create PR + if: ${{ steps.check-update.outputs.has_update == 'true' }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + branch='feat/update-to-${{ steps.check-update.outputs.ib-gateway-ver }}-ibc${{steps.check-update.outputs.ibc-ver}}' + git config user.name github-actions + git config user.email github-actions@github.com + git config advice.addIgnoredFile false + git pull + git checkout -b "$branch" origin/develop + # Update files + cp README.update README.md + cp Dockerfile.update Dockerfile + echo "CUR_IB_GATEWAY_VER=${{ steps.check-update.outputs.ib-gateway-ver }}" > .env + echo "CUR_IBC_VER=${{ steps.check-update.outputs.ibc-ver }}" >> .env + ##### + git add README.md + git add Dockerfile + git add -f .env + git commit -m 'Update to `${{ steps.check-update.outputs.ib-gateway-ver }}`' + git push --set-upstream origin "$branch" + + gh pr create --base develop --fill diff --git a/.gitignore b/.gitignore index d3a4a72..568721f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .secrets .env .vscode +healthcheck/healthcheck/src/main/java diff --git a/Dockerfile b/Dockerfile index 1567adb..2590944 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,83 +1,91 @@ -FROM python:3.11-slim +FROM debian:bookworm-slim as downloader # IBC Version : https://github.com/IbcAlpha/IBC/releases ARG IBC_VER="3.18.0" -# ib_insync : https://pypi.org/project/ib-insync/#history -ARG IB_INSYNC_VER="0.9.86" +ARG IBC_ASSET_URL="https://github.com/IbcAlpha/IBC/releases/download/3.18.0-Update.1/IBCLinux-3.18.0.zip" + +# set environment variables +ENV IBC_INI=/root/ibc/config.ini \ + IBC_PATH=/opt/ibc # install dependencies RUN apt-get update \ && apt-get upgrade -y \ && apt-get install -y wget \ - unzip \ + unzip +# make dirs +RUN mkdir -p /tmp + +# download IB TWS +RUN wget -q -O /tmp/ibgw.sh https://download2.interactivebrokers.com/installers/ibgateway/stable-standalone/ibgateway-stable-standalone-linux-x64.sh +RUN chmod +x /tmp/ibgw.sh + +# download IBC +RUN wget -q -O /tmp/IBC.zip ${IBC_ASSET_URL} +RUN unzip /tmp/IBC.zip -d ${IBC_PATH} +RUN chmod +x ${IBC_PATH}/*.sh ${IBC_PATH}/*/*.sh + +# copy IBC/Jts configs +COPY ibc/config.ini ${IBC_INI} + +FROM debian:bookworm-slim +ARG IB_GATEWAY_MAJOR="10" +ARG IB_GATEWAY_MINOR="19" + +# install dependencies +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y \ xvfb \ libxtst6 \ libxrender1 \ - build-essential \ net-tools \ x11-utils \ socat \ - expect \ procps \ xterm RUN apt install -y openjdk-17-jre -RUN pip install ib_insync==$IB_INSYNC_VER # set environment variables ENV TWS_INSTALL_LOG=/root/Jts/tws_install.log \ - ibcIni=/root/ibc/config.ini \ - ibcPath=/opt/ibc \ - javaPath=/opt/i4j_jres \ - twsPath=/root/Jts \ - twsSettingsPath=/root/Jts \ - IB_GATEWAY_PING_CLIENT_ID=1 \ - ibAccMaxRetryCount=30 + IBC_INI=/root/ibc/config.ini \ + IBC_PATH=/opt/ibc \ + TWS_PATH=/root/Jts \ + TWOFA_TIMEOUT_ACTION=restart \ + IB_GATEWAY_MAJOR=${IB_GATEWAY_MAJOR} \ + IB_GATEWAY_MINOR=${IB_GATEWAY_MINOR} \ + IB_GATEWAY_VERSION=${IB_GATEWAY_MAJOR}${IB_GATEWAY_MINOR} # make dirs -RUN mkdir -p /tmp && mkdir -p ${ibcPath} && mkdir -p ${twsPath} +RUN mkdir -p /tmp && mkdir -p ${IBC_PATH} && mkdir -p ${TWS_PATH} # download IB TWS -RUN wget -q -O /tmp/ibgw.sh https://download2.interactivebrokers.com/installers/ibgateway/stable-standalone/ibgateway-stable-standalone-linux-x64.sh -RUN chmod +x /tmp/ibgw.sh +COPY --from=downloader /tmp/ibgw.sh /tmp/ibgw.sh -# download IBC -RUN wget -q -O /tmp/IBC.zip https://github.com/IbcAlpha/IBC/releases/download/$IBC_VER-Update.1/IBCLinux-$IBC_VER.zip -RUN unzip /tmp/IBC.zip -d ${ibcPath} -RUN chmod +x ${ibcPath}/*.sh ${ibcPath}/*/*.sh +RUN /tmp/ibgw.sh -q -dir /root/Jts/ibgateway/${IB_GATEWAY_VERSION} +# remove downloaded files +RUN rm /tmp/ibgw.sh -# install TWS, write output to file so that we can parse the TWS version number later -RUN touch $TWS_INSTALL_LOG -COPY install_ibgw.exp /tmp/install_ibgw.exp -RUN chmod +x /tmp/install_ibgw.exp -RUN /tmp/install_ibgw.exp +COPY --from=downloader /opt/ibc /opt/ibc +COPY --from=downloader /root/ibc /root/ibc -# remove downloaded files -RUN rm /tmp/ibgw.sh /tmp/IBC.zip +# install healthcheck tool +ADD healthcheck/healthcheck/build/distributions/healthcheck.tar / +ENV PATH="${PATH}:/healthcheck/bin" -# copy IBC/Jts configs -COPY ibc/config.ini ${ibcIni} +ADD healthcheck/healthcheck-rest/build/distributions/healthcheck-rest-boot.tar / +ENV PATH="${PATH}:/healthcheck-rest-boot/bin" # copy cmd script WORKDIR /root -COPY cmd.sh /root/cmd.sh -RUN chmod +x /root/cmd.sh - -# python script for /root directory -COPY src/bootstrap.py /root/bootstrap.py -RUN chmod +x /root/bootstrap.py -COPY src/ib_account.py /root/ib_account.py -RUN chmod +x /root/ib_account.py +COPY start.sh /root/start.sh +RUN chmod +x /root/start.sh # set display environment variable (must be set after TWS installation) ENV DISPLAY=:0 -ENV GCP_SECRET=False ENV IBGW_PORT 4002 -ENV IBGW_WATCHDOG_CONNECT_TIMEOUT 30 -ENV IBGW_WATCHDOG_APP_STARTUP_TIME 30 -ENV IBGW_WATCHDOG_APP_TIMEOUT 30 -ENV IBGW_WATCHDOG_RETRY_DELAY 2 -ENV IBGW_WATCHDOG_PROBE_TIMEOUT 4 +ENV JAVA_HEAP_SIZE 768 EXPOSE $IBGW_PORT -ENTRYPOINT [ "sh", "/root/cmd.sh" ] +ENTRYPOINT [ "sh", "/root/start.sh" ] diff --git a/Dockerfile.template b/Dockerfile.template new file mode 100644 index 0000000..30febdd --- /dev/null +++ b/Dockerfile.template @@ -0,0 +1,92 @@ +FROM debian:bookworm-slim as downloader +# IBC Version : https://github.com/IbcAlpha/IBC/releases +ARG IBC_VER="###IBC_VER###" +ARG IBC_ASSET_URL="###IBC_ASSET_URL###" + +# set environment variables +ENV IBC_INI=/root/ibc/config.ini \ + IBC_PATH=/opt/ibc + +# install dependencies +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y wget \ + unzip +# make dirs +RUN mkdir -p /tmp + +# download IB TWS +RUN wget -q -O /tmp/ibgw.sh https://download2.interactivebrokers.com/installers/ibgateway/stable-standalone/ibgateway-stable-standalone-linux-x64.sh +RUN chmod +x /tmp/ibgw.sh + +# download IBC +RUN wget -q -O /tmp/IBC.zip ${IBC_ASSET_URL} +RUN unzip /tmp/IBC.zip -d ${IBC_PATH} +RUN chmod +x ${IBC_PATH}/*.sh ${IBC_PATH}/*/*.sh + +# copy IBC/Jts configs +COPY ibc/config.ini ${IBC_INI} + +FROM debian:bookworm-slim +ARG IB_GATEWAY_MAJOR="###IB_GATEWAY_MAJOR###" +ARG IB_GATEWAY_MINOR="###IB_GATEWAY_MINOR###" + + +# install dependencies +RUN apt-get update \ + && apt-get upgrade -y \ + && apt-get install -y \ + xvfb \ + libxtst6 \ + libxrender1 \ + net-tools \ + x11-utils \ + socat \ + procps \ + xterm +RUN apt install -y openjdk-17-jre + +# set environment variables +ENV TWS_INSTALL_LOG=/root/Jts/tws_install.log \ + IBC_INI=/root/ibc/config.ini \ + IBC_PATH=/opt/ibc \ + TWS_PATH=/root/Jts \ + TWOFA_TIMEOUT_ACTION=restart \ + IB_GATEWAY_MAJOR=${IB_GATEWAY_MAJOR} \ + IB_GATEWAY_MINOR=${IB_GATEWAY_MINOR} \ + IB_GATEWAY_VERSION=${IB_GATEWAY_MAJOR}${IB_GATEWAY_MINOR} + +# make dirs +RUN mkdir -p /tmp && mkdir -p ${IBC_PATH} && mkdir -p ${TWS_PATH} + +# download IB TWS +COPY --from=downloader /tmp/ibgw.sh /tmp/ibgw.sh + +RUN /tmp/ibgw.sh -q -dir /root/Jts/ibgateway/${IB_GATEWAY_VERSION} +# remove downloaded files +RUN rm /tmp/ibgw.sh + +COPY --from=downloader /opt/ibc /opt/ibc +COPY --from=downloader /root/ibc /root/ibc + +# install healthcheck tool +ADD healthcheck/healthcheck/build/distributions/healthcheck.tar / +ENV PATH="${PATH}:/healthcheck/bin" + +ADD healthcheck/healthcheck-rest/build/distributions/healthcheck-rest-boot.tar / +ENV PATH="${PATH}:/healthcheck-rest-boot/bin" + +# copy cmd script +WORKDIR /root +COPY start.sh /root/start.sh +RUN chmod +x /root/start.sh + +# set display environment variable (must be set after TWS installation) +ENV DISPLAY=:0 + +ENV IBGW_PORT 4002 +ENV JAVA_HEAP_SIZE 768 + +EXPOSE $IBGW_PORT + +ENTRYPOINT [ "sh", "/root/start.sh" ] diff --git a/README.md b/README.md index 5810ff1..8770526 100644 --- a/README.md +++ b/README.md @@ -1,99 +1,148 @@ -# IB Gateway docker - -![Build test](https://github.com/manhinhang/ib-gateway-docker/workflows/Build%20test/badge.svg?branch=master) -[![Docker Pulls](https://img.shields.io/docker/pulls/manhinhang/ib-gateway-docker)](https://hub.docker.com/r/manhinhang/ib-gateway-docker) -[![GitHub](https://img.shields.io/github/license/manhinhang/ib-gateway-docker)](https://github.com/manhinhang/ib-gateway-docker/blob/develop/LICENSE) - -lightweight interactive brokers gateway docker - -It's just pure `IB Gateway` and don't include any VNC service (for security reason, I don't like expose extra port) - -This docker image just installed: - -- [IB Gateway](https://www.interactivebrokers.com/en/index.php?f=16457) (10.19.2) - -- [IBC](https://github.com/IbcAlpha/IBC) (3.18.0) - -- [ib_insync](https://github.com/erdewit/ib_insync) (0.9.86) - -- [google-cloud-secret-manager](https://github.com/googleapis/python-secret-manager) (2.11.1) - -## Pull the Docker image from Docker Hub - -```bash -docker pull manhinhang/ib-gateway-docker -``` - -### Create a container from the image and run it -```bash -docker run -d \ ---env IB_ACCOUNT= \ #YOUR_USER_ID ---env IB_PASSWORD= \ #YOUR_PASSWORD ---env TRADE_MODE= \ #paper or live ---p 4002:4002 \ #brige IB gateway port to your local port 4002 -manhinhang/ib-gateway-docker tail -f /dev/null -``` - ---- - -## Build & Run locally - -```bash -git clone git@github.com:manhinhang/ib-gateway-docker.git -cd ib-gateway-docker -docker build --no-cache -t ib-gateway-docker . -docker run -d \ ---env IB_ACCOUNT= \ #YOUR_USER_ID ---env IB_PASSWORD= \ #YOUR_PASSWORD ---env TRADE_MODE= \ #paper or live --p 4002:4002 \ #brige IB gateway port to your local port 4002 -ib-gateway-docker \ -tail -f /dev/null -``` - - -## Container usage example - -| Example | Link | Description | -| - | - | - | -| ib_insync | [examples/ib_insync](./examples/ib_insync) | This example demonstrated how to connect `IB Gateway` -| google cloud secret manager | [examples/google_cloud_secret_manager](./examples/google_cloud_secret_manager) | retreive your interactive brokers account from google cloud secret manager | - - -# Tests - -The [test cases](test/test_ib_gateway.py) written with testinfra. - -Run the tests - -``` -pytest -``` - -# Github Actions for continuous integration - -After forking `IB Gateway docker` repository, you need config your **interactive brokers** paper account & password in *github secret* - -| Key | Description | -| - | - | -| IB_ACCOUNT | your paper account name | -| IB_PASSWORD | your paper account password | - -# Other environment variable - -| Variable Name | Description | Default value | -| - | - | - | -| IB_GATEWAY_PING_CLIENT_ID | ib gateway client id for pinging client status | 1 | -| IBGW_WATCHDOG_CONNECT_TIMEOUT | Ref to [ib_insync.ibcontroller.Watchdog.connectTimeout](https://ib-insync.readthedocs.io/api.html#ib_insync.ibcontroller.Watchdog.connectTimeout) | 30 | -| IBGW_WATCHDOG_APP_STARTUP_TIME | [ib_insync.ibcontroller.Watchdog.appStartupTime](https://ib-insync.readthedocs.io/api.html#ib_insync.ibcontroller.Watchdog.appStartupTime) | 30 | -| IBGW_WATCHDOG_APP_TIMEOUT | Ref to [ib_insync.ibcontroller.Watchdog.appTimeout](https://ib-insync.readthedocs.io/api.html#ib_insync.ibcontroller.Watchdog.appTimeout) | 30 | -| IBGW_WATCHDOG_RETRY_DELAY | Ref to [ib_insync.ibcontroller.Watchdog.retryDelay](https://ib-insync.readthedocs.io/api.html#ib_insync.ibcontroller.Watchdog.retryDelay) | 2 | -| IBGW_WATCHDOG_PROBE_TIMEOUT | Ref to [ib_insync.ibcontroller.Watchdog.probeTimeout](https://ib-insync.readthedocs.io/api.html#ib_insync.ibcontroller.Watchdog.probeTimeout) | 4 | - - -# Disclaimer - -This project is not affiliated with [Interactive Brokers Group, Inc.'s](https://www.interactivebrokers.com). - -Good luck and enjoy. - +# IB Gateway docker + +![Build test](https://github.com/manhinhang/ib-gateway-docker/workflows/Build%20test/badge.svg?branch=master) +[![Docker Pulls](https://img.shields.io/docker/pulls/manhinhang/ib-gateway-docker)](https://hub.docker.com/r/manhinhang/ib-gateway-docker) +[![GitHub](https://img.shields.io/github/license/manhinhang/ib-gateway-docker)](https://github.com/manhinhang/ib-gateway-docker/blob/develop/LICENSE) + +lightweight interactive brokers gateway docker + +It's just pure `IB Gateway` and don't include any VNC service (for security reason, I don't like expose extra port) + +This docker image just installed: + +- [IB Gateway](https://www.interactivebrokers.com/en/index.php?f=16457) (10.19.2l) + +- [IBC](https://github.com/IbcAlpha/IBC) (3.18.0) + +## Pull the Docker image from Docker Hub + +```bash +docker pull manhinhang/ib-gateway-docker +``` + +### Create a container from the image and run it +```bash +docker run -d \ +--env IB_ACCOUNT= \ #YOUR_USER_ID +--env IB_PASSWORD= \ #YOUR_PASSWORD +--env TRADING_MODE= \ #paper or live +--p 4002:4002 \ #brige IB gateway port to your local port 4002 +manhinhang/ib-gateway-docker +``` + +--- + +## Build & Run locally + +```bash +git clone git@github.com:manhinhang/ib-gateway-docker.git +cd ib-gateway-docker +docker build --no-cache -t ib-gateway-docker . +docker run -d \ +--env IB_ACCOUNT= \ #YOUR_USER_ID +--env IB_PASSWORD= \ #YOUR_PASSWORD +--env TRADING_MODE= \ #paper or live +-p 4002:4002 \ #brige IB gateway port to your local port 4002 +ib-gateway-docker +``` + + +## Container usage example + +| Example | Link | Description | +| - | - | - | +| ib_insync | [examples/ib_insync](./examples/ib_insync) | This example demonstrated how to connect `IB Gateway` + + +## Health check container + +### API + +Healthcheck via api call `http://localhost:8080/healthcheck` + +```bash +curl -f http://localhost:8080/healthcheck +``` + +- Docker compose example + +```yaml +services: + ib-gateway: + image: manhinhang/ib-gateway-docker + ports: + - 4002:4002 + environment: + - IB_ACCOUNT=$IB_ACCOUNT + - IB_PASSWORD=$IB_PASSWORD + - TRADING_MODE=$TRADING_MODE + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/healthcheck"] + interval: 60s + timeout: 30s + retries: 3 + start_period: 60s +``` + +### CLI +Execute `healthcheck` to detect IB gateway haelth status + +```bash +healthcheck +# output: Ping IB Gateway successful +echo $? +# output: 0 +``` + +```bash +healthcheck +# output: Can not connect to IB Gateway +echo $? +# output: 1 +``` + +- Docker compose example + +```yaml +services: + ib-gateway: + image: manhinhang/ib-gateway-docker + ports: + - 4002:4002 + environment: + - IB_ACCOUNT=$IB_ACCOUNT + - IB_PASSWORD=$IB_PASSWORD + - TRADING_MODE=$TRADING_MODE + healthcheck: + test: /healthcheck/bin/healthcheck + interval: 60s + timeout: 30s + retries: 3 + start_period: 60s +``` + +# Tests + +The [test cases](test/test_ib_gateway.py) written with testinfra. + +Run the tests + +``` +pytest +``` + +# Github Actions for continuous integration + +After forking `IB Gateway docker` repository, you need config your **interactive brokers** paper account & password in *github secret* + +| Key | Description | +| - | - | +| IB_ACCOUNT | your paper account name | +| IB_PASSWORD | your paper account password | + +# Disclaimer + +This project is not affiliated with [Interactive Brokers Group, Inc.'s](https://www.interactivebrokers.com). + +Good luck and enjoy. + diff --git a/README.template b/README.template new file mode 100644 index 0000000..87d47d5 --- /dev/null +++ b/README.template @@ -0,0 +1,80 @@ +# IB Gateway docker + +![Build test](https://github.com/manhinhang/ib-gateway-docker/workflows/Build%20test/badge.svg?branch=master) +[![Docker Pulls](https://img.shields.io/docker/pulls/manhinhang/ib-gateway-docker)](https://hub.docker.com/r/manhinhang/ib-gateway-docker) +[![GitHub](https://img.shields.io/github/license/manhinhang/ib-gateway-docker)](https://github.com/manhinhang/ib-gateway-docker/blob/develop/LICENSE) + +lightweight interactive brokers gateway docker + +It's just pure `IB Gateway` and don't include any VNC service (for security reason, I don't like expose extra port) + +This docker image just installed: + +- [IB Gateway](https://www.interactivebrokers.com/en/index.php?f=16457) (###IB_GATEWAY_VER###) + +- [IBC](https://github.com/IbcAlpha/IBC) (###IBC_VER###) + +## Pull the Docker image from Docker Hub + +```bash +docker pull manhinhang/ib-gateway-docker +``` + +### Create a container from the image and run it +```bash +docker run -d \ +--env IB_ACCOUNT= \ #YOUR_USER_ID +--env IB_PASSWORD= \ #YOUR_PASSWORD +--env TRADING_MODE= \ #paper or live +--p 4002:4002 \ #brige IB gateway port to your local port 4002 +manhinhang/ib-gateway-docker +``` + +--- + +## Build & Run locally + +```bash +git clone git@github.com:manhinhang/ib-gateway-docker.git +cd ib-gateway-docker +docker build --no-cache -t ib-gateway-docker . +docker run -d \ +--env IB_ACCOUNT= \ #YOUR_USER_ID +--env IB_PASSWORD= \ #YOUR_PASSWORD +--env TRADING_MODE= \ #paper or live +-p 4002:4002 \ #brige IB gateway port to your local port 4002 +ib-gateway-docker +``` + + +## Container usage example + +| Example | Link | Description | +| - | - | - | +| ib_insync | [examples/ib_insync](./examples/ib_insync) | This example demonstrated how to connect `IB Gateway` + +# Tests + +The [test cases](test/test_ib_gateway.py) written with testinfra. + +Run the tests + +``` +pytest +``` + +# Github Actions for continuous integration + +After forking `IB Gateway docker` repository, you need config your **interactive brokers** paper account & password in *github secret* + +| Key | Description | +| - | - | +| IB_ACCOUNT | your paper account name | +| IB_PASSWORD | your paper account password | + +# Disclaimer + +This project is not affiliated with [Interactive Brokers Group, Inc.'s](https://www.interactivebrokers.com). + +Good luck and enjoy. + diff --git a/cmd.sh b/cmd.sh deleted file mode 100644 index 926b414..0000000 --- a/cmd.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/bin/bash -set -e - -echo "Starting Xvfb..." -rm -f /tmp/.X0-lock -/usr/bin/Xvfb "$DISPLAY" -ac -screen 0 1024x768x16 +extension RANDR & - -echo "Waiting for Xvfb to be ready..." -while ! xdpyinfo -display "$DISPLAY"; do - echo -n '' - sleep 0.1 -done - -echo "Xvfb is ready" -echo "Setup port forwarding..." - -socat TCP-LISTEN:$IBGW_PORT,fork TCP:localhost:4001,forever & -echo "*****************************" - -python /root/bootstrap.py - -echo "IB gateway is ready." - -#Define cleanup procedure -cleanup() { - pkill java - pkill Xvfb - pkill socat - echo "Container stopped, performing cleanup..." -} - -#Trap TERM -trap 'cleanup' INT TERM - -$@ diff --git a/doc/Debugging.md b/doc/Debugging.md index b3b156e..89bf92a 100644 --- a/doc/Debugging.md +++ b/doc/Debugging.md @@ -22,9 +22,9 @@ For debugging, Use x11 forwarding to visit IB gateway GUI for the investigation. docker run --platform linux/amd64 -d \ --env IB_ACCOUNT= \ --env IB_PASSWORD= \ - --env TRADE_MODE= \ + --env TRADING_MODE= \ -v ~/.Xauthority:/root/.Xauthority \ -e DISPLAY=$ip:0 \ -p 4002:4002 \ - ib-gateway-docker tail -f /dev/null + ib-gateway-docker ``` diff --git a/docker-compose.yaml b/docker-compose.yaml index 43e62c8..d41c9e9 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,17 +1,17 @@ services: ib-gateway: - image: manhinhang/ib-gateway-docker - command: tail -f /dev/null + build: + context: . ports: - 4002:4002 environment: - IB_ACCOUNT=$IB_ACCOUNT - IB_PASSWORD=$IB_PASSWORD - - TRADE_MODE=$TRADE_MODE -secrets: - IB_ACCOUNT: - environment: "IB_ACCOUNT" - IB_PASSWORD: - environment: "IB_PWD" - TRADE_MODE: - environment: "TRADE_MODE" + - TRADING_MODE=$TRADING_MODE + healthcheck: + test: /healthcheck/bin/healthcheck + interval: 60s + timeout: 30s + retries: 3 + start_period: 60s + diff --git a/examples/google_cloud_secret_manager/README.md b/examples/google_cloud_secret_manager/README.md deleted file mode 100644 index d6220dd..0000000 --- a/examples/google_cloud_secret_manager/README.md +++ /dev/null @@ -1,35 +0,0 @@ -# Example for google cloud secret manager - -If you considing choose Google cloud as your IB gateway host, Google recommanded store your sensitive key in secret manager. - -Reference: https://cloud.google.com/functions/docs/env-var#managing_secrets - -> Environment variables can be used for function configuration, but are not recommended as a way to store secrets such as database credentials or API keys. These more sensitive values should be stored outside both your source code and outside environment variables. Some execution environments or the use of some frameworks can result in the contents of environment variables being sent to logs, and storing sensitive credentials in YAML files, deployment scripts or under source control is not recommended. -> -> For storing secrets, we recommend that you review the best practices for secret management. Note that there is no Cloud Functions-specific integration with Cloud KMS. - ---- - -This example just shown you how run docker & retreive secret locally. - -> *The deploy guide for google cloud may provide later.* - -1. Setup your credentials path - - ```bash - export GOOGLE_APPLICATION_CREDENTIALS= #your credentials json path - ``` - -2. Run docker run command - - ```bash - docker run --rm -it \ - --env GCP_SECRET=True \ # Enable Google secret manager feature - --env GCP_SECRET_IB_ACCOUNT= \ # secret key name of your interactive brokers account name - --env GCP_SECRET_IB_PASSWORD= \ # secret key name of your interactive brokers password - --env GCP_SECRET_IB_TRADE_MODE= \ # secret key name of trade mode - --env GCP_PROJECT_ID= \ #your project id - -e GOOGLE_APPLICATION_CREDENTIALS=/tmp/keys/FILE_NAME.json \ - -v $GOOGLE_APPLICATION_CREDENTIALS:/tmp/keys/FILE_NAME.json:ro \ - manhinhang/ib-gateway-docker - ``` \ No newline at end of file diff --git a/examples/ib_insync/README.md b/examples/ib_insync/README.md index 41a290b..c04417d 100644 --- a/examples/ib_insync/README.md +++ b/examples/ib_insync/README.md @@ -10,17 +10,17 @@ Python script ## Docker run command ```bash -export TRADE_MODE=#paper or live +export TRADING_MODE=#paper or live export IB_ACCOUNT=# your interactive brokers account name export IB_PASSWORD=# your interactive brokers account password docker run --rm \ -e IB_ACCOUNT=$IB_ACCOUNT \ -e IB_PASSWORD=$IB_PASSWORD \ --e TRADE_MODE=$TRADE_MODE \ +-e TRADING_MODE=$TRADING_MODE \ -p 4001:4002 \ -d \ -manhinhang/ib-gateway-docker:latest tail -f /dev/null +manhinhang/ib-gateway-docker:latest pip install ib_insync pandas python ib_insync/scripts/connect_gateway.py diff --git a/healthcheck/.gitattributes b/healthcheck/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/healthcheck/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/healthcheck/.gitignore b/healthcheck/.gitignore new file mode 100644 index 0000000..1b6985c --- /dev/null +++ b/healthcheck/.gitignore @@ -0,0 +1,5 @@ +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/healthcheck/gradle.properties b/healthcheck/gradle.properties new file mode 100644 index 0000000..18f452c --- /dev/null +++ b/healthcheck/gradle.properties @@ -0,0 +1,6 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties + +org.gradle.parallel=true +org.gradle.caching=true + diff --git a/healthcheck/gradle/libs.versions.toml b/healthcheck/gradle/libs.versions.toml new file mode 100644 index 0000000..d590309 --- /dev/null +++ b/healthcheck/gradle/libs.versions.toml @@ -0,0 +1,11 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format + +[versions] +guava = "32.1.3-jre" + +[libraries] +guava = { module = "com.google.guava:guava", version.ref = "guava" } + +[plugins] +jvm = { id = "org.jetbrains.kotlin.jvm", version = "1.9.22" } diff --git a/healthcheck/gradle/wrapper/gradle-wrapper.jar b/healthcheck/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..e6441136f3d4ba8a0da8d277868979cfbc8ad796 GIT binary patch literal 43453 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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
NnX)EH+Nua)3Y(c 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 '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/healthcheck/gradlew.bat b/healthcheck/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/healthcheck/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/healthcheck/healthcheck-rest/build.gradle.kts b/healthcheck/healthcheck-rest/build.gradle.kts new file mode 100644 index 0000000..6ca3a84 --- /dev/null +++ b/healthcheck/healthcheck-rest/build.gradle.kts @@ -0,0 +1,64 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Kotlin application project to get you started. + * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.7/userguide/building_java_projects.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. + alias(libs.plugins.jvm) + id("org.springframework.boot") version "3.2.2" + id("io.spring.dependency-management") version "1.1.4" + kotlin("plugin.spring") version "1.9.22" + kotlin("plugin.jpa") version "1.9.22" + // Apply the application plugin to add support for building a CLI application in Java. + application +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + // This dependency is used by the application. + implementation(libs.guava) + implementation(platform("org.springframework.boot:spring-boot-dependencies:3.2.2")) + implementation("org.springframework.boot:spring-boot-starter") + implementation("org.springframework.boot:spring-boot-starter-web") +// implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1-Beta") + implementation(project(":healthcheck")) +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "17" + } +} + +testing { + suites { + // Configure the built-in test suite + val test by getting(JvmTestSuite::class) { + // Use Kotlin Test test framework + useKotlinTest("1.9.22") + } + } +} + +// Apply a specific Java toolchain to ease working on different environments. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +application { + // Define the main class for the application. + mainClass = "com.manhinhang.ibgatewaydocker.healthcheck.rest.MainKt" +} diff --git a/healthcheck/healthcheck-rest/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/rest/HttpControllers.kt b/healthcheck/healthcheck-rest/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/rest/HttpControllers.kt new file mode 100644 index 0000000..e295f07 --- /dev/null +++ b/healthcheck/healthcheck-rest/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/rest/HttpControllers.kt @@ -0,0 +1,27 @@ +package com.manhinhang.ibgatewaydocker.healthcheck.rest +import com.manhinhang.ibgatewaydocker.healthcheck.IBGatewayClient +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* +import kotlinx.coroutines.* + +@RestController +@RequestMapping("/") +class HealthcheckApiController() { + + val ibClient = IBGatewayClient() + + @GetMapping("/ready") + fun ready(): ResponseEntity { + return ResponseEntity.status(HttpStatus.OK).body("OK"); + } + + @GetMapping("/healthcheck") + fun healthcheck(): ResponseEntity { + val result = runBlocking { ibClient.ping() } + if (result.isSuccess) { + return ResponseEntity.status(HttpStatus.OK).body("OK") + } + return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Fail") + } +} \ No newline at end of file diff --git a/healthcheck/healthcheck-rest/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/rest/Main.kt b/healthcheck/healthcheck-rest/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/rest/Main.kt new file mode 100644 index 0000000..aa8d4a5 --- /dev/null +++ b/healthcheck/healthcheck-rest/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/rest/Main.kt @@ -0,0 +1,11 @@ +package com.manhinhang.ibgatewaydocker.healthcheck.rest + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class RestApplication + +fun main(args: Array) { + runApplication(*args) +} \ No newline at end of file diff --git a/healthcheck/healthcheck/build.gradle.kts b/healthcheck/healthcheck/build.gradle.kts new file mode 100644 index 0000000..b980f5c --- /dev/null +++ b/healthcheck/healthcheck/build.gradle.kts @@ -0,0 +1,71 @@ +import org.gradle.wrapper.Download + +/* + * This file was generated by the Gradle 'init' task. + * + * This generated file contains a sample Kotlin application project to get you started. + * For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.7/userguide/building_java_projects.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + // Apply the org.jetbrains.kotlin.jvm Plugin to add support for Kotlin. + alias(libs.plugins.jvm) + id("de.undercouch.download") version "5.6.0" + // Apply the application plugin to add support for building a CLI application in Java. + application +} + +repositories { + // Use Maven Central for resolving dependencies. + mavenCentral() +} + +dependencies { + // This dependency is used by the application. + implementation(libs.guava) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1-Beta") +} + +val downloadIbApiTask = tasks.register("downloadIbApi") { + onlyIf("ibapi.zip not found") { + !File("${layout.buildDirectory.asFile.get().path}/ibapi.zip").exists() + } + src("https://interactivebrokers.github.io/downloads/twsapi_macunix.1019.04.zip") + dest("${layout.buildDirectory.asFile.get().path}/ibapi.zip") +} + +val unzipIbApiTask = tasks.register("unzipIbApi") { + from(zipTree("${layout.buildDirectory.asFile.get().path}/ibapi.zip")) { + include("IBJts/source/JavaClient/com/**") + eachFile { + relativePath = RelativePath(true, *relativePath.segments.drop(3).toTypedArray()) + } + includeEmptyDirs = false + } + into("${projectDir}/src/main/java") +} +unzipIbApiTask { dependsOn(downloadIbApiTask) } +tasks.compileKotlin { dependsOn(unzipIbApiTask) } + +testing { + suites { + // Configure the built-in test suite + val test by getting(JvmTestSuite::class) { + // Use Kotlin Test test framework + useKotlinTest("1.9.22") + } + } +} + +// Apply a specific Java toolchain to ease working on different environments. +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +application { + // Define the main class for the application. + mainClass = "com.manhinhang.ibgatewaydocker.healthcheck.AppKt" +} diff --git a/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/App.kt b/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/App.kt new file mode 100644 index 0000000..c62edab --- /dev/null +++ b/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/App.kt @@ -0,0 +1,12 @@ +/* + * This source file was generated by the Gradle 'init' task + */ +package com.manhinhang.ibgatewaydocker.healthcheck +import kotlinx.coroutines.* + +fun main() = runBlocking { + val client = IBGatewayClient() + val result = client.ping() + result.exceptionOrNull()?.let { throw it } + return@runBlocking +} \ No newline at end of file diff --git a/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/IBGatewayClient.kt b/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/IBGatewayClient.kt new file mode 100644 index 0000000..ee3322d --- /dev/null +++ b/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/IBGatewayClient.kt @@ -0,0 +1,48 @@ +package com.manhinhang.ibgatewaydocker.healthcheck + +import com.ib.client.EClientSocket +import com.ib.client.EJavaSignal +import kotlinx.coroutines.* + +class IBGatewayClient { + val client: EClientSocket + companion object { + val clientId = (System.getenv("HEALTHCHECK_CLIENT_ID")?.toIntOrNull() ?: 999) + val port = (System.getenv("IB_GATEWAY_INTERNAL_PORT")?.toIntOrNull() ?: 4001) + val host = "localhost" + } + + init { + client = createIBClient() + } + + private fun createIBClient(): EClientSocket { + val signal = EJavaSignal(); + val client = EClientSocket(Wrapper(), signal) + return client + } + + private suspend fun connect():Boolean = withContext(Dispatchers.IO) { + if (!client.isConnected) { + client.eConnect(host, port, clientId) + } + client.isConnected + } + + private fun disconnect() { + client.eDisconnect() + } + + suspend fun ping():Result = coroutineScope { + runCatching { + val isConnected = connect() + if (isConnected) { + println("Ping IB Gateway successful") + disconnect() + }else { + throw InterruptedException("Can not connect to IB Gateway") + } + } + } + +} \ No newline at end of file diff --git a/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/Wrapper.kt b/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/Wrapper.kt new file mode 100644 index 0000000..74baabc --- /dev/null +++ b/healthcheck/healthcheck/src/main/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/Wrapper.kt @@ -0,0 +1,509 @@ +package com.manhinhang.ibgatewaydocker.healthcheck + +import com.ib.client.* +import java.lang.Exception + +class Wrapper: EWrapper { + override fun tickPrice(tickerId: Int, field: Int, price: Double, attrib: TickAttrib?) { + + } + + override fun tickSize(tickerId: Int, field: Int, size: Decimal?) { + + } + + override fun tickOptionComputation( + tickerId: Int, + field: Int, + tickAttrib: Int, + impliedVol: Double, + delta: Double, + optPrice: Double, + pvDividend: Double, + gamma: Double, + vega: Double, + theta: Double, + undPrice: Double + ) { + + } + + override fun tickGeneric(tickerId: Int, tickType: Int, value: Double) { + + } + + override fun tickString(tickerId: Int, tickType: Int, value: String?) { + + } + + override fun tickEFP( + tickerId: Int, + tickType: Int, + basisPoints: Double, + formattedBasisPoints: String?, + impliedFuture: Double, + holdDays: Int, + futureLastTradeDate: String?, + dividendImpact: Double, + dividendsToLastTradeDate: Double + ) { + + } + + override fun orderStatus( + orderId: Int, + status: String?, + filled: Decimal?, + remaining: Decimal?, + avgFillPrice: Double, + permId: Int, + parentId: Int, + lastFillPrice: Double, + clientId: Int, + whyHeld: String?, + mktCapPrice: Double + ) { + + } + + override fun openOrder(orderId: Int, contract: Contract?, order: Order?, orderState: OrderState?) { + + } + + override fun openOrderEnd() { + + } + + override fun updateAccountValue(key: String?, value: String?, currency: String?, accountName: String?) { + + } + + override fun updatePortfolio( + contract: Contract?, + position: Decimal?, + marketPrice: Double, + marketValue: Double, + averageCost: Double, + unrealizedPNL: Double, + realizedPNL: Double, + accountName: String? + ) { + + } + + override fun updateAccountTime(timeStamp: String?) { + + } + + override fun accountDownloadEnd(accountName: String?) { + + } + + override fun nextValidId(orderId: Int) { + + } + + override fun contractDetails(reqId: Int, contractDetails: ContractDetails?) { + + } + + override fun bondContractDetails(reqId: Int, contractDetails: ContractDetails?) { + + } + + override fun contractDetailsEnd(reqId: Int) { + + } + + override fun execDetails(reqId: Int, contract: Contract?, execution: Execution?) { + + } + + override fun execDetailsEnd(reqId: Int) { + + } + + override fun updateMktDepth( + tickerId: Int, + position: Int, + operation: Int, + side: Int, + price: Double, + size: Decimal? + ) { + + } + + override fun updateMktDepthL2( + tickerId: Int, + position: Int, + marketMaker: String?, + operation: Int, + side: Int, + price: Double, + size: Decimal?, + isSmartDepth: Boolean + ) { + + } + + override fun updateNewsBulletin(msgId: Int, msgType: Int, message: String?, origExchange: String?) { + + } + + override fun managedAccounts(accountsList: String?) { + + } + + override fun receiveFA(faDataType: Int, xml: String?) { + + } + + override fun historicalData(reqId: Int, bar: Bar?) { + + } + + override fun scannerParameters(xml: String?) { + + } + + override fun scannerData( + reqId: Int, + rank: Int, + contractDetails: ContractDetails?, + distance: String?, + benchmark: String?, + projection: String?, + legsStr: String? + ) { + + } + + override fun scannerDataEnd(reqId: Int) { + + } + + override fun realtimeBar( + reqId: Int, + time: Long, + open: Double, + high: Double, + low: Double, + close: Double, + volume: Decimal?, + wap: Decimal?, + count: Int + ) { + + } + + override fun currentTime(time: Long) { + + } + + override fun fundamentalData(reqId: Int, data: String?) { + + } + + override fun deltaNeutralValidation(reqId: Int, deltaNeutralContract: DeltaNeutralContract?) { + + } + + override fun tickSnapshotEnd(reqId: Int) { + + } + + override fun marketDataType(reqId: Int, marketDataType: Int) { + + } + + override fun commissionReport(commissionReport: CommissionReport?) { + + } + + override fun position(account: String?, contract: Contract?, pos: Decimal?, avgCost: Double) { + + } + + override fun positionEnd() { + + } + + override fun accountSummary(reqId: Int, account: String?, tag: String?, value: String?, currency: String?) { + + } + + override fun accountSummaryEnd(reqId: Int) { + + } + + override fun verifyMessageAPI(apiData: String?) { + + } + + override fun verifyCompleted(isSuccessful: Boolean, errorText: String?) { + + } + + override fun verifyAndAuthMessageAPI(apiData: String?, xyzChallenge: String?) { + + } + + override fun verifyAndAuthCompleted(isSuccessful: Boolean, errorText: String?) { + + } + + override fun displayGroupList(reqId: Int, groups: String?) { + + } + + override fun displayGroupUpdated(reqId: Int, contractInfo: String?) { + + } + + override fun error(e: Exception?) { + + } + + override fun error(str: String?) { + + } + + override fun error(id: Int, errorCode: Int, errorMsg: String?, advancedOrderRejectJson: String?) { + + } + + override fun connectionClosed() { + + } + + override fun connectAck() { + + } + + override fun positionMulti( + reqId: Int, + account: String?, + modelCode: String?, + contract: Contract?, + pos: Decimal?, + avgCost: Double + ) { + + } + + override fun positionMultiEnd(reqId: Int) { + + } + + override fun accountUpdateMulti( + reqId: Int, + account: String?, + modelCode: String?, + key: String?, + value: String?, + currency: String? + ) { + + } + + override fun accountUpdateMultiEnd(reqId: Int) { + + } + + override fun securityDefinitionOptionalParameter( + reqId: Int, + exchange: String?, + underlyingConId: Int, + tradingClass: String?, + multiplier: String?, + expirations: MutableSet?, + strikes: MutableSet? + ) { + + } + + override fun securityDefinitionOptionalParameterEnd(reqId: Int) { + + } + + override fun softDollarTiers(reqId: Int, tiers: Array?) { + + } + + override fun familyCodes(familyCodes: Array?) { + + } + + override fun symbolSamples(reqId: Int, contractDescriptions: Array?) { + + } + + override fun historicalDataEnd(reqId: Int, startDateStr: String?, endDateStr: String?) { + + } + + override fun mktDepthExchanges(depthMktDataDescriptions: Array?) { + + } + + override fun tickNews( + tickerId: Int, + timeStamp: Long, + providerCode: String?, + articleId: String?, + headline: String?, + extraData: String? + ) { + + } + + override fun smartComponents(reqId: Int, theMap: MutableMap>?) { + + } + + override fun tickReqParams(tickerId: Int, minTick: Double, bboExchange: String?, snapshotPermissions: Int) { + + } + + override fun newsProviders(newsProviders: Array?) { + + } + + override fun newsArticle(requestId: Int, articleType: Int, articleText: String?) { + + } + + override fun historicalNews( + requestId: Int, + time: String?, + providerCode: String?, + articleId: String?, + headline: String? + ) { + + } + + override fun historicalNewsEnd(requestId: Int, hasMore: Boolean) { + + } + + override fun headTimestamp(reqId: Int, headTimestamp: String?) { + + } + + override fun histogramData(reqId: Int, items: MutableList?) { + + } + + override fun historicalDataUpdate(reqId: Int, bar: Bar?) { + + } + + override fun rerouteMktDataReq(reqId: Int, conId: Int, exchange: String?) { + + } + + override fun rerouteMktDepthReq(reqId: Int, conId: Int, exchange: String?) { + + } + + override fun marketRule(marketRuleId: Int, priceIncrements: Array?) { + + } + + override fun pnl(reqId: Int, dailyPnL: Double, unrealizedPnL: Double, realizedPnL: Double) { + + } + + override fun pnlSingle( + reqId: Int, + pos: Decimal?, + dailyPnL: Double, + unrealizedPnL: Double, + realizedPnL: Double, + value: Double + ) { + + } + + override fun historicalTicks(reqId: Int, ticks: MutableList?, done: Boolean) { + + } + + override fun historicalTicksBidAsk(reqId: Int, ticks: MutableList?, done: Boolean) { + + } + + override fun historicalTicksLast(reqId: Int, ticks: MutableList?, done: Boolean) { + + } + + override fun tickByTickAllLast( + reqId: Int, + tickType: Int, + time: Long, + price: Double, + size: Decimal?, + tickAttribLast: TickAttribLast?, + exchange: String?, + specialConditions: String? + ) { + + } + + override fun tickByTickBidAsk( + reqId: Int, + time: Long, + bidPrice: Double, + askPrice: Double, + bidSize: Decimal?, + askSize: Decimal?, + tickAttribBidAsk: TickAttribBidAsk? + ) { + + } + + override fun tickByTickMidPoint(reqId: Int, time: Long, midPoint: Double) { + + } + + override fun orderBound(orderId: Long, apiClientId: Int, apiOrderId: Int) { + + } + + override fun completedOrder(contract: Contract?, order: Order?, orderState: OrderState?) { + + } + + override fun completedOrdersEnd() { + + } + + override fun replaceFAEnd(reqId: Int, text: String?) { + + } + + override fun wshMetaData(reqId: Int, dataJson: String?) { + + } + + override fun wshEventData(reqId: Int, dataJson: String?) { + + } + + override fun historicalSchedule( + reqId: Int, + startDateTime: String?, + endDateTime: String?, + timeZone: String?, + sessions: MutableList? + ) { + + } + + override fun userInfo(reqId: Int, whiteBrandingId: String?) { + + } + +} \ No newline at end of file diff --git a/healthcheck/healthcheck/src/test/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/AppTest.kt b/healthcheck/healthcheck/src/test/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/AppTest.kt new file mode 100644 index 0000000..b0ee115 --- /dev/null +++ b/healthcheck/healthcheck/src/test/kotlin/com/manhinhang/ibgatewaydocker/healthcheck/AppTest.kt @@ -0,0 +1,13 @@ +/* + * This source file was generated by the Gradle 'init' task + */ +package com.manhinhang.ibgatewaydocker.healthcheck + +import kotlin.test.Test +import kotlin.test.assertNotNull + +class AppTest { + @Test fun appHasAGreeting() { + + } +} diff --git a/healthcheck/settings.gradle.kts b/healthcheck/settings.gradle.kts new file mode 100644 index 0000000..39f410d --- /dev/null +++ b/healthcheck/settings.gradle.kts @@ -0,0 +1,16 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.7/userguide/multi_project_builds.html in the Gradle documentation. + * This project uses @Incubating APIs which are subject to change. + */ + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} + +rootProject.name = "healthcheck" +include("healthcheck") +include("healthcheck-rest") diff --git a/install_ibgw.exp b/install_ibgw.exp index 803d4f7..6eef697 100644 --- a/install_ibgw.exp +++ b/install_ibgw.exp @@ -6,11 +6,11 @@ log_file /root/Jts/tws_install.log spawn /tmp/ibgw.sh -expect "Where should IB Gateway 10.19 be installed?\r" +expect "Where should IB Gateway $::env(IB_GATEWAY_MAJOR).$::env(IB_GATEWAY_MINOR) be installed?\r" send -- "\r" -expect "Run IB Gateway 10.19?\r" +expect "Run IB Gateway $::env(IB_GATEWAY_MAJOR).$::env(IB_GATEWAY_MINOR)?\r" send -- "n\r" diff --git a/scripts/detect_ib_gateway_ver.py b/scripts/detect_ib_gateway_ver.py new file mode 100644 index 0000000..014cc1a --- /dev/null +++ b/scripts/detect_ib_gateway_ver.py @@ -0,0 +1,14 @@ +import requests +import json +import re + +if __name__ == "__main__": + url = "https://download2.interactivebrokers.com/installers/ibgateway/stable-standalone/version.json" + regex = r"([^(]+)\)" + response = requests.get(url) + response_text = response.text + matches = re.finditer(regex, response_text) + # print(matches) + json_str = next(matches).group(1) + data = json.loads(json_str) + print(data["buildVersion"]) diff --git a/scripts/detect_ibc_ver.py b/scripts/detect_ibc_ver.py new file mode 100644 index 0000000..a10266d --- /dev/null +++ b/scripts/detect_ibc_ver.py @@ -0,0 +1,16 @@ +import requests +import os + +if __name__ == "__main__": + url = "https://api.github.com/repos/IbcAlpha/IBC/releases" + response = requests.get(url) + data = response.json() + latest = data[0] + ver = latest["name"] + for asset in latest["assets"]: + if asset["name"].startswith("IBCLinux"): + asset_url = asset["browser_download_url"] + + with open('.env', 'a') as fp: + fp.write(f'IBC_VER={ver}\n') + fp.write(f'IBC_ASSET_URL={asset_url}\n') diff --git a/scripts/extract_ib_gateway_major_minor.sh b/scripts/extract_ib_gateway_major_minor.sh new file mode 100644 index 0000000..b412543 --- /dev/null +++ b/scripts/extract_ib_gateway_major_minor.sh @@ -0,0 +1,4 @@ +if [[ $1 =~ ([0-9]+)\.([0-9]+) ]]; then + IB_GATEWAY_MAJOR=${BASH_REMATCH[1]} + IB_GATEWAY_MINOR=${BASH_REMATCH[2]} +fi diff --git a/src/bootstrap.py b/src/bootstrap.py deleted file mode 100644 index a03ce2b..0000000 --- a/src/bootstrap.py +++ /dev/null @@ -1,64 +0,0 @@ -from ib_insync import IBC, IB, Watchdog -import os -import logging -from ib_account import IBAccount -import sys - -if __name__ == "__main__": - logging.basicConfig(level=logging.INFO, stream=sys.stdout, format="[%(asctime)s]%(levelname)s:%(message)s") - logging.info('start ib gateway...') - logging.info('---ib gateway info---') - twsPath = os.environ['twsPath'] - logging.info(f'twsPath: {twsPath}') - gatewayRootPath = "{}/ibgateway".format(twsPath) - ib_gateway_version = int(os.listdir(gatewayRootPath)[0]) - gatewayPath = "{}/{}".format(gatewayRootPath, ib_gateway_version) - logging.info("ib gateway version:{}".format(ib_gateway_version)) - logging.info("ib gateway path:{}".format(gatewayPath)) - logging.info('-------------------') - account = IBAccount.account() - password = IBAccount.password() - trade_mode = IBAccount.trade_mode() - ibc = IBC(ib_gateway_version, - gateway=True, - tradingMode=trade_mode, - userid=account, - password=password, - twsPath=twsPath) - ib = IB() - def onConnected(): - logging.info('IB gateway connected') - logging.info(ib.accountValues()) - - def onDisconnected(): - logging.info('IB gateway disconnected') - ib.connectedEvent += onConnected - ib.disconnectedEvent += onDisconnected - watchdog = Watchdog(ibc, ib, port=4001, - connectTimeout=int(os.environ['IBGW_WATCHDOG_CONNECT_TIMEOUT']), - appStartupTime=int(os.environ['IBGW_WATCHDOG_APP_STARTUP_TIME']), - appTimeout=int(os.environ['IBGW_WATCHDOG_APP_TIMEOUT']), - retryDelay=int(os.environ['IBGW_WATCHDOG_RETRY_DELAY']), - probeTimeout=int(os.environ['IBGW_WATCHDOG_PROBE_TIMEOUT'])) - def onWatchDogStarting(_): - logging.info('WatchDog Starting...') - def onWatchDogStarted(_): - logging.info('WatchDog Started!') - def onWatchDogStopping(_): - logging.info('WatchDog Stopping...') - def onWatchDogStopped(_): - logging.info('WatchDog Stopped!') - def onWatchDogSoftTimeout(_): - logging.info('WatchDog soft timeout!') - def onWatchDogHardTimeoutEvent(_): - logging.info('WatchDog hard timeout!') - watchdog.startingEvent += onWatchDogStarting - watchdog.startedEvent += onWatchDogStarted - watchdog.stoppingEvent += onWatchDogStopping - watchdog.stoppedEvent += onWatchDogStopped - watchdog.softTimeoutEvent += onWatchDogSoftTimeout - watchdog.hardTimeoutEvent += onWatchDogHardTimeoutEvent - watchdog.start() - ib.run() - logging.info('IB gateway is ready.') - diff --git a/src/ib_account.py b/src/ib_account.py deleted file mode 100644 index c77baeb..0000000 --- a/src/ib_account.py +++ /dev/null @@ -1,44 +0,0 @@ -import os -# from distutils.util import strtobool -# from google.cloud import secretmanager - -class IBAccount(object): - # Create the Secret Manager client. - __client = None - - # @classmethod - # def retrieve_secret(cls, secret_id): - # if not cls.__client: - # cls.__client = secretmanager.SecretManagerServiceClient() - # gcp_project_id = os.environ['GCP_PROJECT_ID'] - # name = cls.__client.secret_version_path(gcp_project_id, secret_id, 'latest') - # response = cls.__client.access_secret_version(name=name) - # payload = response.payload.data.decode('UTF-8') - # return payload - - # @staticmethod - # def isEnabledGCPSecret(): - # try: - # return bool(strtobool(os.environ['GCP_SECRET'])) - # except ValueError: - # return False - - @classmethod - def account(cls): - # if not cls.isEnabledGCPSecret(): - return os.environ['IB_ACCOUNT'] - # return cls.retrieve_secret(os.environ['GCP_SECRET_IB_ACCOUNT']) - - @classmethod - def password(cls): - # if not cls.isEnabledGCPSecret(): - return os.environ['IB_PASSWORD'] - # return cls.retrieve_secret(os.environ['GCP_SECRET_IB_PASSWORD']) - - @classmethod - def trade_mode(cls): - # if not cls.isEnabledGCPSecret(): - return os.environ['TRADE_MODE'] - # return cls.retrieve_secret(os.environ['GCP_SECRET_IB_TRADE_MODE']) - - \ No newline at end of file diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..96bd699 --- /dev/null +++ b/start.sh @@ -0,0 +1,57 @@ +#!/bin/bash +set -e + +echo "Starting Xvfb..." +rm -f /tmp/.X0-lock +/usr/bin/Xvfb "$DISPLAY" -ac -screen 0 1024x768x16 +extension RANDR >&1 & + +echo "Waiting for Xvfb to be ready..." +while ! xdpyinfo -display "$DISPLAY"; do + echo -n '' + sleep 0.1 +done + +echo "Xvfb is ready" +echo "Setup port forwarding..." + +socat TCP-LISTEN:$IBGW_PORT,fork TCP:localhost:4001,forever >&1 & +echo "*****************************" + +# python /root/bootstrap.py + +# echo "IB gateway is ready." + +#Define cleanup procedure +cleanup() { + pkill java + pkill Xvfb + pkill socat + echo "Container stopped, performing cleanup..." +} + +#Trap TERM +trap 'cleanup' INT TERM +echo "IB gateway starting..." + +set_java_heap() { + # set java heap size in vm options + if [ -n "${JAVA_HEAP_SIZE}" ]; then + _vmpath="${TWS_PATH}/ibgateway/${IB_GATEWAY_VERSION}" + _string="s/-Xmx([0-9]+)m/-Xmx${JAVA_HEAP_SIZE}m/g" + sed -i -E "${_string}" "${_vmpath}/ibgateway.vmoptions" + echo "Java heap size set to ${JAVA_HEAP_SIZE}m" + else + echo "Usign default Java heap size." + fi +} + +# Java heap size +set_java_heap + +# start rest api for healthcheck +healthcheck-rest >&1 & + +${IBC_PATH}/scripts/ibcstart.sh "1019" -g \ + "--ibc-path=${IBC_PATH}" "--ibc-ini=${IBC_INI}" \ + "--user=${IB_ACCOUNT}" "--pw=${IB_PASSWORD}" "--mode=${TRADING_MODE}" \ + "--on2fatimeout=${TWOFA_TIMEOUT_ACTION}" diff --git a/test/test_docker_interactive.py b/test/test_docker_interactive.py index f604459..34824bb 100644 --- a/test/test_docker_interactive.py +++ b/test/test_docker_interactive.py @@ -8,22 +8,25 @@ IMAGE_NAME = os.environ['IMAGE_NAME'] @pytest.fixture(scope='function') -def ib_docker(): +def ib_docker(request): account = os.environ['IB_ACCOUNT'] password = os.environ['IB_PASSWORD'] - trade_mode = os.environ['TRADE_MODE'] + trading_mode = os.environ['TRADING_MODE'] # run a container docker_id = subprocess.check_output( ['docker', 'run', '--env', 'IB_ACCOUNT={}'.format(account), '--env', 'IB_PASSWORD={}'.format(password), - '--env', 'TRADE_MODE={}'.format(trade_mode), + '--env', 'TRADING_MODE={}'.format(trading_mode), '-p', '4002:4002', - '-d', IMAGE_NAME, - "tail", "-f", "/dev/null"]).decode().strip() + '-d', IMAGE_NAME]).decode().strip() + + # at the end of the test suite, destroy the container + def remove_container(): + subprocess.check_call(['docker', 'rm', '-f', docker_id]) + request.addfinalizer(remove_container) yield docker_id - subprocess.check_call(['docker', 'rm', '-f', docker_id]) def test_ibgw_interactive(ib_docker): diff --git a/test/test_ib_gateway.py b/test/test_ib_gateway.py index 22f7a97..d4d36b8 100644 --- a/test/test_ib_gateway.py +++ b/test/test_ib_gateway.py @@ -2,50 +2,41 @@ import subprocess import testinfra import os +import time +import requests IMAGE_NAME = os.environ['IMAGE_NAME'] -# scope='session' uses the same container for all the tests; -# scope='function' uses a new container per test function. -@pytest.fixture(scope='session') -def host(request): +def test_healthcheck(): account = os.environ['IB_ACCOUNT'] password = os.environ['IB_PASSWORD'] - trade_mode = os.environ['TRADE_MODE'] + trading_mode = os.environ['TRADING_MODE'] # run a container docker_id = subprocess.check_output( ['docker', 'run', '--env', 'IB_ACCOUNT={}'.format(account), '--env', 'IB_PASSWORD={}'.format(password), - '--env', 'TRADE_MODE={}'.format(trade_mode), - '-d', IMAGE_NAME, - "tail", "-f", "/dev/null"]).decode().strip() - # return a testinfra connection to the container - yield testinfra.get_host("docker://" + docker_id) - # at the end of the test suite, destroy the container + '--env', 'TRADING_MODE={}'.format(trading_mode), + '-d', IMAGE_NAME]).decode().strip() + time.sleep(30) + assert subprocess.check_call(['docker', 'exec', docker_id, 'healthcheck']) == 0 subprocess.check_call(['docker', 'rm', '-f', docker_id]) -def test_ibgateway_version(host): - int(host.run("ls /root/Jts/ibgateway").stdout) - -def test_ib_connect(host): - script = """ -from ib_insync import * -from concurrent.futures import TimeoutError +def test_healthcheck_rest(): + account = os.environ['IB_ACCOUNT'] + password = os.environ['IB_PASSWORD'] + trading_mode = os.environ['TRADING_MODE'] -ib = IB() -wait = 60 -while not ib.isConnected(): - try: - IB.sleep(1) - ib.connect('localhost', 4002, clientId=999) - except (ConnectionRefusedError, OSError, TimeoutError): - pass - wait -= 1 - if wait <= 0: - break -ib.disconnect() -""" - cmd = host.run("python -c \"{}\"".format(script)) - assert cmd.rc == 0 + # run a container + docker_id = subprocess.check_output( + ['docker', 'run', + '--env', 'IB_ACCOUNT={}'.format(account), + '--env', 'IB_PASSWORD={}'.format(password), + '--env', 'TRADING_MODE={}'.format(trading_mode), + '-p', '8080:8080', + '-d', IMAGE_NAME]).decode().strip() + time.sleep(30) + response = requests.get("http://127.0.0.1:8080/healthcheck") + assert response.ok + subprocess.check_call(['docker', 'rm', '-f', docker_id]) diff --git a/test/test_ib_gateway_fail.py b/test/test_ib_gateway_fail.py index 6c89536..61b622e 100644 --- a/test/test_ib_gateway_fail.py +++ b/test/test_ib_gateway_fail.py @@ -3,6 +3,8 @@ import testinfra import os import time +import requests +from ib_insync import IB, util, Forex IMAGE_NAME = os.environ['IMAGE_NAME'] @@ -12,28 +14,54 @@ def host(request): account = 'test' password = 'test' - trade_mode = 'paper' + trading_mode = 'paper' # run a container docker_id = subprocess.check_output( ['docker', 'run', '--env', 'IB_ACCOUNT={}'.format(account), '--env', 'IB_PASSWORD={}'.format(password), - '--env', 'TRADE_MODE={}'.format(trade_mode), - '-d', IMAGE_NAME, - "tail", "-f", "/dev/null"]).decode().strip() + '--env', 'TRADING_MODE={}'.format(trading_mode), + '-p', '8080:8080', + '-d', IMAGE_NAME]).decode().strip() + + # at the end of the test suite, destroy the container + def remove_container(): + subprocess.check_call(['docker', 'rm', '-f', docker_id]) + request.addfinalizer(remove_container) + # return a testinfra connection to the container yield testinfra.get_host("docker://" + docker_id) - # at the end of the test suite, destroy the container - subprocess.check_call(['docker', 'rm', '-f', docker_id]) - -def test_ib_connect_fail(host): - script = """ -from ib_insync import * -IB.sleep(60) -ib = IB() -ib.connect('localhost', 4001, clientId=1) -ib.disconnect() -""" - cmd = host.run("python -c \"{}\"".format(script)) - assert cmd.rc != 0 + + +def test_ib_insync_connect_fail(host): + try: + ib = IB() + wait = 60 + while not ib.isConnected(): + try: + IB.sleep(1) + ib.connect('localhost', 4002, clientId=999) + except: + pass + wait -= 1 + if wait <= 0: + break + + contract = Forex('EURUSD') + bars = ib.reqHistoricalData( + contract, endDateTime='', durationStr='30 D', + barSizeSetting='1 hour', whatToShow='MIDPOINT', useRTH=True) + assert False + except: + pass + +def test_healthcheck_fail(host): + time.sleep(30) + assert host.exists("healthcheck") + assert host.run('/healthcheck/bin/healthcheck').rc == 1 + +def test_healthcheck_rest_fail(host): + time.sleep(30) + response = requests.get("http://127.0.0.1:8080/healthcheck") + assert not response.ok \ No newline at end of file