Skip to content

Commit a42f5be

Browse files
committed
Prepare stable public release and repair order reconciliation
1 parent d6f9893 commit a42f5be

18 files changed

Lines changed: 1126 additions & 656 deletions

File tree

‎.dockerignore‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
1-
data/
1+
**
2+
!go.mod
3+
!go.sum
4+
!cmd/
5+
!cmd/**
6+
!exchange/
7+
!exchange/**
8+
!service/
9+
!service/**
10+
!storage/
11+
!storage/**

‎.github/workflows/ci.yml‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
workflow_dispatch:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
verify:
14+
name: verify
15+
runs-on: ubuntu-latest
16+
steps:
17+
- name: Check out the triggering commit
18+
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
19+
with:
20+
ref: ${{ github.sha }}
21+
22+
- name: Set up Go
23+
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
24+
with:
25+
go-version-file: go.mod
26+
27+
- name: Download Go modules
28+
run: go mod download
29+
30+
- name: Verify Go formatting
31+
shell: bash
32+
run: |
33+
unformatted="$(git ls-files -z -- '*.go' | xargs -0 gofmt -l)"
34+
if [[ -n "$unformatted" ]]; then
35+
printf 'The following Go files are not gofmt-formatted:\n%s\n' "$unformatted"
36+
exit 1
37+
fi
38+
39+
- name: Vet
40+
run: go vet ./...
41+
42+
- name: Test with race detector
43+
run: go test -race ./...
44+
45+
- name: Build
46+
run: go build ./...
47+
48+
- name: Check shell script syntax
49+
shell: bash
50+
run: git ls-files -z -- '*.sh' | xargs -0 -r -n1 bash -n
51+
52+
- name: Build Docker image
53+
run: docker build --tag binancetrader:${{ github.sha }} .

‎.gitignore‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ go.work
1717
.env
1818
.env.*
1919
!.env.example
20+
.private/
21+
/vendor/
2022

2123
# Python virtual environment
2224
.venv/
@@ -29,6 +31,10 @@ __pycache__/
2931
*.db-wal
3032
*.db-shm
3133
data.sqlite
34+
*.duckdb
35+
*.duckdb.wal
36+
/prediction-markets-archive/data/
37+
/prediction-markets-archive/db/
3238

3339
# IDE
3440
.idea/

‎CLAUDE.md‎

Lines changed: 35 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,101 +1,51 @@
1-
# CLAUDE.md
1+
# Repository development guide
22

3-
This file provides guidance when working with code in this repository.
3+
## Purpose and layout
44

5-
## Project Overview
5+
A single Go service runs the Binance spot buy-low/sell-high strategy across configured pairs. `cmd/binancetrader` loads configuration and serves `/health`, `/ready`, and `/metrics`; `exchange` wraps the Binance SDK; `service` manages reconciliation and order placement; `storage` persists JSON in Redis. `prediction-markets-archive` contains independent public-data utilities, not trading inputs.
66

7-
binancetrader is an automated trading bot for Binance. A single service orchestrates all configured trading pairs with a 1-minute main loop, debounced by async events (websocket, webhooks). Strategy: buy-low/sell-high — place GTC limit buys below market, sell at take-profit. Orders have manual expiry management (Binance spot has no GTT).
8-
9-
## Build & Development Commands
10-
11-
```bash
12-
make run # Run locally with race detector
13-
make test # fmt + vet + race-detector tests
14-
make build-docker # Build Docker image
15-
make run-docker # Build and run in Docker (host networking)
16-
```
17-
18-
Run a single test:
19-
```bash
20-
go test -race -run TestName ./service/...
21-
```
22-
23-
24-
## Architecture
7+
## Commands
258

9+
```sh
10+
make run # local application with race detector; requires configured .env and Redis
11+
make test # formatting check, vet, race-detector regression tests
12+
make build # compile packages
13+
make build-docker # non-root image with VERSION and Git revision labels
14+
make run-docker # Linux host networking, configured .env and external Redis
2615
```
27-
cmd/binancetrader/main.go → Entry point, config loading, HTTP server (Chi on :8123)
28-
Routes: /health, /ready, /metrics
29-
Parses ENABLED_PAIRS (BTC/USDT → BTCUSDT), creates
30-
Binance client, single Service for all pairs
3116

32-
exchange/binance.go → Binance spot client (wraps github.com/adshao/go-binance/v2)
33-
- Public: Ping, ServerTime, TickerPrice, Klines
34-
- Authenticated: Account, CreateOrder, GetOrder,
35-
CancelOrder, ListOpenOrders
36-
- Spot() exposes underlying go-binance client
37-
- WithTestnet() option for testnet keys
38-
- WebSocket intentionally excluded (go-binance#800
39-
data race); will build our own WS layer later
17+
Use Go modules, not a committed vendor directory. Update `go.mod` and `go.sum` together. Go 1.26 is the minimum toolchain; CI reads it from `go.mod`.
4018

41-
service/service.go → Single orchestrator for all configured pairs
42-
- 1-minute ticker triggers main loop
43-
- triggerCh (buffered 16) for async events
44-
- Async events reset ticker (debounce)
45-
- Inject() method for external event sources
46-
- tick() iterates all pairs, processPair() per pair
47-
- exchangeClient interface: Ping, TickerPrice,
48-
CreateOrder, GetOrder, CancelOrder, ListOpenOrders, Spot
49-
- storageClient interface: Close, Set, Get, Delete, List
19+
## Trading invariants
5020

51-
service/types.go → Domain types
52-
- PairConfig{Symbol, Base, Quote}
53-
- OrderRecord — local mirror of Binance order
54-
Key: orders:{symbol}:{orderID}
55-
- Position — inventory per symbol (one max)
56-
Key: positions:{symbol}
57-
- SymbolFilters — cached lot/price filter params
58-
- All financial fields use shopspring/decimal
21+
- One process owns each account/managed symbol set and dedicated durable Redis database. No replicas or external ledger mutations.
22+
- Persist `intents:{symbol}` before submitting to Binance with a unique client order ID. Recover by that ID; never automatically resubmit an uncertain order.
23+
- `orders:{symbol}:{orderID}` stores cumulative executed base and quote amounts. Replay snapshots in order to derive average-cost inventory. `positions:{symbol}` is only a derived cache.
24+
- Any active order blocks further placement on that symbol, including partially filled buys. Unmanaged exchange orders block buys and sells.
25+
- Reconcile expiry cancellation against final exchange state before deriving inventory. Partial sells must preserve unsold inventory.
26+
- Malformed state, storage errors, unexpected statuses, and uncertain exchange outcomes pause trading. Never turn an error into an empty order set.
27+
- Startup rejects unversioned legacy trading state and database/account/environment mismatches. Never bypass these guards to make startup succeed. See README recovery instructions.
28+
- Dry-run only previews buy intentions: no order endpoints, synthetic orders, or trading-state writes. Startup still requires authenticated API access and Redis.
29+
- Use decimal arithmetic; floor price and quantity to exchange increments. Never increase the configured budget to satisfy a minimum.
30+
- Fees are not deducted from tracked inventory. Base-asset commissions, dust, and insufficient balances can require manual reconciliation; do not claim production trading safety.
5931

60-
service/strategy.go → Buy-low/sell-high strategy
61-
- processPair(): fetch price → syncOrders →
62-
checkExpiredOrders → evaluate state → place order
63-
- syncOrders(): reconcile DB vs Binance open orders,
64-
handle fills (create/delete positions)
65-
- placeBuyOrder(): market × (1-BUY_OFFSET), GTC limit
66-
- placeSellOrder(): entry × (1+TAKE_PROFIT), GTC limit
67-
- checkExpiredOrders(): cancel GTC > ORDER_EXPIRY
68-
- Symbol filters cached per symbol (via Spot() escape hatch),
69-
includes MinNotional validation
70-
- Immediate fills handled inline (no wait for next tick)
71-
- DRY_RUN: logs intent, saves synthetic order records
72-
- Rounding: roundToTickSize, roundToStepSize (floor)
32+
The exchange SDK exposes `Spot()` for metadata and client-ID recovery. Websocket transport is not implemented. Strategy research in `docs/` is not necessarily implemented behavior.
7333

74-
storage/client.go → Generic Redis/DragonFly JSON store
75-
- Key scheme: {table}:{id}
76-
- CRUD: Set, Get, Delete, Exists, List
77-
- Reusable for any record type
78-
```
34+
## Verification
7935

80-
## Configuration
36+
Service regression tests exercise the real SDK against a loopback HTTP fixture, never live trading credentials. Keep tests deterministic and isolated. Reproduce reconciliation bugs before fixing them; validate failure/recovery transitions and inventory rather than internal wiring. `make test` must pass, as must the Docker build and CI on the intended release commit.
8137

82-
Environment variables loaded from `.env` (see `.env.example`). Key vars:
83-
- `ENV_MODE`: `dev` (DEBUG logs) or `prod` (INFO logs)
84-
- `REDIS_URL`: DragonFly/Redis connection string
85-
- `BINANCE_API_KEY` / `BINANCE_API_SECRET`: Binance API credentials
86-
- `BINANCE_MODE`: `live`, `demo`, or `testnet`
87-
- `ENABLED_PAIRS`: Comma-separated trading pairs, format `BASE/QUOTE` (e.g., `BTC/USDT,ETH/USDT`)
88-
- `DRY_RUN`: `true` (default) disables real order placement
89-
- `BUY_OFFSET`: Decimal, how far below market to buy (default `0.001` = 0.1%)
90-
- `BUY_QUANTITY_USDT`: Decimal, USDT amount per buy order (default `5`)
91-
- `TAKE_PROFIT`: Decimal, sell target above entry (default `0.01` = 1%)
92-
- `ORDER_EXPIRY`: Go duration, cancel stale GTC orders (default `1h`)
38+
Keep `.env`, `.private/`, database dumps, local logs, and generated archive data out of Git and Docker contexts. Never add credentials, personal infrastructure, or authorship/co-author credits for development tools.
9339

94-
## Docs
40+
## Required release workflow
9541

96-
- [docs/fee-analysis.md](docs/fee-analysis.md) — Binance fee breakdown, breakeven math, config presets for scalping profitability
97-
- [docs/scalping-strategy.md](docs/scalping-strategy.md) — Volatility analysis, tiered drawdown response (re-anchor / park), capital budgeting
42+
Every shipped change must be versioned, tagged, and named in a GitHub release. Use stable SemVer: patch for fixes/documentation, minor for compatible additions, major for breaking changes. `VERSION` is the source of truth for package releases and Makefile image labels; independently versioned dependencies retain their own versions.
9843

99-
## Testing Patterns
44+
1. Make and verify the change; update README/config examples when behavior changes.
45+
2. Commit the change on `main` using the maintainer's intended public Git identity.
46+
3. Write accurate release notes in `.private/` or outside the repository, including compatibility changes, verification, and remaining risks.
47+
4. Run `make release RELEASE_VERSION=X.Y.Z RELEASE_NAME="Descriptive release name" RELEASE_NOTES=.private/release-notes.md`.
48+
5. `scripts/release.sh` bumps/commits `VERSION`, pushes `main`, waits for successful CI on that exact SHA, verifies remote `main`, then creates and pushes an annotated `vX.Y.Z` tag and publishes a stable, non-draft release using `gh`.
49+
6. Verify the published release and tag target. If a tag or release already exists, stop and investigate; never move it silently. A failure after tag push requires manual release completion, not retagging.
10050

101-
Tests use table-driven style. Service tests should use interface mocks for the exchange client and storage layer.
51+
No release should precede successful CI on its exact commit. The workflow does not change repository visibility.

‎Dockerfile‎

Lines changed: 22 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,27 @@
1-
FROM golang:1.26.0-alpine AS builder
2-
3-
ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64 GO111MODULE=on
1+
FROM golang:1.26-alpine AS builder
42

53
WORKDIR /build
6-
7-
# Copy go.mod and go.sum first (cached layer unless dependencies change)
84
COPY go.mod go.sum ./
9-
10-
# Download dependencies (cached layer)
115
RUN go mod download
12-
13-
# Copy source code (this layer changes frequently)
14-
COPY . .
15-
16-
# Build the binancetrader service
17-
RUN go build -ldflags="-s -w" -o binancetrader ./cmd/binancetrader
18-
19-
# Slim final image
20-
FROM alpine:latest
21-
22-
EXPOSE 8123
23-
24-
COPY --from=builder /build/binancetrader /usr/bin/
25-
26-
RUN addgroup -S binancetrader && \
27-
adduser -S -G binancetrader -s /sbin/nologin binancetrader && \
28-
chmod +x /usr/bin/binancetrader
29-
6+
COPY cmd/ cmd/
7+
COPY exchange/ exchange/
8+
COPY service/ service/
9+
COPY storage/ storage/
10+
RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /bin/binancetrader ./cmd/binancetrader
11+
12+
FROM alpine:3.24
13+
RUN apk add --no-cache ca-certificates && \
14+
addgroup -S binancetrader && \
15+
adduser -S -G binancetrader binancetrader
16+
COPY --from=builder /bin/binancetrader /usr/local/bin/binancetrader
3017
USER binancetrader
31-
32-
ENTRYPOINT ["/usr/bin/binancetrader"]
33-
18+
ENV LOG_FILE="-"
19+
EXPOSE 8123
20+
ARG APP_VERSION=dev
21+
ARG GIT_COMMIT=unknown
22+
LABEL org.opencontainers.image.title="binancetrader" \
23+
org.opencontainers.image.description="Binance spot trading bot and prediction-market archive utilities" \
24+
org.opencontainers.image.source="https://github.com/foae/binancetrader" \
25+
org.opencontainers.image.version="$APP_VERSION" \
26+
org.opencontainers.image.revision="$GIT_COMMIT"
27+
ENTRYPOINT ["/usr/local/bin/binancetrader"]

‎Makefile‎

Lines changed: 15 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,48 +1,25 @@
1-
SERVICE_NAME := binancetrader
2-
IMAGE_NAME = github.com/foae/binancetrader
3-
VERSION := latest
4-
GIT_COMMIT_SHA = $(shell git describe --tags --always --dirty)
5-
BUILD_ARGS := --build-arg GIT_COMMIT=$(shell git show -s --format=%H) \
6-
--build-arg APP_VERSION="$(shell git log -1 --date=format:%Y%m%d%H%M%S --format=%cd-%h)" \
7-
--build-arg GIT_COMMIT_DATE="$(shell git show -s --format=%ci)" \
8-
--build-arg BUILD_DATE=$(shell date +"%s") \
9-
--build-arg HOST_MACHINE="$(shell hostname)" \
10-
--build-arg IMAGE_NAME="$(IMAGE_NAME)"
1+
IMAGE_NAME ?= binancetrader
2+
VERSION := $(shell cat VERSION)
3+
GIT_COMMIT := $(shell git rev-parse HEAD)
114

12-
.PHONY: clean-logs
13-
clean-logs:
14-
cat /dev/null > ./binancetrader.log
15-
printf "\n=== Cleaned up binancetrader.log file\n"
16-
17-
.PHONY: run
5+
.PHONY: run test build build-docker run-docker release
186
run:
19-
go run -race ./cmd/binancetrader/main.go
20-
21-
.PHONY: run-logged
22-
run-logged: clean-logs
23-
go run -race ./cmd/binancetrader/main.go >> binancetrader.log 2>&1
7+
go run -race ./cmd/binancetrader
248

25-
.PHONY: test
269
test:
27-
go fmt ./...
10+
@test -z "$$(gofmt -l cmd exchange service storage)" || { echo "Run gofmt on the listed source directories"; exit 1; }
2811
go vet ./...
2912
go test -race ./...
3013

31-
.PHONY: docker-build
14+
build:
15+
go build -trimpath -o binancetrader ./cmd/binancetrader
16+
3217
build-docker:
33-
docker buildx build \
34-
--cache-to type=inline \
35-
--load \
36-
$(BUILD_ARGS) \
37-
-f Dockerfile \
38-
-t $(IMAGE_NAME):$(VERSION) .
18+
docker build --build-arg APP_VERSION="$(VERSION)" --build-arg GIT_COMMIT="$(GIT_COMMIT)" -t $(IMAGE_NAME):$(VERSION) .
3919

40-
.PHONY: docker-run
41-
run-docker: docker-build
42-
@echo "Starting $(SERVICE_NAME) in Docker..."
43-
docker run -it --rm \
44-
--name $(SERVICE_NAME) \
45-
--network=host \
46-
$(shell test -f .env && echo "--env-file .env") \
47-
$(IMAGE_NAME):$(VERSION)
20+
run-docker:
21+
docker compose up --build
4822

23+
# Example: make release RELEASE_VERSION=1.0.1 RELEASE_NAME="Reconciliation fixes" RELEASE_NOTES=release-notes.md
24+
release:
25+
bash scripts/release.sh "$(RELEASE_VERSION)" "$(RELEASE_NAME)" "$(RELEASE_NOTES)"

0 commit comments

Comments
 (0)