A quantitative trading system for Binance that starts as a rule-based bot and is built to grow into a decision-making agent: real-time market ingestion via WebSockets, a technical-signal engine, risk management, and order execution today, with sentiment-aware and self-tuning strategy agents on the roadmap -- evolving from a set of standalone scripts into a service architecture with transactional persistence, cached state, and a control panel.
Current status: project under active refactoring. REAL_TRADES=False (simulation mode) until Phase 1 is complete and the strategy has been validated through backtesting. Do not trade real capital before then.
- Overview
- Trading strategy
- Technology stack
- Phased roadmap
- Known bugs (Phase 1)
- Target folder structure
- Local setup
The system today consists of two independent processes that communicate through module import (not over the network), running as threads inside the same Python process and sharing state through global variables:
| Component | Current file | Responsibility |
|---|---|---|
| Market Scanner | wy_function_winners_list_v03.py |
Subscribes to Binance's !ticker@arr stream, ranks all USDT pairs by 24h volume (quoteVolume), and exposes the Top N via get_updated_symbols(). |
| Trading Engine | wy_multicoin__v47.py |
Consumes the symbol list, maintains 1m candles per symbol (klines plus tick-by-tick trades over a multi-stream channel), computes technical indicators, ranks by score, executes buys/sells, manages stops, and sends Telegram notifications. |
| Emergency Brake | wy_autosell_module_test.py (imported, not included in this repo) |
Exposes get_conditions() to force manual stop-loss/trailing-stop closes from outside the main engine. |
- Trading universe: Top 20 USDT symbols by 24h volume (liquidity filter), refreshed in real time by the Market Scanner.
- Entry signal: bullish MACD crossover over the signal line (DEA) -- MACD moves from below to above DEA on the most recently closed candle (
get_macd_crossover_signal). - Entry is only evaluated for symbols within the Top 3 of the score ranking at the moment their candle closes (
rankings[:3]). - Position size:
CAPITAL / MAX_TRADES_OPEN, minus a 1.5% fee, rounded down to Binance's minimum lot size.
Multiplicative/additive scoring based on:
- ADX (trend strength):
>50x3,>30x2,>25x1, otherwise discard (x0). - +DI vs -DI: discard if there is no bullish predominance.
- RSI: bonus for the neutral zone (30-70) x2, penalty for overbought/oversold x0.5.
- Bollinger slope (SMA): discard if the slope isn't positive; a strong slope (
>2) additionally requires ADX above the minimum and RSI below 70. - Relative volume: +1 if current volume exceeds the 20-candle average by more than 20%.
- MACD/DEA: x1.2 if MACD>DEA and MACD>0; x0.9 if MACD<0.
- ATR: x0.95 if ATR>3 (penalizes excessive volatility).
Note: the min_score=7 threshold is not empirically calibrated -- it is an arbitrary value pending validation through backtesting (see Phase 3).
The position is closed if any of the following occurs:
- Price touches the Bollinger upper band (take profit).
- RSI(12) > 70 (overbought).
- ADX < 20 and price near/below the SMA (loss of trend).
- Fixed stop-loss: -2% from entry price (
STOP_LOSS_PERCENT). - Trailing stop: -5% from the highest price reached since entry (
TRAILING_STOP_PERCENT). - External manual conditions (
get_conditions()viawy_autosell_module_test). - (Optional, currently disabled via flags) Live tick-based stop-loss, two variants of a live staged trailing stop, dynamic ATR-based stop (
ENABLE_LIVE_STOPLOSS,ENABLE_LIVE_TRAILING_STOP_1/2-- currentlyFalse).
- Market regime detector (ranging vs. trending) using ADX on a higher timeframe (1h/4h) -- MACD wins in trending markets, Bollinger wins in ranging markets; the detector decides which to use at each moment.
- Beta vs. BTC: relative risk filter; block altcoin buys with high beta when BTC is falling sharply.
- Backtesting engine: validate the strategy and calibrate
min_scoreand risk parameters against historical data before risking real capital.
- Python 3,
websocket-client,pandas,python-binance,python-telegram-bot,python-dotenv - Persistence: JSON files (
trade_history,open_trades,error_log,real_operations_errors) and CSV (candles, indataframes__v47/) - Dependency management: no formal lockfile
| Piece | Use | Why |
|---|---|---|
| Python 3.11+ / asyncio | Central engine | Unifies WebSockets, strategy execution, and the API without blocking between tasks (today send_telegram_message_sync creates a new event loop on every call from a synchronous thread). |
| FastAPI | Control panel | Endpoints to view open trades, pause/resume the bot, adjust parameters without restarting the process. Requires authentication from day one -- it exposes control over real orders. |
| PostgreSQL + SQLAlchemy/SQLModel | Transactional persistence | Trade history, error logs, performance metrics (PnL, win rate), historical candles for backtesting. |
| Redis | Shared state across processes | Live prices and open trades, designed for when the scanner and the strategy engine run as separate processes (not just threads within one process) -- that is where it stops being cosmetic and actually resolves the decoupling, and eliminates the current race condition on open_trades/dataframes. |
| Docker / docker-compose | Reproducible environment | Bring up Postgres + Redis + the app with a single command, same environment on any machine. |
pyproject.toml |
Dependency and build management | Replaces a loose requirements.txt; reproducible environment with a lockfile (uv/poetry), project metadata, and tool configuration (ruff, pytest) in a single file. |
logging / loguru |
Observability | Replaces the ~60 print() calls in the current code; levels, file rotation, structured logs. |
| Piece | Use | Why |
|---|---|---|
pgvector (reuses the Phase 2 Postgres instance) |
Sentiment/news vector store | Embeds ingested news, social posts, and on-chain alerts so the RAG pipeline can retrieve the most relevant context for a symbol before scoring its sentiment -- no separate vector database service to operate. |
LLM provider (pluggable via an LLM_PROVIDER-style factory, same pattern as the exchange client) |
Sentiment classification and the strategy decision agent | Classifies retrieved text into a sentiment signal and, separately, chooses which strategy component should be active for current conditions. Kept behind a factory so the provider can change without touching trading/. |
| News/social/on-chain APIs (e.g. a crypto news aggregator, a fear-and-greed index endpoint) | Sentiment data ingestion | Raw input for the RAG pipeline; each source is fetched, chunked, and embedded before being queried. |
DEAP (or a hand-rolled genetic algorithm module) |
Strategy parameter evolution | Evolves strategy parameters (score weights, stop-loss/trailing-stop percentages, indicator periods) against the Phase 3 backtesting engine as the fitness function -- a parsimonious choice over a full ML framework, since the search space here is a fixed set of numeric parameters, not a learned function. |
Goal: make the current system correct and safe in simulation mode before touching the architecture. See Known bugs. No new infrastructure is added until this phase is closed.
- Migrate to
pyproject.toml. - Replace
print()withlogging. - Remove
generate_and_run_crypto_script(script generation viasubprocess). - Introduce
threading.Lock(or migrate toasyncio) for shared state (open_trades,dataframes,SYMBOLS). docker-compose.ymlwith Postgres + Redis.- Data models in Postgres (trades, logs, metrics) and migration from JSON/CSV.
- Redis layer for live prices and open trades, with the scanner and the engine as separate processes.
- FastAPI with basic authentication: status, open trades, pause/resume, adjust parameters.
- WebSocket reconnection with exponential backoff in both scripts (the main engine already reconnects with a fixed 5s retry; it still needs exponential backoff and a unified approach with the scanner, which currently does not reconnect at all).
- Regime detector (ranging vs. trending).
- Beta vs. BTC calculation as a risk filter.
- Backtesting engine over historical data in Postgres.
- Empirical calibration of
min_scoreand risk parameters against backtesting results.
Goal: give the bot a signal that plain price/volume indicators cannot see -- news, social sentiment, and on-chain events -- and let an agent act on it, without letting that agent bypass the existing risk layer.
- Sentiment ingestion: scheduled API calls to news/social/on-chain sources per tracked symbol, stored as raw text with a timestamp and source.
- Physics-Based RAG & Vector Search: Designed a dynamic market-regime detector that maps multidimensional (3D) market structure into vector embeddings (pgvector), retrieves the physical model matched to the closest historical regime, and feeds that context to an LLM strategy-selection agent that decides which numerical strategy to deploy.
- Sentiment scoring: an LLM call classifies the retrieved context into a sentiment signal (e.g. bullish/neutral/bearish, or a bounded numeric score) for the symbol under evaluation.
- Circuit-breaker agent: monitors sentiment and news-event signals and can veto new entries or force-close existing positions on a symbol -- for example, on a detected exchange hack, a regulatory action, a stablecoin depeg, or a sharp sentiment-price divergence. This agent can only ever make trading more conservative (block or close), never open a position or override the risk layer's caps; every halt decision is logged with its trigger and reasoning, the same way a rejected order is logged today.
- Strategy decision agent: given the current regime (Phase 3), the indicator scores (existing
strategy.py), and the sentiment signal (this phase), chooses which strategy component should be active (e.g. MACD-trend vs. Bollinger-range vs. sitting out) instead of that choice being hardcoded. Its output is a strategy selection, not an order -- it still goes through the normal entry/exit and risk logic. - Both agents are advisory/gating signals that feed into the existing
trading/strategy.pyandtrading/risk.pylayer; neither one calls the exchange client directly, following the same separation-of-concerns rule as the rest of the trading layer. - This phase is design/prototype status, same as Phase 3's regime detector was before validation -- ship the ingestion and scoring first, validate the sentiment signal's actual predictive value against historical data, and only then wire the circuit-breaker into live decisions.
Goal: replace hand-tuned strategy parameters (the currently arbitrary min_score=7, the fixed 2%/5% stop-loss/trailing-stop, indicator periods) with values found by evolving them against real backtest performance.
- Encode a strategy configuration (score weights, thresholds, stop-loss/trailing-stop percentages, indicator periods) as a genome.
- Fitness function: run the Phase 3 backtesting engine on historical data for a candidate genome and score it on a risk-adjusted metric (e.g. profit factor or Sharpe ratio adjusted for max drawdown, not raw return alone -- raw return rewards reckless parameter sets).
- Evolve a population of configurations over generations (selection, crossover, mutation) using
DEAPor an equivalent library. - Runs entirely offline against historical data, out-of-band from live trading, mirroring how the ML comparison track is meant to work in the other project's roadmap -- its output is a candidate parameter set for a human to review, never a set of parameters auto-applied to the live bot.
- Any winning configuration must still pass the same Phase 3 validation step (backtest report reviewed before use) before being adopted -- a genetic algorithm optimizing against historical data can overfit to that specific history, so a proposed configuration is a hypothesis to validate out-of-sample, not a result to trust directly.
calculate_total_profitnever reads the correct file. It is called withfile_path = "TRADE_HISTORY_FILE"(a string literal, line 374) instead of theTRADE_HISTORY_FILEvariable. It always fails with "File does not exist" on module startup.execute_tradeextracts the symbol incorrectly in the SELL branch without a quantity. Line 909: callsget_sell_quantity(symbol.split('USDT')[0]), butget_sell_quantityalready doessymbol.replace("USDT", "")internally -- passing the already-stripped asset (e.g."BTC"instead of"BTCUSDT") corrupts the balance lookup. (Note: insideexecute_trade, in theREAL_TRADESbranch for SELL, line 933, the same incorrect pattern is repeated.)round_quantityis defined but never used. Quantities are never rounded to Binance'sstepSizebefore sending the order -- a real risk of order rejection due to invalid precision whenREAL_TRADES=True. OnlyminQty(minimum lot) is validated, notstepSize.- RSI can divide by zero. In
calculate_rsi(line 613), iflosses == 0within the window,rs = gains / lossesproducesinforNaNwith no explicit handling, andrsi.iloc[-1]can propagateNaNintocalculate_score. calculate_scoretreatsrsi == 0as falsy. The conditionif rsi:(line 679) skips the block whenrsiis exactly0, treating it as "no data" instead of a valid extreme-oversold value.- Inconsistent discard pattern in the score. It mixes
return 0.0(short-circuits the whole function, lines 695/701) withscore *= 0(a multiplication that could theoretically be "revived" if the evaluation order changed, lines 667/675) for the same type of discard condition -- inconsistent behavior that is hard to audit or test. close_all_open_tradescloses at the entry price, not the current price. Line 1440: usestrade["price"]as the close price, which reports a false PnL (~0) on emergency closes triggered by a WebSocket failure. In addition, this function is not currently called fromon_close(it is commented out on line 1486), so a connection close no longer forces positions to close -- this needs to be a deliberate decision, not an accident.- No lock between threads for shared state.
open_trades,dataframes, andtrade_historyare read and written from the main WebSocket thread and the volume-scanner thread without athreading.Lock, exposed to race conditions (currently mitigated only by the GIL, not by design). - Disk I/O in the hot path of price updates.
check_stops_for_tradecallssave_trade_history()(a full JSON write, line 1117) every timehighest_priceis updated, which can happen on every closed kline -- unnecessary performance impact and disk wear. - The scanner's reconnection thread is duplicated on every retry.
run_bot()(line 1501) relaunchesthreading.Thread(target=start_websocket, daemon=True).start()every time it is called, including on every reconnection triggered fromon_close()-- accumulating duplicate volume-scanner threads across successive reconnections of the main WebSocket. send_telegram_message_synccreates a new event loop on every call (asyncio.run(...), line 400) from the WebSocket thread -- unnecessary overhead and a risk of momentarily blocking the thread that processes market messages right when a position opens or closes.generate_and_run_crypto_script(line 200) spawns a new Python process per symbol by writing a.pyfile to disk and running it viasubprocess.Popen. It is not active in the current flow (calls are commented out on lines 923 and 947), but it is technical debt and a real risk surface if reactivated unintentionally.- No handling of Binance REST API rate limits in synchronous calls within hot paths (
get_symbol_info,get_asset_balance, called on everyexecute_trade/open_position). retriesis a global counter that is never reset after a successful reconnection. Line 1469: once theMAX_RETRIES=60reconnection attempts accumulated over the entire lifetime of the process are exhausted (not per disconnection episode), the bot stops reconnecting permanently, even if it ran stably for days in between.
on_closedoes not reconnect. It only doesprint("WebSocket closed"). If Binance cuts the connection (which it routinely does every 24h), the Top N stays frozen indefinitely and the main engine operates with a stale symbol list, with no alert.update_every_hour()is redundant dead logic.SYMBOLSis already updated on every WebSocket message viaprocess_data()->update_symbols(); the hourly loop serves no real purpose other than keeping the main thread alive, which can be achieved more cleanly (e.g.threading.Event().wait()).- No lock for
SYMBOLS/RANKED_BY_VOLUMEshared between the WebSocket thread and the main thread (same pattern as bug #8 in the main engine). - No retry handling for the subscription message if the initial
SUBSCRIBEcall fails silently (there is no confirmation that Binance accepted the subscription to!ticker@arr).
The trading rules summarized above are the ones currently active. Detailed
write-ups of each strategy component -- including the physical mechanism
each formula is meant to capture, its valid regime, and known failure modes
-- live in docs/strategies/.
This project also builds on a series of earlier prototypes (rule-based
physics-inspired models, and an unfinished 3D shape-recognition neural
network blocked by local compute) kept in legacy/ with their own
post-mortem in docs/research.md. They did not reach production, but each
one narrowed down what does and doesn't hold up when the market's regime
shifts -- see that log for the reasoning behind design choices in this
README's Phase 3 and Phase 4 sections.
crypto_bot_project/
├── pyproject.toml
├── docker-compose.yml
├── Dockerfile
├── .env.example
├── README.md
│
├── legacy-experimental-phase/ # early prototypes, original scripts and experiments
│ ├── wy_multicoin_engine_v47.py
│ ├── wy_function_winners_list_v03.py
│ └── experiments/
│ ├── VLU_test01/ # C++/Python optimization & benchmark tests
│ ├── VLU_test02/ # neuro-plastic & symbolic trading experiments
│ └── VLU_test03/ # high-frequency & volatility trading models
│
├── docs/
│ ├── strategies/ # one doc per model/strategy
│ │ ├── macd-dea-crossover.md
│ │ └── bollinger-lateral.md
│ └── research.md # log of prototypes: what worked, what failed, why
│
├── src/
│ ├── main.py # Entry point (FastAPI + background tasks)
│ │
│ ├── api/
│ │ └── routes.py # GET /status, GET /trades, POST /pause, etc.
│ │
│ ├── core/
│ │ ├── config.py # Pydantic Settings (.env)
│ │ ├── exceptions.py
│ │ ├── logger.py
│ │ └── trade_history.py # Total profit calculation and file persistence
│ │
│ ├── db/
│ │ ├── models.py # TradeHistory, SystemLog, Candle
│ │ └── database.py # SQLAlchemy connection / session
│ │
│ ├── state/
│ │ └── redis_client.py # Live prices, open trades
│ │
│ ├── services/
│ │ ├── binance_api.py # REST client: orders, balances, lot size
│ │ ├── telegram.py # Notifications
│ │ └── websockets.py # WSS connections (klines, trades, ticker) + reconnection
│ │
│ ├── market_context/
│ │ ├── regime.py # Ranging vs. trending (macro ADX)
│ │ └── beta_calc.py # Altcoin beta vs. BTC
│ │
│ ├── trading/
│ │ ├── indicators.py # MACD, RSI, Bollinger, ADX, ATR
│ │ ├── strategy.py # Scoring and entry/exit rules
│ │ ├── risk.py # Stop-loss, trailing stop, position sizing
│ │ └── executor.py # Orchestrates buy/sell based on the rules
│ │
│ ├── backtesting/
│ │ ├── data_loader.py # Loads historical candles into Postgres
│ │ ├── engine.py # Historical simulation of the strategy
│ │ └── reports.py # Metrics: drawdown, win rate, profit factor
│ │
│ ├── sentiment/ # Phase 4 (experimental): RAG ingestion and scoring
│ │ ├── ingestion.py # Scheduled pulls from news/social/on-chain APIs
│ │ ├── rag_pipeline.py # Chunking, embedding, and retrieval over pgvector
│ │ └── scoring.py # LLM-based sentiment classification
│ │
│ ├── agents/ # Phase 4 (experimental): advisory/gating agents
│ │ ├── circuit_breaker.py # Can veto entries or force-close on adverse signals
│ │ └── strategy_selector.py # Chooses active strategy component (trend/range/sentiment)
│ │
│ └── optimization/ # Phase 5 (experimental): genetic algorithm search
│ ├── genome.py # Strategy configuration encoded as a genome
│ ├── fitness.py # Wraps the backtesting engine as a fitness function
│ └── evolve.py # Selection, crossover, mutation loop (DEAP-based)
│
└── tests/
├── unit/
│ ├── core/
│ │ └── test_trade_history.py
│ ├── trading/
│ └── market_context/
├── integration/
└── performance/
# Clone and enter the project
git clone <repo>
cd crypto_bot_project
# Install dependencies (with uv or poetry, to be decided)
uv sync
# Bring up infrastructure (Postgres + Redis)
docker compose up -d
# Configure environment variables
cp .env.example .env
# fill in BINANCE_API_TEST_KEY, BINANCE_API_TEST_SECRET, TELEGRAM_BOT_TOKEN, etc.
# Run in development mode
python -m src.mainREAL_TRADES=False by default. Do not switch to True without having completed Phase 1 and run the Phase 3 backtesting.