From 74ee43ee100a0ec2ea1348a01681bf663c08871f Mon Sep 17 00:00:00 2001 From: yah2er0ne Date: Mon, 3 Nov 2025 22:16:28 +0800 Subject: [PATCH 1/8] clean Signed-off-by: yah2er0ne --- .env.example | 19 +- README.md | 8 + docs/CONFIGURATION_GUIDE.md | 46 +- docs/OKX_SETUP.md | 50 ++ python/README.md | 31 ++ python/pyproject.toml | 1 + python/scripts/launch.py | 13 +- python/uv.lock | 201 ++++++- .../agents/auto_trading_agent/agent.py | 204 ++++++- .../auto_trading_agent/exchanges/__init__.py | 6 +- .../exchanges/base_exchange.py | 1 + .../exchanges/okx_exchange.py | 519 ++++++++++++++++++ .../exchanges/paper_trading.py | 1 + .../agents/auto_trading_agent/market_data.py | 70 +++ .../agents/auto_trading_agent/models.py | 94 ++++ .../auto_trading_agent/technical_analysis.py | 7 +- .../auto_trading_agent/trading_executor.py | 115 +++- python/valuecell/server/api/app.py | 5 + .../valuecell/server/api/routers/trading.py | 100 ++++ .../valuecell/server/api/schemas/trading.py | 38 ++ 20 files changed, 1480 insertions(+), 49 deletions(-) create mode 100644 docs/OKX_SETUP.md create mode 100644 python/valuecell/agents/auto_trading_agent/exchanges/okx_exchange.py create mode 100644 python/valuecell/server/api/routers/trading.py create mode 100644 python/valuecell/server/api/schemas/trading.py diff --git a/.env.example b/.env.example index b28b3507e..c4ff0bcd8 100644 --- a/.env.example +++ b/.env.example @@ -78,8 +78,25 @@ SEC_EMAIL= FINNHUB_API_KEY= +# Hyperliquid Trading (live trading requires explicit opt-in) +AUTO_TRADING_EXCHANGE=paper +HYPERLIQUID_NETWORK=testnet +HYPERLIQUID_ACCOUNT_ADDRESS= +HYPERLIQUID_SECRET_KEY= +HYPERLIQUID_ALLOW_LIVE_TRADING=false +HYPERLIQUID_DEFAULT_SLIPPAGE=0.02 + +# OKX Trading +OKX_NETWORK=paper +OKX_API_KEY= +OKX_API_SECRET= +OKX_API_PASSPHRASE= +OKX_ALLOW_LIVE_TRADING=false +OKX_MARGIN_MODE=cash +OKX_USE_SERVER_TIME=false +======= # ============================================ # Additional Configurations # ============================================ # Optional: Set your https://xueqiu.com/ token if YFinance data fetching becomes unstable. -# XUEQIU_TOKEN= \ No newline at end of file +# XUEQIU_TOKEN= diff --git a/README.md b/README.md index d390d0392..46387be36 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,7 @@ Welcome to join our Discord community to share feedback and issues you encounter - **Multiple LLM Providers**: Support OpenRouter, SiliconFlow, Google and OpenAI - **Popular Market Data**: Cover US market, Crypto market, Hong Kong market, China market and more - **Multi-Agent Framework Compatible**: Support Langchain, Agno by A2A Protocol for research and development integration +- **Exchange Connectivity**: Optional live routing to Hyperliquid or OKX with built-in guardrails # Quick Start @@ -156,6 +157,13 @@ If it has been a long time since the last update, you can delete the database fi Once the application is running, you can explore the web interface to interact with ValueCell's features and capabilities. +## Live Trading (Hyperliquid & OKX Preview) + +- Set `AUTO_TRADING_EXCHANGE` to `hyperliquid` or `okx` and populate the respective credentials in `.env` (see [Configuration Guide](docs/CONFIGURATION_GUIDE.md)). +- Start the stack with `./start.sh --exchange hyperliquid --network testnet` or `./start.sh --exchange okx --network paper` so overrides reach the Auto Trading agent. +- Follow the dedicated setup guides ([Hyperliquid](docs/HYPERLIQUID_SETUP.md) and [OKX](docs/OKX_SETUP.md)) to source API keys, fund your demo wallets, and understand safety toggles. +- Keep the corresponding `*_ALLOW_LIVE_TRADING=false` flags until strategies pass paper trading validation and stakeholders approve mainnet deployment. + --- **Note**: Ensure all prerequisites are installed and environment variables are properly configured before running the application. diff --git a/docs/CONFIGURATION_GUIDE.md b/docs/CONFIGURATION_GUIDE.md index 928790787..a8b079182 100644 --- a/docs/CONFIGURATION_GUIDE.md +++ b/docs/CONFIGURATION_GUIDE.md @@ -21,12 +21,12 @@ This hierarchy allows you to: ValueCell supports multiple LLM providers. Choose at least one: -| Provider | Sign Up | -|----------|---------| -| **OpenRouter** | [openrouter.ai](https://openrouter.ai/) | -| **SiliconFlow** | [siliconflow.cn](https://www.siliconflow.cn/) | -| **Google** | [ai.google.dev](https://ai.google.dev/) | -| **OpenAI** | [platform.openai.com](https://platform.openai.com/) | +| Provider | Sign Up | +| --------------- | --------------------------------------------------- | +| **OpenRouter** | [openrouter.ai](https://openrouter.ai/) | +| **SiliconFlow** | [siliconflow.cn](https://www.siliconflow.cn/) | +| **Google** | [ai.google.dev](https://ai.google.dev/) | +| **OpenAI** | [platform.openai.com](https://platform.openai.com/) | ### Step 2: Configure .env File @@ -485,11 +485,37 @@ models: ### Pattern 3: Development vs Production -```bash -# .env.development -OPENROUTER_API_KEY=sk-or-v1-dev-xxxxx -APP_ENVIRONMENT=development +### Hyperliquid Trading + +| Variable | Default | Description | +| -------------------------------- | --------- | ----------------------------------------------------------------------------------------- | +| `HYPERLIQUID_NETWORK` | `testnet` | Select `testnet` for paper funds or `mainnet` for live trading. | +| `HYPERLIQUID_ACCOUNT_ADDRESS` | — | Wallet address used to sign orders. Required when `exchange=hyperliquid`. | +| `HYPERLIQUID_SECRET_KEY` | — | Private key that signs Hyperliquid orders. Load from a secure secret store in production. | +| `HYPERLIQUID_ALLOW_LIVE_TRADING` | `false` | Must be `true` to permit mainnet order placement. Acts as a safety toggle. | +| `AUTO_TRADING_EXCHANGE` | `paper` | Default exchange for the Auto Trading agent (`paper` or `hyperliquid`). | +| `HYPERLIQUID_DEFAULT_SLIPPAGE` | `0.02` | Slippage buffer applied to Hyperliquid market orders (0.0–0.5). | + +> [!WARNING] +> Never commit your Hyperliquid private key. Prefer OS keychains or environment injection in CI. Test new strategies on `testnet` before enabling `mainnet` funds. + +### OKX Trading +| Variable | Default | Description | +| ------------------------ | ------- | ------------------------------------------------------------------ | +| `OKX_NETWORK` | `paper` | Choose `paper` for demo trading or `mainnet` for live environment. | +| `OKX_API_KEY` | — | OKX API key generated from the OKX console. | +| `OKX_API_SECRET` | — | API secret corresponding to the key. | +| `OKX_API_PASSPHRASE` | — | Passphrase set when creating the OKX API key. | +| `OKX_ALLOW_LIVE_TRADING` | `false` | Must be `true` before routing orders to the mainnet environment. | +| `OKX_MARGIN_MODE` | `cash` | Trading mode passed to OKX (`cash`, `cross`, `isolated`). | +| `OKX_USE_SERVER_TIME` | `false` | Enable to sync with OKX server time for order stamping. | + +> [!IMPORTANT] +> Keep `OKX_ALLOW_LIVE_TRADING=false` until strategies are validated on the OKX paper environment. Treat API secrets as production credentials and store them in a secure vault. + +## Troubleshooting +```bash # .env.production OPENROUTER_API_KEY=sk-or-v1-prod-xxxxx SILICONFLOW_API_KEY=sk-prod-xxxxx diff --git a/docs/OKX_SETUP.md b/docs/OKX_SETUP.md new file mode 100644 index 000000000..59cb0d087 --- /dev/null +++ b/docs/OKX_SETUP.md @@ -0,0 +1,50 @@ +# OKX Setup Guide + +Use this checklist to enable the Auto Trading agent to route spot orders through OKX. Start with the paper environment before touching mainnet funds. + +## 1. Create API Credentials +- Log in to the OKX console → *API* → *Create V5 API key*. +- Enable **Trade** permission. Withdrawals are not required for trading. +- Record the **API Key**, **Secret Key**, and **Passphrase** immediately; you will not be able to view the secret again. +- For paper trading, toggle the **Demo trading** switch when generating the key (or set `flag=0` by using the paper environment). + +## 2. Configure Environment Variables +Add the following entries to `.env` (or export them before launching): + +```bash +AUTO_TRADING_EXCHANGE=okx +OKX_NETWORK=paper # change to mainnet after validation +OKX_API_KEY=your_api_key +OKX_API_SECRET=your_secret +OKX_API_PASSPHRASE=your_passphrase +OKX_ALLOW_LIVE_TRADING=false +OKX_MARGIN_MODE=cash # use cross / isolated for margin derivatives +OKX_USE_SERVER_TIME=false # set true if you see timestamp drift errors +``` + +## 3. Launch the Stack +- Install dependencies: `uv sync --group dev` and `bun install --cwd frontend` (first run only). +- Start services with paper overrides: + + ```bash + ./start.sh --exchange okx --network paper + ``` + +- This propagates the environment into `python/scripts/launch.py`, ensuring the Auto Trading agent connects with paper credentials. + +## 4. Validate Paper Trading +1. Trigger the Auto Trading agent via the UI (http://localhost:1420) or CLI and request trades such as “Trade BTC-USD with 5000 USD on OKX”. +2. Watch the logs under `logs//AutoTradingAgent.log` for entries like `exchange=okx` and `status=filled`. +3. Open https://www.okx.com/paper/account/trade to confirm the orders appear in the simulated environment. +4. Run the OKX unit tests locally: `uv run python -m pytest valuecell/agents/auto_trading_agent/tests/test_okx_exchange.py`. + +## 5. Promote to Mainnet (Optional and High Risk) +- Flip `OKX_NETWORK=mainnet` and `OKX_ALLOW_LIVE_TRADING=true` only after paper validation and formal approval. +- Restart with `./start.sh --exchange okx --network mainnet --allow-live-trading`. +- Monitor fills and balances continuously; revert the toggle if behaviour is unexpected. + +## 6. Safety Best Practices +- Store secrets in a vault (1Password, AWS Secrets Manager, etc.) and inject them at runtime instead of committing to disk. +- Rotate keys periodically and whenever you suspect compromise. +- Set conservative order sizes (`risk_per_trade`) and verify instrument availability (`BTC-USDT`, `ETH-USDT`, etc.) before relying on automation. +- Archive trading logs for audit purposes and switch the system back to paper mode when not actively monitoring. diff --git a/python/README.md b/python/README.md index f8852bac1..9ff85832c 100644 --- a/python/README.md +++ b/python/README.md @@ -55,3 +55,34 @@ uv venv --python 3.12 && uv sync && uv pip list - Python >= 3.12 - Dependencies managed via `pyproject.toml` + +## Hyperliquid Trading (Preview) + +The Auto Trading agent can submit orders to Hyperliquid via the official SDK. Install dependencies with `uv sync --group dev`, then populate the following environment variables: + +```bash +AUTO_TRADING_EXCHANGE=hyperliquid # switch from paper trading to Hyperliquid +HYPERLIQUID_NETWORK=testnet # switch to mainnet once validated +HYPERLIQUID_ACCOUNT_ADDRESS=0x... +HYPERLIQUID_SECRET_KEY=0x... # store securely! +HYPERLIQUID_ALLOW_LIVE_TRADING=false +HYPERLIQUID_DEFAULT_SLIPPAGE=0.02 +``` + +> Test new strategies on the Hyperliquid testnet before enabling `HYPERLIQUID_ALLOW_LIVE_TRADING=true`. Mainnet trading requires sufficient collateral in your Hyperliquid wallet. + +## OKX Trading (Preview) + +Add OKX credentials to `.env` (or export them before launch): + +```bash +AUTO_TRADING_EXCHANGE=okx +OKX_NETWORK=paper # switch to mainnet only after validation +OKX_API_KEY=... +OKX_API_SECRET=... +OKX_API_PASSPHRASE=... +OKX_ALLOW_LIVE_TRADING=false +OKX_MARGIN_MODE=cash # or cross / isolated +``` + +Launch with `./start.sh --exchange okx --network paper` to route the Auto Trading agent through OKX. Flip `OKX_ALLOW_LIVE_TRADING=true` only when you're ready for real execution. diff --git a/python/pyproject.toml b/python/pyproject.toml index 380baed5f..e90044315 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -9,6 +9,7 @@ description = "ValueCell is a community-driven, multi-agent platform for financi readme = "README.md" requires-python = ">=3.12" dependencies = [ + "python-okx>=0.4.0", "pytz>=2023.3", "python-dateutil>=2.8.2", "fastapi>=0.104.0", diff --git a/python/scripts/launch.py b/python/scripts/launch.py index 0ae1c3b3f..22bcd653e 100644 --- a/python/scripts/launch.py +++ b/python/scripts/launch.py @@ -49,6 +49,17 @@ PYTHON_DIR_STR = PYTHON_DIR.as_posix() ENV_PATH_STR = ENV_PATH.as_posix() +AUTO_TRADING_ENV_OVERRIDES = { + "AUTO_TRADING_EXCHANGE": os.getenv("AUTO_TRADING_EXCHANGE"), +} +AUTO_TRADING_ENV_PREFIX = " ".join( + f"{key}={value}" + for key, value in AUTO_TRADING_ENV_OVERRIDES.items() + if value not in (None, "") +) +if AUTO_TRADING_ENV_PREFIX: + AUTO_TRADING_ENV_PREFIX = f"{AUTO_TRADING_ENV_PREFIX} " + # Mapping from agent name to launch command MAP_NAME_COMMAND: Dict[str, str] = {} for name, analyst in MAP_NAME_ANALYST.items(): @@ -62,7 +73,7 @@ f"uv run --env-file {ENV_PATH_STR} -m valuecell.agents.research_agent" ) MAP_NAME_COMMAND[AUTO_TRADING_AGENT_NAME] = ( - f"uv run --env-file {ENV_PATH_STR} -m valuecell.agents.auto_trading_agent" + f"{AUTO_TRADING_ENV_PREFIX}uv run --env-file {ENV_PATH_STR} -m valuecell.agents.auto_trading_agent" ) MAP_NAME_COMMAND[NEWS_AGENT_NAME] = ( f"uv run --env-file {ENV_PATH_STR} -m valuecell.agents.news_agent" diff --git a/python/uv.lock b/python/uv.lock index a9026465f..b938008f6 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.13'", @@ -247,6 +247,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "automat" +version = "25.4.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977, upload-time = "2025-04-16T20:12:16.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, +] + [[package]] name = "backoff" version = "2.2.1" @@ -466,6 +475,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "constantly" +version = "23.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300, upload-time = "2023-10-28T23:18:24.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" }, +] + [[package]] name = "coverage" version = "7.10.6" @@ -1207,6 +1225,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, ] +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -1228,6 +1258,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] +[[package]] +name = "incremental" +version = "24.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157, upload-time = "2024-07-29T20:03:55.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516, upload-time = "2024-07-29T20:03:53.677Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -1246,6 +1288,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186, upload-time = "2025-02-26T21:13:14.911Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1348,6 +1432,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + [[package]] name = "lance-namespace" version = "0.0.18" @@ -1588,6 +1689,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0e/a9943f90b4a8a6d3849b81a00a00d2db128d876365385af382a0e2caf191/mini_racer-0.12.4-py3-none-win_amd64.whl", hash = "sha256:9446e3bd6a4eb9fbedf1861326f7476080995a31c9b69308acef17e5b7ecaa1b", size = 13674040, upload-time = "2024-06-20T14:44:37.851Z" }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + [[package]] name = "multidict" version = "6.6.4" @@ -2422,6 +2532,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "python-okx" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "importlib-metadata" }, + { name = "keyring" }, + { name = "loguru" }, + { name = "pyopenssl" }, + { name = "requests" }, + { name = "twisted" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/07/07320f86d8c66bbbfc940cb4de1caf8699a211e8839e14b3b970a4132496/python_okx-0.4.0.tar.gz", hash = "sha256:c2a878dc2c2c3badac61dbb3f980b386dd89932a4b5d11e82fe1cce6619c92c2", size = 23474, upload-time = "2025-07-28T03:20:55.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/35/bb5f1cea2d3432cac1fe448f1b80d5f42712cc2ac7221449ec168af86a7f/python_okx-0.4.0-py3-none-any.whl", hash = "sha256:20e2df6a1ed1ea0d995eb31307ca8d76749b7b880db88f1252e98ad16242e5f2", size = 33189, upload-time = "2025-07-28T03:20:54.009Z" }, +] + [[package]] name = "python-oxmsg" version = "0.0.2" @@ -2445,6 +2573,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -2874,6 +3011,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, ] +[[package]] +name = "secretstorage" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + [[package]] name = "shapely" version = "2.1.2" @@ -3199,6 +3358,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/bc/f36afd1ad03d09cd0d311be5951f2dd705a824f34dd871779d42c277d741/trimesh-4.8.3-py3-none-any.whl", hash = "sha256:f9f1622ecac5f3bed5b65d3a748bb536481f52cd4e4e120e0b637cabe6cf1542", size = 735464, upload-time = "2025-09-26T20:07:56.512Z" }, ] +[[package]] +name = "twisted" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "automat" }, + { name = "constantly" }, + { name = "hyperlink" }, + { name = "incremental" }, + { name = "typing-extensions" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725, upload-time = "2025-06-07T09:52:24.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767, upload-time = "2025-06-07T09:52:21.428Z" }, +] + [[package]] name = "typer" version = "0.17.4" @@ -3355,6 +3532,7 @@ dependencies = [ { name = "markdown" }, { name = "pydantic" }, { name = "python-dateutil" }, + { name = "python-okx" }, { name = "pytz" }, { name = "requests" }, { name = "sqlalchemy" }, @@ -3413,6 +3591,7 @@ requires-dist = [ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, { name = "python-dateutil", specifier = ">=2.8.2" }, + { name = "python-okx", specifier = ">=0.4.0" }, { name = "pytz", specifier = ">=2023.3" }, { name = "requests", specifier = ">=2.32.5" }, { name = "ruff", marker = "extra == 'dev'" }, @@ -3730,3 +3909,23 @@ sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50e wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] + +[[package]] +name = "zope-interface" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/3a/7fcf02178b8fad0a51e67e32765cd039ae505d054d744d76b8c2bbcba5ba/zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1", size = 253746, upload-time = "2025-09-25T05:55:51.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/a6/0f08713ddda834c428ebf97b2a7fd8dea50c0100065a8955924dbd94dae8/zope_interface-8.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:115f27c1cc95ce7a517d960ef381beedb0a7ce9489645e80b9ab3cbf8a78799c", size = 208609, upload-time = "2025-09-25T05:58:53.698Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/d423045f54dc81e0991ec655041e7a0eccf6b2642535839dd364b35f4d7f/zope_interface-8.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af655c573b84e3cb6a4f6fd3fbe04e4dc91c63c6b6f99019b3713ef964e589bc", size = 208797, upload-time = "2025-09-25T05:58:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/c6/43/39d4bb3f7a80ebd261446792493cfa4e198badd47107224f5b6fe1997ad9/zope_interface-8.0.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:23f82ef9b2d5370750cc1bf883c3b94c33d098ce08557922a3fbc7ff3b63dfe1", size = 259242, upload-time = "2025-09-25T05:58:21.602Z" }, + { url = "https://files.pythonhosted.org/packages/da/29/49effcff64ef30731e35520a152a9dfcafec86cf114b4c2aff942e8264ba/zope_interface-8.0.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35a1565d5244997f2e629c5c68715b3d9d9036e8df23c4068b08d9316dcb2822", size = 264696, upload-time = "2025-09-25T05:58:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/b947673ec9a258eeaa20208dd2f6127d9fbb3e5071272a674ebe02063a78/zope_interface-8.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:029ea1db7e855a475bf88d9910baab4e94d007a054810e9007ac037a91c67c6f", size = 264229, upload-time = "2025-09-25T06:26:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/eed6efd1fc3788d1bef7a814e0592d8173b7fe601c699b935009df035fc2/zope_interface-8.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0beb3e7f7dc153944076fcaf717a935f68d39efa9fce96ec97bafcc0c2ea6cab", size = 212270, upload-time = "2025-09-25T05:58:53.584Z" }, + { url = "https://files.pythonhosted.org/packages/5f/dc/3c12fca01c910c793d636ffe9c0984e0646abaf804e44552070228ed0ede/zope_interface-8.0.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:c7cc027fc5c61c5d69e5080c30b66382f454f43dc379c463a38e78a9c6bab71a", size = 208992, upload-time = "2025-09-25T05:58:40.712Z" }, + { url = "https://files.pythonhosted.org/packages/46/71/6127b7282a3e380ca927ab2b40778a9c97935a4a57a2656dadc312db5f30/zope_interface-8.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcf9097ff3003b7662299f1c25145e15260ec2a27f9a9e69461a585d79ca8552", size = 209051, upload-time = "2025-09-25T05:58:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/56/86/4387a9f951ee18b0e41fda77da77d59c33e59f04660578e2bad688703e64/zope_interface-8.0.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6d965347dd1fb9e9a53aa852d4ded46b41ca670d517fd54e733a6b6a4d0561c2", size = 259223, upload-time = "2025-09-25T05:58:23.191Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/ce60a114466abc067c68ed41e2550c655f551468ae17b4b17ea360090146/zope_interface-8.0.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a3b8bb77a4b89427a87d1e9eb969ab05e38e6b4a338a9de10f6df23c33ec3c2", size = 264690, upload-time = "2025-09-25T05:58:15.052Z" }, + { url = "https://files.pythonhosted.org/packages/36/9a/62a9ba3a919594605a07c34eee3068659bbd648e2fa0c4a86d876810b674/zope_interface-8.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:87e6b089002c43231fb9afec89268391bcc7a3b66e76e269ffde19a8112fb8d5", size = 264201, upload-time = "2025-09-25T06:26:27.797Z" }, + { url = "https://files.pythonhosted.org/packages/da/06/8fe88bd7edef60566d21ef5caca1034e10f6b87441ea85de4bbf9ea74768/zope_interface-8.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:64a43f5280aa770cbafd0307cb3d1ff430e2a1001774e8ceb40787abe4bb6658", size = 212273, upload-time = "2025-09-25T06:00:25.398Z" }, +] diff --git a/python/valuecell/agents/auto_trading_agent/agent.py b/python/valuecell/agents/auto_trading_agent/agent.py index aa46892e4..82c84f468 100644 --- a/python/valuecell/agents/auto_trading_agent/agent.py +++ b/python/valuecell/agents/auto_trading_agent/agent.py @@ -25,16 +25,26 @@ ENV_PARSER_MODEL_ID, ENV_SIGNAL_MODEL_ID, ) +from .exchanges import ( + ExchangeBase, + ExchangeType, + OKXExchange, + OKXExchangeError, + PaperTrading, +) from .formatters import MessageFormatter from .models import ( AutoTradingConfig, TradingRequest, + SUPPORTED_EXCHANGES, + SUPPORTED_NETWORKS, ) from .portfolio_decision_manager import ( AssetAnalysis, PortfolioDecisionManager, ) from .technical_analysis import AISignalGenerator, TechnicalAnalyzer +from .market_data import OkxMarketDataProvider, MarketDataProvider from .trading_executor import TradingExecutor # Configure logging @@ -54,6 +64,33 @@ class AutoTradingAgent(BaseAgent): def __init__(self): super().__init__() + # Configuration + self.parser_model_id = os.getenv("TRADING_PARSER_MODEL_ID", DEFAULT_AGENT_MODEL) + self.default_exchange = self._sanitize_exchange( + os.getenv("AUTO_TRADING_EXCHANGE", ExchangeType.PAPER.value) + ) + self.okx_api_key = os.getenv("OKX_API_KEY") + self.okx_api_secret = os.getenv("OKX_API_SECRET") + self.okx_api_passphrase = os.getenv("OKX_API_PASSPHRASE") + self.okx_network = self._sanitize_network(os.getenv("OKX_NETWORK", "paper")) + self.okx_allow_live_trading_flag = self._parse_bool( + os.getenv("OKX_ALLOW_LIVE_TRADING", "false") + ) + self.okx_margin_mode = os.getenv("OKX_MARGIN_MODE", "cash") + self.okx_use_server_time = self._parse_bool( + os.getenv("OKX_USE_SERVER_TIME", "false") + ) + self.default_exchange_network = self.okx_network + + # Select price data provider based on default exchange configuration + try: + if self.default_exchange == ExchangeType.OKX.value: + TechnicalAnalyzer.set_provider(OkxMarketDataProvider()) + else: + TechnicalAnalyzer.set_provider(MarketDataProvider()) + except Exception: + pass + # Multi-instance state management # Structure: {session_id: {instance_id: TradingInstanceData}} self.trading_instances: Dict[str, Dict[str, Dict[str, Any]]] = {} @@ -84,6 +121,28 @@ def __init__(self): logger.error(f"Failed to initialize Auto Trading Agent: {e}") raise + @staticmethod + def _parse_bool(value: Optional[str], default: bool = False) -> bool: + if value is None: + return default + return value.strip().lower() in {"1", "true", "yes", "on"} + + @staticmethod + def _parse_float(value: Optional[str], fallback: float) -> float: + try: + parsed = float(value) if value is not None else fallback + except (TypeError, ValueError): + return fallback + return max(0.0, min(parsed, 0.5)) + + def _sanitize_exchange(self, value: Optional[str]) -> str: + lowered = (value or ExchangeType.PAPER.value).lower() + return lowered if lowered in SUPPORTED_EXCHANGES else ExchangeType.PAPER.value + + def _sanitize_network(self, value: Optional[str]) -> str: + lowered = (value or "testnet").lower() + return lowered if lowered in SUPPORTED_NETWORKS else "testnet" + async def _process_trading_instance( self, session_id: str, @@ -254,7 +313,7 @@ async def _process_trading_instance( continue # Execute trade - trade_details = executor.execute_trade( + trade_details = await executor.execute_trade( symbol, action, trade_type, asset_analysis.indicators ) @@ -311,14 +370,12 @@ async def _process_trading_instance( if executor.positions: portfolio_msg += "\n**Open Positions:**\n" + provider = TechnicalAnalyzer._market_data_provider for symbol, pos in executor.positions.items(): try: - import yfinance as yf - - ticker = yf.Ticker(symbol) - current_price = ticker.history(period="1d", interval="1m")[ - "Close" - ].iloc[-1] + current_price = ( + provider.get_current_price(symbol) or pos.entry_price + ) if pos.trade_type.value == "long": current_pnl = (current_price - pos.entry_price) * abs( pos.quantity @@ -447,7 +504,10 @@ async def _parse_trading_request(self, query: str) -> TradingRequest: 2. initial_capital: Initial trading capital in USD (default: 100000 if not specified) 3. use_ai_signals: Whether to use AI-enhanced signals (default: true) 4. agent_model: Model ID for trading decisions (default: DEFAULT_AGENT_MODEL) - + 5. exchange (optional): Trading venue. Use "paper" (default) or "okx" if the user requests real execution. + 6. exchange_network (optional): OKX network to use, default "paper". Accept "paper" or "testnet". + 7. allow_live_trading (optional): Boolean that must be true to enable OKX live trading. + Examples: - "Trade Bitcoin and Ethereum with $50000" -> {{"crypto_symbols": ["BTC-USD", "ETH-USD"], "initial_capital": 50000, "use_ai_signals": true}} - "Start auto trading BTC-USD" -> {{"crypto_symbols": ["BTC-USD"], "initial_capital": 100000, "use_ai_signals": true}} @@ -516,6 +576,45 @@ def _initialize_ai_signal_generator( ) return None + async def _build_exchange(self, config: AutoTradingConfig) -> ExchangeBase: + exchange_name = (config.exchange or ExchangeType.PAPER.value).lower() + + if exchange_name == ExchangeType.PAPER.value: + adapter = PaperTrading(initial_balance=config.initial_capital) + await adapter.connect() + return adapter + + if exchange_name == ExchangeType.OKX.value: + api_key = config.okx_api_key or self.okx_api_key + api_secret = config.okx_api_secret or self.okx_api_secret + passphrase = config.okx_api_passphrase or self.okx_api_passphrase + + if not api_key or not api_secret or not passphrase: + raise OKXExchangeError( + "OKX credentials missing. Set OKX_API_KEY/OKX_API_SECRET/OKX_API_PASSPHRASE." + ) + + if ( + config.exchange_network not in {"paper", "demo", "testnet"} + and not config.allow_live_trading + ): + raise OKXExchangeError( + "Live OKX trading disabled. Set OKX_ALLOW_LIVE_TRADING=true or request allow_live_trading in the query." + ) + + adapter = OKXExchange( + api_key=api_key, + api_secret=api_secret, + passphrase=passphrase, + network=config.exchange_network, + margin_mode=config.okx_margin_mode, + use_server_time=config.okx_use_server_time, + ) + await adapter.connect() + return adapter + + raise ValueError(f"Unsupported exchange '{exchange_name}'") + def _get_instance_status_component_data( self, session_id: str, instance_id: str ) -> Optional[FilteredCardPushNotificationComponentData]: @@ -578,12 +677,10 @@ def _get_instance_status_component_data( for symbol, pos in executor.positions.items(): try: - import yfinance as yf - - ticker = yf.Ticker(symbol) - current_price = ticker.history(period="1d", interval="1m")[ - "Close" - ].iloc[-1] + provider = TechnicalAnalyzer._market_data_provider + current_price = ( + provider.get_current_price(symbol) or pos.entry_price + ) # Calculate unrealized P&L if pos.trade_type.value == "long": @@ -807,6 +904,8 @@ async def _handle_status_command( f"**Instance:** `{instance_id}` {status}\n" f"- Model: {config.agent_model}\n" f"- Symbols: {', '.join(config.crypto_symbols)}\n" + f"- Exchange: {config.exchange} ({config.exchange_network})\n" + f"- Live Trading: {'✅' if config.allow_live_trading else '❌'}\n" f"- Portfolio Value: ${portfolio_value:,.2f}\n" f"- P&L: ${total_pnl:,.2f}\n" f"- Open Positions: {len(executor.positions)}\n" @@ -897,15 +996,78 @@ async def stream( instance_id = self._generate_instance_id(task_id, model_id) # Create configuration for this specific model + exchange_choice = self._sanitize_exchange( + trading_request.exchange or self.default_exchange + ) + # If parser filled default 'paper' but environment prefers a real exchange, + # and the user did not explicitly mention paper/demo/testnet in the query, + # honor environment default to avoid surprising fallback to paper. + if ( + exchange_choice == ExchangeType.PAPER.value + and self.default_exchange != ExchangeType.PAPER.value + ): + hints = ["paper", "demo", "testnet", "模拟", "仿真", "纸面", "演示"] + if not any(hint in query_lower for hint in hints): + exchange_choice = self.default_exchange + + exchange_network_input = trading_request.exchange_network + if not exchange_network_input: + if exchange_choice == ExchangeType.OKX.value: + exchange_network_input = self.okx_network + else: + exchange_network_input = self.default_exchange_network + exchange_network = self._sanitize_network(exchange_network_input) + + if trading_request.allow_live_trading is not None: + allow_live_trading = trading_request.allow_live_trading + elif exchange_choice == ExchangeType.OKX.value: + allow_live_trading = self.okx_allow_live_trading_flag + else: + allow_live_trading = False + config = AutoTradingConfig( initial_capital=trading_request.initial_capital or 100000, crypto_symbols=trading_request.crypto_symbols, use_ai_signals=trading_request.use_ai_signals or False, agent_model=model_id, + exchange=exchange_choice, + exchange_network=exchange_network, + allow_live_trading=allow_live_trading, + okx_api_key=self.okx_api_key + if exchange_choice == ExchangeType.OKX.value + else None, + okx_api_secret=self.okx_api_secret + if exchange_choice == ExchangeType.OKX.value + else None, + okx_api_passphrase=self.okx_api_passphrase + if exchange_choice == ExchangeType.OKX.value + else None, + okx_margin_mode=self.okx_margin_mode, + okx_use_server_time=self.okx_use_server_time, ) - # Initialize executor - executor = TradingExecutor(config) + # Initialize exchange adapter + try: + exchange_adapter = await self._build_exchange(config) + except (OKXExchangeError,) as exc: + logger.error("Failed to create exchange adapter: %s", exc) + if exchange_choice == ExchangeType.OKX.value: + remediation = "Please verify your OKX API credentials and live-trading flag." + else: + remediation = "Please verify exchange credentials." + yield streaming.failed( + f"❌ **Instance {instance_id}**: {str(exc)}\n" + remediation + ) + continue + except Exception as exc: # noqa: BLE001 + logger.error("Unexpected exchange initialization error: %s", exc) + yield streaming.failed( + f"❌ **Instance {instance_id}**: Unexpected exchange initialization failure." + ) + continue + + # Initialize executor with exchange adapter + executor = TradingExecutor(config, exchange=exchange_adapter) # Initialize AI signal generator if enabled ai_signal_generator = self._initialize_ai_signal_generator(config) @@ -915,6 +1077,7 @@ async def stream( "instance_id": instance_id, "config": config, "executor": executor, + "exchange": exchange_adapter, "ai_signal_generator": ai_signal_generator, "active": True, "created_at": datetime.now(), @@ -926,6 +1089,13 @@ async def stream( # Display configuration for this instance ai_status = "✅ Enabled" if config.use_ai_signals else "❌ Disabled" + exchange_label = config.exchange.capitalize() + exchange_details = ( + f"{exchange_label} ({config.exchange_network})" + if config.exchange in {ExchangeType.OKX.value} + else exchange_label + ) + live_flag = "✅ Enabled" if config.allow_live_trading else "❌ Disabled" config_message = ( f"✅ **Trading Instance Created**\n\n" f"**Instance ID:** `{instance_id}`\n" @@ -936,6 +1106,8 @@ async def stream( f"- Check Interval: {config.check_interval}s (1 minute)\n" f"- Risk Per Trade: {config.risk_per_trade * 100:.1f}%\n" f"- Max Positions: {config.max_positions}\n" + f"- Exchange: {exchange_details}\n" + f"- Live Trading: {live_flag}\n" f"- AI Signals: {ai_status}\n\n" ) diff --git a/python/valuecell/agents/auto_trading_agent/exchanges/__init__.py b/python/valuecell/agents/auto_trading_agent/exchanges/__init__.py index b2df842f9..8fd74f495 100644 --- a/python/valuecell/agents/auto_trading_agent/exchanges/__init__.py +++ b/python/valuecell/agents/auto_trading_agent/exchanges/__init__.py @@ -9,12 +9,16 @@ - BinanceExchange: Live trading on Binance (requires API keys) """ -from .base_exchange import ExchangeBase, ExchangeType, OrderStatus +from .base_exchange import ExchangeBase, ExchangeType, Order, OrderStatus +from .okx_exchange import OKXExchange, OKXExchangeError from .paper_trading import PaperTrading __all__ = [ "ExchangeBase", "ExchangeType", + "Order", "OrderStatus", + "OKXExchange", + "OKXExchangeError", "PaperTrading", ] diff --git a/python/valuecell/agents/auto_trading_agent/exchanges/base_exchange.py b/python/valuecell/agents/auto_trading_agent/exchanges/base_exchange.py index 4346f2393..d79eeeee4 100644 --- a/python/valuecell/agents/auto_trading_agent/exchanges/base_exchange.py +++ b/python/valuecell/agents/auto_trading_agent/exchanges/base_exchange.py @@ -18,6 +18,7 @@ class ExchangeType(str, Enum): BINANCE = "binance" # Binance exchange BYBIT = "bybit" # Bybit exchange (future support) COINBASE = "coinbase" # Coinbase (future support) + OKX = "okx" # OKX spot/derivatives exchange class OrderStatus(str, Enum): diff --git a/python/valuecell/agents/auto_trading_agent/exchanges/okx_exchange.py b/python/valuecell/agents/auto_trading_agent/exchanges/okx_exchange.py new file mode 100644 index 000000000..4cc7fb783 --- /dev/null +++ b/python/valuecell/agents/auto_trading_agent/exchanges/okx_exchange.py @@ -0,0 +1,519 @@ +"""OKX exchange adapter for live trading (spot and contracts).""" + +from __future__ import annotations + +import asyncio +import logging +import uuid +from typing import Any, Dict, List, Optional + +from okx.Account import AccountAPI +from okx.MarketData import MarketAPI +from okx.PublicData import PublicAPI +from okx.Trade import TradeAPI + +from .base_exchange import ExchangeBase, ExchangeType, Order, OrderStatus + +logger = logging.getLogger(__name__) + + +class OKXExchangeError(RuntimeError): + """Raised for recoverable OKX-related failures.""" + + +class OKXExchange(ExchangeBase): + """Exchange adapter for OKX via the official SDK. + + Supports both SPOT and CONTRACTS (e.g., SWAP). Default is contracts mode. + """ + + def __init__( + self, + api_key: str, + api_secret: str, + passphrase: str, + *, + network: str = "paper", + # Default to contracts trading mode (cross) instead of spot cash + margin_mode: str = "cross", + inst_type: str = "SWAP", + use_server_time: bool = False, + domain: str = "https://www.okx.com", + ) -> None: + super().__init__(ExchangeType.OKX) + + if not (api_key and api_secret and passphrase): + raise OKXExchangeError("OKX API key/secret/passphrase must be provided") + + normalized_network = (network or "paper").lower() + # OKX SDK flag semantics: "1" => demo/paper, "0" => live/mainnet + self._flag = "1" if normalized_network in {"paper", "demo", "testnet"} else "0" + self.margin_mode = ( + margin_mode or ("cash" if (inst_type or "").upper() == "SPOT" else "cross") + ).lower() + self.inst_type = (inst_type or "SWAP").upper() + self.use_server_time = use_server_time + self.domain = domain + + self._trade_client = TradeAPI( + api_key, + api_secret, + passphrase, + use_server_time=self.use_server_time, + flag=self._flag, + domain=self.domain, + ) + self._account_client = AccountAPI( + api_key, + api_secret, + passphrase, + use_server_time=self.use_server_time, + flag=self._flag, + domain=self.domain, + ) + self._market_client = MarketAPI( + api_key, + api_secret, + passphrase, + use_server_time=self.use_server_time, + flag=self._flag, + domain=self.domain, + ) + self._public_client = PublicAPI( + api_key, + api_secret, + passphrase, + use_server_time=self.use_server_time, + flag=self._flag, + domain=self.domain, + ) + + # ------------------------------------------------------------------ + # Connection lifecycle + # ------------------------------------------------------------------ + + async def connect(self) -> bool: + try: + await asyncio.to_thread(self._account_client.get_account_balance) + self.is_connected = True + logger.info( + "Connected to OKX (%s mode)", "paper" if self._flag == "1" else "live" + ) + return True + except Exception as exc: # noqa: BLE001 + logger.error("Failed to connect to OKX: %s", exc) + raise OKXExchangeError("Unable to connect to OKX") from exc + + async def disconnect(self) -> bool: + self.is_connected = False + return True + + async def validate_connection(self) -> bool: + try: + await asyncio.to_thread(self._account_client.get_account_config) + return True + except Exception as exc: # noqa: BLE001 + logger.warning("OKX connection validation failed: %s", exc) + self.is_connected = False + return False + + # ------------------------------------------------------------------ + # Account information + # ------------------------------------------------------------------ + + async def get_balance(self) -> Dict[str, float]: + response = await asyncio.to_thread(self._account_client.get_account_balance) + return self._parse_balance(response) + + async def get_asset_balance(self, asset: str) -> float: + balances = await self.get_balance() + return balances.get(asset.upper(), 0.0) + + # ------------------------------------------------------------------ + # Market data + # ------------------------------------------------------------------ + + async def get_current_price(self, symbol: str) -> float: + inst_id = self.normalize_symbol(symbol) + response = await asyncio.to_thread(self._market_client.get_ticker, inst_id) + data = self._extract_first(response) + if not data: + raise OKXExchangeError(f"No ticker data returned for {inst_id}") + return float(data.get("last", data.get("lastPx", 0.0))) + + async def get_24h_ticker(self, symbol: str) -> Dict[str, Any]: + inst_id = self.normalize_symbol(symbol) + response = await asyncio.to_thread(self._market_client.get_ticker, inst_id) + data = self._extract_first(response) or {} + return { + "symbol": inst_id, + "last": self._safe_float(data.get("last", data.get("lastPx"))), + "high_24h": self._safe_float(data.get("high24h")), + "low_24h": self._safe_float(data.get("low24h")), + "volume_24h": self._safe_float(data.get("vol24h")), + "best_bid": self._safe_float(data.get("bidPx")), + "best_ask": self._safe_float(data.get("askPx")), + } + + # ------------------------------------------------------------------ + # Order management + # ------------------------------------------------------------------ + + async def place_order( + self, + symbol: str, + side: str, + quantity: float, + price: Optional[float] = None, + order_type: str = "limit", + **kwargs: Any, + ) -> Order: + inst_id = self.normalize_symbol(symbol) + cloid = kwargs.get("client_order_id") or uuid.uuid4().hex + ord_type = "market" if order_type == "market" or price is None else "limit" + px_value = "" if ord_type == "market" else f"{price:.8f}" + + payload = { + "instId": inst_id, + "tdMode": self.margin_mode, + "side": side.lower(), + "ordType": ord_type, + "sz": f"{quantity:.8f}", + "clOrdId": cloid, + } + # tgtCcy is only applicable for SPOT orders + if self.inst_type == "SPOT": + payload["tgtCcy"] = "base_ccy" + if px_value: + payload["px"] = px_value + + try: + response = await asyncio.to_thread( + self._trade_client.place_order, **payload + ) + except Exception as exc: # noqa: BLE001 + logger.error("OKX order submission failed: %s", exc) + raise OKXExchangeError("Order submission failed") from exc + + order_data = self._extract_first(response) + if not order_data: + logger.error("Invalid response from OKX place_order: %s", response) + raise OKXExchangeError("Order submission returned no data") + + status = self._map_order_state(order_data.get("state")) + filled_px = self._safe_float(order_data.get("avgPx")) or self._safe_float( + px_value + ) + + order = Order( + order_id=order_data.get("ordId", cloid), + symbol=symbol, + side=side.lower(), + quantity=quantity, + price=filled_px or 0.0, + order_type=ord_type, + trade_type=kwargs.get("trade_type"), + ) + order.status = status + if filled_px: + order.filled_price = filled_px + order.filled_quantity = quantity if status == OrderStatus.FILLED else 0.0 + self.orders[order.order_id] = order + self.order_history.append(order) + return order + + async def cancel_order(self, symbol: str, order_id: str) -> bool: + inst_id = self.normalize_symbol(symbol) + try: + await asyncio.to_thread( + self._trade_client.cancel_order, inst_id, ordId=order_id + ) + if order_id in self.orders: + self.orders[order_id].status = OrderStatus.CANCELLED + return True + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to cancel OKX order %s: %s", order_id, exc) + return False + + async def get_order_status(self, symbol: str, order_id: str) -> OrderStatus: + inst_id = self.normalize_symbol(symbol) + try: + response = await asyncio.to_thread( + self._trade_client.get_order, inst_id, ordId=order_id + ) + except Exception as exc: # noqa: BLE001 + logger.warning("Failed to query OKX order %s: %s", order_id, exc) + return self.orders.get( + order_id, Order(order_id, symbol, "buy", 0, 0) + ).status + + order_data = self._extract_first(response) + status = ( + self._map_order_state(order_data.get("state")) + if order_data + else OrderStatus.PENDING + ) + if order_id in self.orders: + self.orders[order_id].status = status + return status + + async def get_open_orders(self, symbol: Optional[str] = None) -> List[Order]: + inst_id = self.normalize_symbol(symbol) if symbol else "" + response = await asyncio.to_thread( + self._trade_client.get_order_list, self.inst_type, instId=inst_id + ) + orders: List[Order] = [] + for item in response.get("data", []): + inst = item.get("instId") + client_symbol = self._from_okx_symbol(inst) + order = Order( + order_id=item.get("ordId") or item.get("clOrdId", uuid.uuid4().hex), + symbol=client_symbol, + side=item.get("side", "buy"), + quantity=self._safe_float(item.get("sz"), default=0.0), + price=self._safe_float(item.get("px"), default=0.0), + ) + order.status = self._map_order_state(item.get("state")) + orders.append(order) + return orders + + async def get_order_history( + self, symbol: Optional[str] = None, limit: int = 100 + ) -> List[Order]: + inst_id = self.normalize_symbol(symbol) if symbol else "" + response = await asyncio.to_thread( + self._trade_client.get_orders_history, + self.inst_type, + instId=inst_id, + limit=str(limit), + ) + orders: List[Order] = [] + for item in response.get("data", []): + inst = item.get("instId") + client_symbol = self._from_okx_symbol(inst) + order = Order( + order_id=item.get("ordId") or item.get("clOrdId", uuid.uuid4().hex), + symbol=client_symbol, + side=item.get("side", "buy"), + quantity=self._safe_float(item.get("sz"), default=0.0), + price=self._safe_float( + item.get("fillPx") or item.get("avgPx"), default=0.0 + ), + ) + order.status = self._map_order_state(item.get("state")) + order.filled_quantity = self._safe_float(item.get("accFillSz"), default=0.0) + order.filled_price = self._safe_float( + item.get("avgPx"), default=order.price + ) + orders.append(order) + return orders + + # ------------------------------------------------------------------ + # Position helpers + # ------------------------------------------------------------------ + + async def get_open_positions( + self, symbol: Optional[str] = None + ) -> Dict[str, Dict[str, Any]]: + # Contracts mode: query positions; Spot mode: derive from balances + if self.inst_type != "SPOT": + try: + response = await asyncio.to_thread( + self._account_client.get_positions, self.inst_type + ) + positions: Dict[str, Dict[str, Any]] = {} + for item in response.get("data", []) or []: + inst = item.get("instId") + client_symbol = self._from_okx_symbol(inst) + if symbol and client_symbol != symbol: + continue + qty = ( + self._safe_float( + item.get("pos") + or item.get("posSz") + or item.get("availPos"), + default=0.0, + ) + or 0.0 + ) + entry_px = self._safe_float(item.get("avgPx"), default=0.0) or 0.0 + unreal_pnl = self._safe_float(item.get("upl"), default=0.0) or 0.0 + positions[client_symbol] = { + "quantity": qty, + "entry_price": entry_px, + "unrealized_pnl": unreal_pnl, + } + return positions + except Exception: # noqa: BLE001 - fallback to spot-like behavior + pass + + balances = await self.get_balance() + # Spot balances act as positions for cash mode; return filtered view + positions: Dict[str, Dict[str, Any]] = {} + for asset, balance in balances.items(): + if asset in {"USD", "USDT", "USDC", "withdrawable_usd"}: + continue + client_symbol = f"{asset}-USDT" + if symbol and client_symbol != symbol: + continue + positions[client_symbol] = { + "quantity": balance, + "entry_price": 0.0, + "unrealized_pnl": 0.0, + } + return positions + + async def get_position_details(self, symbol: str) -> Optional[Dict[str, Any]]: + positions = await self.get_open_positions(symbol) + return positions.get(symbol) + + async def execute_buy( + self, + symbol: str, + quantity: float, + price: Optional[float] = None, + **kwargs: Any, + ) -> Optional[Order]: + order = await self.place_order( + symbol=symbol, + side="buy", + quantity=quantity, + price=price, + order_type="market" if price is None else "limit", + **kwargs, + ) + return order + + async def execute_sell( + self, + symbol: str, + quantity: float, + price: Optional[float] = None, + **kwargs: Any, + ) -> Optional[Order]: + order = await self.place_order( + symbol=symbol, + side="sell", + quantity=quantity, + price=price, + order_type="market" if price is None else "limit", + **kwargs, + ) + return order + + # ------------------------------------------------------------------ + # Utilities + # ------------------------------------------------------------------ + + def normalize_symbol(self, symbol: str) -> str: + clean = symbol.replace("/", "-").upper() + base, _, quote = clean.partition("-") + if self.inst_type == "SPOT": + if not quote: + return clean + if quote == "USD": + quote = "USDT" + return f"{base}-{quote}" + # Contracts (default): map to USDT-margined perpetual swap by default + return f"{base}-USDT-SWAP" + + def _from_okx_symbol(self, inst_id: Optional[str]) -> str: + if not inst_id: + return "" + parts = inst_id.upper().split("-") + if not parts: + return "" + base = parts[0] + quote = parts[1] if len(parts) > 1 else "USD" + # Strip contract suffix like SWAP/QUARTER/NEXT_QUARTER if present + if len(parts) > 2: + quote = parts[1] + if quote == "USDT": + quote = "USD" + return f"{base}-{quote}" + + async def get_fee_tier(self) -> Dict[str, float]: + try: + response = await asyncio.to_thread( + self._account_client.get_fee_rates, self.inst_type + ) + except Exception as exc: # noqa: BLE001 + logger.debug("Failed to fetch OKX fee rates: %s", exc) + return {"maker": 0.0, "taker": 0.0} + + data = self._extract_first(response) or {} + return { + "maker": self._safe_float(data.get("maker"), default=0.0) or 0.0, + "taker": self._safe_float(data.get("taker"), default=0.0) or 0.0, + } + + async def get_trading_limits(self, symbol: str) -> Dict[str, float]: + inst_id = self.normalize_symbol(symbol) + try: + response = await asyncio.to_thread( + self._public_client.get_instruments, self.inst_type, instId=inst_id + ) + except Exception as exc: # noqa: BLE001 + logger.debug("Failed to fetch OKX instrument metadata: %s", exc) + return {} + + data = self._extract_first(response) or {} + min_sz = self._safe_float(data.get("minSz")) + lot_sz = self._safe_float(data.get("lotSz")) + return { + "min_quantity": min_sz or 0.0, + "lot_size": lot_sz or 0.0, + "tick_size": self._safe_float(data.get("tickSz"), default=0.0) or 0.0, + } + + # ------------------------------------------------------------------ + # Helper methods + # ------------------------------------------------------------------ + + @staticmethod + def _extract_first(response: Dict[str, Any]) -> Optional[Dict[str, Any]]: + data = response.get("data") if isinstance(response, dict) else None + if isinstance(data, list) and data: + return data[0] + return None + + @staticmethod + def _safe_float(value: Any, default: Optional[float] = None) -> Optional[float]: + if value in (None, ""): + return default + try: + return float(value) + except (TypeError, ValueError): + return default + + @staticmethod + def _map_order_state(state: Optional[str]) -> OrderStatus: + if not state: + return OrderStatus.PENDING + state = state.lower() + if state in {"filled", "canceledfilled", "partially_filled"}: + return OrderStatus.FILLED + if state in {"live", "partially_filled_not_canceled"}: + return OrderStatus.PENDING + if state in {"canceled", "cancelled"}: + return OrderStatus.CANCELLED + if state in {"rejected"}: + return OrderStatus.REJECTED + return OrderStatus.PENDING + + def _parse_balance(self, response: Dict[str, Any]) -> Dict[str, float]: + balances: Dict[str, float] = {} + for account in response.get("data", []): + total_eq = account.get("totalEq") + if total_eq is not None: + balances["USD"] = float(total_eq) + for detail in account.get("details", []): + currency = detail.get("ccy") + if not currency: + continue + total = detail.get("eq") or detail.get("cashBal") or "0" + try: + balances[currency.upper()] = float(total) + except (TypeError, ValueError): + continue + return balances diff --git a/python/valuecell/agents/auto_trading_agent/exchanges/paper_trading.py b/python/valuecell/agents/auto_trading_agent/exchanges/paper_trading.py index 5ebc92c06..cab1d31d6 100644 --- a/python/valuecell/agents/auto_trading_agent/exchanges/paper_trading.py +++ b/python/valuecell/agents/auto_trading_agent/exchanges/paper_trading.py @@ -182,6 +182,7 @@ async def place_order( quantity=quantity, price=price, order_type=order_type, + trade_type=kwargs.get("trade_type"), ) # Immediately fill market orders diff --git a/python/valuecell/agents/auto_trading_agent/market_data.py b/python/valuecell/agents/auto_trading_agent/market_data.py index 166760e12..711b062ca 100644 --- a/python/valuecell/agents/auto_trading_agent/market_data.py +++ b/python/valuecell/agents/auto_trading_agent/market_data.py @@ -6,6 +6,8 @@ import pandas as pd import yfinance as yf +from .exchanges.okx_exchange import OKXExchange +import os from .models import TechnicalIndicators @@ -154,6 +156,74 @@ def safe_float(value): ) +class OkxMarketDataProvider(MarketDataProvider): + """Market data provider that uses OKX APIs for spot/contract prices. + + Falls back to parent implementation for indicators (yfinance) to keep + indicator availability without extra OKX Kline wiring for now. + """ + + def __init__(self, cache_ttl_seconds: int = 60): + super().__init__(cache_ttl_seconds=cache_ttl_seconds) + self._okx = None + + def _ensure_okx(self) -> OKXExchange: + if self._okx is None: + api_key = os.getenv("OKX_API_KEY", "") + api_secret = os.getenv("OKX_API_SECRET", "") + passphrase = os.getenv("OKX_API_PASSPHRASE", "") + network = os.getenv("OKX_NETWORK", "paper") + self._okx = OKXExchange( + api_key=api_key, + api_secret=api_secret, + passphrase=passphrase, + network=network, + ) + return self._okx + + def get_current_price(self, symbol: str) -> Optional[float]: + try: + okx = self._ensure_okx() + # Connect lazily on first price call + if not getattr(okx, "is_connected", False): + import asyncio + + try: + asyncio.get_running_loop() + # we're in an async context; run sync via to_thread later + # Fallback to parent if connection cannot be ensured here + except RuntimeError: + # No running loop; safe to run connect synchronously + asyncio.run(okx.connect()) + + # Get price from OKX adapter + price = None + try: + # Try synchronous bridge via asyncio for adapter's async method + import asyncio + + async def _get(): + if not okx.is_connected: + await okx.connect() + return await okx.get_current_price(symbol) + + try: + loop = asyncio.get_running_loop() + # If running loop exists, schedule and block until done + price = loop.run_until_complete(_get()) # type: ignore + except RuntimeError: + # No running loop; we can create one + price = asyncio.run(_get()) + except Exception: + # As a fallback, use parent (yfinance) + return super().get_current_price(symbol) + + return float(price) if price is not None else None + except Exception as e: + logger.error(f"Failed to get OKX price for {symbol}: {e}") + return super().get_current_price(symbol) + + class SignalGenerator: """ Generates trading signals from technical indicators. diff --git a/python/valuecell/agents/auto_trading_agent/models.py b/python/valuecell/agents/auto_trading_agent/models.py index a8f77f40c..ee4b250f8 100644 --- a/python/valuecell/agents/auto_trading_agent/models.py +++ b/python/valuecell/agents/auto_trading_agent/models.py @@ -14,6 +14,9 @@ MAX_SYMBOLS, ) +SUPPORTED_EXCHANGES = {"paper", "okx"} +SUPPORTED_NETWORKS = {"testnet", "mainnet", "demo", "beta", "local", "paper"} + class TradeAction(str, Enum): """Trade action enumeration""" @@ -50,6 +53,22 @@ class TradingRequest(BaseModel): default=[DEFAULT_AGENT_MODEL], description="List of model IDs for trading decisions - one instance per model", ) + exchange: Optional[str] = Field( + default=None, + description="Exchange adapter to use (paper or okx)", + ) + exchange_network: Optional[str] = Field( + default=None, + description="Target network for the exchange (testnet, mainnet, etc.)", + ) + allow_live_trading: Optional[bool] = Field( + default=None, + description="Explicit confirmation toggle for mainnet trading", + ) + okx_td_mode: Optional[str] = Field( + default=None, + description="OKX trading mode (cash, cross, isolated); defaults to cash", + ) @field_validator("crypto_symbols") @classmethod @@ -61,6 +80,26 @@ def validate_symbols(cls, v): # Normalize symbols to uppercase return [s.upper() for s in v] + @field_validator("exchange") + @classmethod + def validate_exchange(cls, value): + if value is None: + return value + lowered = value.lower() + if lowered not in SUPPORTED_EXCHANGES: + raise ValueError(f"Unsupported exchange '{value}'") + return lowered + + @field_validator("exchange_network") + @classmethod + def validate_exchange_network(cls, value): + if value is None: + return value + lowered = value.lower() + if lowered not in SUPPORTED_NETWORKS: + raise ValueError(f"Unsupported exchange network '{value}'") + return lowered + class AutoTradingConfig(BaseModel): """Configuration for auto trading agent""" @@ -99,6 +138,45 @@ class AutoTradingConfig(BaseModel): default=False, description="Whether to use AI model for enhanced signal generation", ) + openrouter_api_key: Optional[str] = Field( + default=None, + description="OpenRouter API key for AI model access", + ) + exchange: str = Field( + default="paper", + description="Exchange adapter to use (paper or okx)", + ) + exchange_network: str = Field( + default="testnet", + description="Exchange network identifier (testnet, mainnet, etc.)", + ) + allow_live_trading: bool = Field( + default=False, + description="Must be true to enable mainnet order placement", + ) + okx_api_key: Optional[str] = Field( + default=None, + description="OKX API key for REST access", + repr=False, + ) + okx_api_secret: Optional[str] = Field( + default=None, + description="OKX API secret", + repr=False, + ) + okx_api_passphrase: Optional[str] = Field( + default=None, + description="OKX API passphrase", + repr=False, + ) + okx_margin_mode: str = Field( + default="cross", + description="OKX trading mode (contracts cross by default; use 'cash' for SPOT)", + ) + okx_use_server_time: bool = Field( + default=False, + description="Sync with OKX server time when placing orders", + ) @field_validator("crypto_symbols") @classmethod @@ -110,6 +188,22 @@ def validate_symbols(cls, v): # Normalize symbols to uppercase return [s.upper() for s in v] + @field_validator("exchange") + @classmethod + def validate_exchange(cls, value: str) -> str: + lowered = value.lower() + if lowered not in SUPPORTED_EXCHANGES: + raise ValueError(f"Unsupported exchange '{value}'") + return lowered + + @field_validator("exchange_network") + @classmethod + def validate_network(cls, value: str) -> str: + lowered = value.lower() + if lowered not in SUPPORTED_NETWORKS: + raise ValueError(f"Unsupported exchange network '{value}'") + return lowered + class Position(BaseModel): """Trading position model""" diff --git a/python/valuecell/agents/auto_trading_agent/technical_analysis.py b/python/valuecell/agents/auto_trading_agent/technical_analysis.py index cc8b29c31..990043aad 100644 --- a/python/valuecell/agents/auto_trading_agent/technical_analysis.py +++ b/python/valuecell/agents/auto_trading_agent/technical_analysis.py @@ -6,7 +6,7 @@ from agno.agent import Agent -from .market_data import MarketDataProvider, SignalGenerator +from .market_data import MarketDataProvider, SignalGenerator, OkxMarketDataProvider from .models import TechnicalIndicators, TradeAction, TradeType logger = logging.getLogger(__name__) @@ -21,6 +21,11 @@ class TechnicalAnalyzer: _market_data_provider = MarketDataProvider() + @staticmethod + def set_provider(provider: MarketDataProvider) -> None: + """Override the default market data provider.""" + TechnicalAnalyzer._market_data_provider = provider + @staticmethod def calculate_indicators( symbol: str, period: str = "5d", interval: str = "1m" diff --git a/python/valuecell/agents/auto_trading_agent/trading_executor.py b/python/valuecell/agents/auto_trading_agent/trading_executor.py index c7c07fb38..5a037bf21 100644 --- a/python/valuecell/agents/auto_trading_agent/trading_executor.py +++ b/python/valuecell/agents/auto_trading_agent/trading_executor.py @@ -4,6 +4,7 @@ from datetime import datetime, timezone from typing import Any, Dict, List, Optional +from .exchanges import ExchangeBase, ExchangeType, Order, OrderStatus, PaperTrading from .models import ( AutoTradingConfig, PortfolioValueSnapshot, @@ -30,7 +31,11 @@ class TradingExecutor: - Cash management (via PositionManager) """ - def __init__(self, config: AutoTradingConfig): + def __init__( + self, + config: AutoTradingConfig, + exchange: Optional[ExchangeBase] = None, + ): """ Initialize trading executor. @@ -40,11 +45,17 @@ def __init__(self, config: AutoTradingConfig): self.config = config self.initial_capital = config.initial_capital + # Exchange adapter (defaults to in-memory paper trading) + self.exchange: ExchangeBase = exchange or PaperTrading( + initial_balance=config.initial_capital + ) + self.exchange_type = self.exchange.exchange_type + # Use specialized modules self._position_manager = PositionManager(config.initial_capital) self._trade_recorder = TradeRecorder() - def execute_trade( + async def execute_trade( self, symbol: str, action: TradeAction, @@ -68,9 +79,13 @@ def execute_trade( timestamp = datetime.now(timezone.utc) if action == TradeAction.BUY: - return self._execute_buy(symbol, trade_type, current_price, timestamp) - elif action == TradeAction.SELL: - return self._execute_sell(symbol, trade_type, current_price, timestamp) + return await self._execute_buy( + symbol, trade_type, current_price, timestamp + ) + if action == TradeAction.SELL: + return await self._execute_sell( + symbol, trade_type, current_price, timestamp + ) return None @@ -78,7 +93,7 @@ def execute_trade( logger.error(f"Failed to execute trade for {symbol}: {e}") return None - def _execute_buy( + async def _execute_buy( self, symbol: str, trade_type: TradeType, @@ -99,7 +114,10 @@ def _execute_buy( # Calculate position size available_cash = self._position_manager.get_available_cash() risk_amount = available_cash * self.config.risk_per_trade - quantity = risk_amount / current_price + quantity = risk_amount / current_price if current_price > 0 else 0.0 + if quantity <= 0: + logger.warning("Calculated quantity is non-positive; skipping trade") + return None notional = quantity * current_price # Check if we have enough cash @@ -109,11 +127,26 @@ def _execute_buy( ) return None + side = "buy" if trade_type == TradeType.LONG else "sell" + order = await self._submit_order( + symbol=symbol, + side=side, + quantity=abs(quantity), + trade_type=trade_type, + ) + + if order is None or order.status in {OrderStatus.REJECTED, OrderStatus.CANCELLED}: + logger.warning("Exchange rejected open order for %s", symbol) + return None + + fill_price = order.price or current_price + notional = abs(quantity) * fill_price + # Create and open position position = Position( symbol=symbol, - entry_price=current_price, - quantity=quantity if trade_type == TradeType.LONG else -quantity, + entry_price=fill_price, + quantity=abs(quantity) if trade_type == TradeType.LONG else -abs(quantity), entry_time=timestamp, trade_type=trade_type, notional=notional, @@ -129,7 +162,7 @@ def _execute_buy( symbol=symbol, action="opened", trade_type=trade_type.value, - price=current_price, + price=fill_price, quantity=abs(position.quantity), notional=notional, pnl=None, @@ -142,13 +175,15 @@ def _execute_buy( "action": "opened", "trade_type": trade_type.value, "symbol": symbol, - "entry_price": current_price, + "entry_price": fill_price, "quantity": position.quantity, "notional": notional, "timestamp": timestamp, + "order_id": order.order_id, + "exchange": self.exchange_type.value, } - def _execute_sell( + async def _execute_sell( self, symbol: str, trade_type: TradeType, @@ -165,11 +200,23 @@ def _execute_sell( if position.trade_type != trade_type: return None - # Calculate P&L - pnl = self._position_manager.calculate_position_pnl(position, current_price) - exit_notional = abs(position.quantity) * current_price + side = "sell" if trade_type == TradeType.LONG else "buy" + order = await self._submit_order( + symbol=symbol, + side=side, + quantity=abs(position.quantity), + trade_type=trade_type, + ) + + if order is None: + logger.warning("Failed to close position on %s via exchange", symbol) + return None + + exit_price = order.price or current_price + pnl = self._position_manager.calculate_position_pnl(position, exit_price) + exit_notional = abs(position.quantity) * exit_price - # Close position + # Close position locally self._position_manager.close_position(symbol) self._position_manager.release_cash(position.notional, pnl) @@ -181,7 +228,7 @@ def _execute_sell( symbol=symbol, action="closed", trade_type=trade_type.value, - price=current_price, + price=exit_price, quantity=abs(position.quantity), notional=exit_notional, pnl=pnl, @@ -195,15 +242,47 @@ def _execute_sell( "trade_type": trade_type.value, "symbol": symbol, "entry_price": position.entry_price, - "exit_price": current_price, + "exit_price": exit_price, "quantity": position.quantity, "entry_notional": position.notional, "exit_notional": exit_notional, "pnl": pnl, "holding_time": holding_time, "timestamp": timestamp, + "order_id": order.order_id, + "exchange": self.exchange_type.value, } + async def _submit_order( + self, + *, + symbol: str, + side: str, + quantity: float, + trade_type: TradeType, + order_type: str = "market", + ) -> Optional[Order]: + try: + if not self.exchange.is_connected: + await self.exchange.connect() + return await self.exchange.place_order( + symbol=symbol, + side=side, + quantity=quantity, + price=None, + order_type=order_type, + trade_type=trade_type, + ) + except Exception as exc: # noqa: BLE001 + logger.error( + "Order submission failed (%s %s %s): %s", + side, + quantity, + symbol, + exc, + ) + return None + # ============ Portfolio Queries ============ def get_portfolio_value(self) -> float: diff --git a/python/valuecell/server/api/app.py b/python/valuecell/server/api/app.py index 050d020be..5a9512753 100644 --- a/python/valuecell/server/api/app.py +++ b/python/valuecell/server/api/app.py @@ -151,6 +151,11 @@ async def root(): # Include task router app.include_router(create_task_router(), prefix=API_PREFIX) + # Include trading router + from .routers.trading import create_trading_router + + app.include_router(create_trading_router(), prefix=API_PREFIX) + # For uvicorn app = create_app() diff --git a/python/valuecell/server/api/routers/trading.py b/python/valuecell/server/api/routers/trading.py new file mode 100644 index 000000000..1ece3b2cb --- /dev/null +++ b/python/valuecell/server/api/routers/trading.py @@ -0,0 +1,100 @@ +"""Trading-related API routes: positions, balances, etc.""" + +from __future__ import annotations + +import os +from typing import Dict, List, Optional + +from fastapi import APIRouter, HTTPException, Query + +from valuecell.agents.auto_trading_agent.exchanges.okx_exchange import ( + OKXExchange, +) +from valuecell.server.api.schemas.trading import ( + OpenPositionsData, + OpenPositionsResponse, + PositionItem, +) +from valuecell.server.api.schemas.base import SuccessResponse + + +def create_trading_router() -> APIRouter: + """Create trading router with endpoints for positions and balances.""" + + router = APIRouter(prefix="/trading", tags=["Trading"]) + + @router.get( + "/positions", + response_model=OpenPositionsResponse, + summary="Get open positions", + description="Fetch current open positions and unrealized P&L from the configured exchange", + ) + async def get_open_positions( + exchange: Optional[str] = Query(None, description="Exchange name"), + network: Optional[str] = Query( + None, description="Network/environment, e.g. testnet|mainnet|paper" + ), + ) -> OpenPositionsResponse: + try: + # Resolve target exchange: query > env > default + resolved_exchange = (exchange or os.getenv("AUTO_TRADING_EXCHANGE", "paper")).lower() + + items: List[PositionItem] = [] + + if resolved_exchange == "okx": + api_key = os.getenv("OKX_API_KEY", "").strip() + api_secret = os.getenv("OKX_API_SECRET", "").strip() + passphrase = os.getenv("OKX_API_PASSPHRASE", "").strip() + allow_live = ( + os.getenv("OKX_ALLOW_LIVE_TRADING", "false").lower() == "true" + ) + resolved_network = (network or os.getenv("OKX_NETWORK", "paper")).lower() + + okx = OKXExchange( + api_key=api_key, + api_secret=api_secret, + passphrase=passphrase, + network=resolved_network, + # default to contracts; margin_mode/inst_type internal defaults + ) + await okx.connect() + raw_positions: Dict[str, Dict[str, float]] = await okx.get_open_positions() + + for symbol, pos in raw_positions.items(): + qty = float(pos.get("quantity", 0.0) or 0.0) + entry_px = float(pos.get("entry_price", 0.0) or 0.0) + try: + current_px = await okx.get_current_price(symbol) + except Exception: + current_px = None + unreal_pnl = float(pos.get("unrealized_pnl", 0.0) or 0.0) + notional = abs(qty) * entry_px if entry_px else 0.0 + pnl_pct = (unreal_pnl / notional * 100.0) if notional else None + items.append( + PositionItem( + symbol=symbol, + quantity=qty, + entry_price=entry_px, + current_price=current_px, + unrealized_pnl=unreal_pnl, + pnl_percent=pnl_pct, + ) + ) + data = OpenPositionsData(exchange="okx", network=resolved_network, positions=items) + return SuccessResponse.create(data=data, msg="Retrieved open positions") + + # Paper / unsupported exchange: return empty set gracefully + resolved_network = (network or os.getenv("OKX_NETWORK") or "paper") + data = OpenPositionsData(exchange=resolved_exchange, network=resolved_network, positions=[]) + return SuccessResponse.create(data=data, msg="No positions for selected exchange") + + except HTTPException: + raise + except Exception as e: # pragma: no cover - generic server error wrapper + raise HTTPException( + status_code=500, detail=f"Failed to fetch positions: {str(e)}" + ) + + return router + + diff --git a/python/valuecell/server/api/schemas/trading.py b/python/valuecell/server/api/schemas/trading.py new file mode 100644 index 000000000..51c72861c --- /dev/null +++ b/python/valuecell/server/api/schemas/trading.py @@ -0,0 +1,38 @@ +"""Trading-related API schemas.""" + +from typing import List, Optional + +from pydantic import BaseModel, Field + +from .base import SuccessResponse + + +class PositionItem(BaseModel): + """Represents a single open position entry.""" + + symbol: str = Field(..., description="Symbol in client format, e.g. BTC-USD") + quantity: float = Field(..., description="Position size") + entry_price: float = Field(..., description="Average entry price") + current_price: Optional[float] = Field( + None, description="Current mark/mid price if available" + ) + unrealized_pnl: float = Field(..., description="Unrealized P&L in quote currency") + pnl_percent: Optional[float] = Field( + None, description="Unrealized P&L percentage relative to notional" + ) + + +class OpenPositionsData(BaseModel): + """Container of open positions for the account.""" + + exchange: str = Field(..., description="Exchange identifier, e.g. okx") + network: Optional[str] = Field(None, description="Network or environment") + positions: List[PositionItem] = Field(default_factory=list) + + +class OpenPositionsResponse(SuccessResponse[OpenPositionsData]): + """Success response wrapping open positions data.""" + + pass + + From 89134465fc11e23b84aacdc75f9d58651f954404 Mon Sep 17 00:00:00 2001 From: yah2er0ne Date: Mon, 3 Nov 2025 22:17:19 +0800 Subject: [PATCH 2/8] remove hyperliquid Signed-off-by: yah2er0ne --- .env.example | 9 --------- 1 file changed, 9 deletions(-) diff --git a/.env.example b/.env.example index c4ff0bcd8..c45b842b1 100644 --- a/.env.example +++ b/.env.example @@ -77,15 +77,6 @@ SEC_EMAIL= # Required by the trading agent. FINNHUB_API_KEY= - -# Hyperliquid Trading (live trading requires explicit opt-in) -AUTO_TRADING_EXCHANGE=paper -HYPERLIQUID_NETWORK=testnet -HYPERLIQUID_ACCOUNT_ADDRESS= -HYPERLIQUID_SECRET_KEY= -HYPERLIQUID_ALLOW_LIVE_TRADING=false -HYPERLIQUID_DEFAULT_SLIPPAGE=0.02 - # OKX Trading OKX_NETWORK=paper OKX_API_KEY= From 0d9735fd5f56470aacfea8b1baf2a9f98ec4a892 Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Mon, 3 Nov 2025 22:25:24 +0800 Subject: [PATCH 3/8] remove hyperliquid in doc --- docs/CONFIGURATION_GUIDE.md | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/docs/CONFIGURATION_GUIDE.md b/docs/CONFIGURATION_GUIDE.md index a8b079182..b4c804b3c 100644 --- a/docs/CONFIGURATION_GUIDE.md +++ b/docs/CONFIGURATION_GUIDE.md @@ -485,20 +485,6 @@ models: ### Pattern 3: Development vs Production -### Hyperliquid Trading - -| Variable | Default | Description | -| -------------------------------- | --------- | ----------------------------------------------------------------------------------------- | -| `HYPERLIQUID_NETWORK` | `testnet` | Select `testnet` for paper funds or `mainnet` for live trading. | -| `HYPERLIQUID_ACCOUNT_ADDRESS` | — | Wallet address used to sign orders. Required when `exchange=hyperliquid`. | -| `HYPERLIQUID_SECRET_KEY` | — | Private key that signs Hyperliquid orders. Load from a secure secret store in production. | -| `HYPERLIQUID_ALLOW_LIVE_TRADING` | `false` | Must be `true` to permit mainnet order placement. Acts as a safety toggle. | -| `AUTO_TRADING_EXCHANGE` | `paper` | Default exchange for the Auto Trading agent (`paper` or `hyperliquid`). | -| `HYPERLIQUID_DEFAULT_SLIPPAGE` | `0.02` | Slippage buffer applied to Hyperliquid market orders (0.0–0.5). | - -> [!WARNING] -> Never commit your Hyperliquid private key. Prefer OS keychains or environment injection in CI. Test new strategies on `testnet` before enabling `mainnet` funds. - ### OKX Trading | Variable | Default | Description | From 90208c759ebebd3b106cae88b8ccd8538f8f5c94 Mon Sep 17 00:00:00 2001 From: yah2er0ne Date: Mon, 3 Nov 2025 22:27:52 +0800 Subject: [PATCH 4/8] remove hyperliquid Signed-off-by: yah2er0ne --- README.md | 11 +++++------ docs/CONFIGURATION_GUIDE.md | 14 -------------- python/README.md | 15 --------------- 3 files changed, 5 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 46387be36..835555647 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ Welcome to join our Discord community to share feedback and issues you encounter - **Multiple LLM Providers**: Support OpenRouter, SiliconFlow, Google and OpenAI - **Popular Market Data**: Cover US market, Crypto market, Hong Kong market, China market and more - **Multi-Agent Framework Compatible**: Support Langchain, Agno by A2A Protocol for research and development integration -- **Exchange Connectivity**: Optional live routing to Hyperliquid or OKX with built-in guardrails +- **Exchange Connectivity**: Optional live routing to OKX with built-in guardrails # Quick Start @@ -157,12 +157,11 @@ If it has been a long time since the last update, you can delete the database fi Once the application is running, you can explore the web interface to interact with ValueCell's features and capabilities. -## Live Trading (Hyperliquid & OKX Preview) +## Live Trading (OKX Preview) -- Set `AUTO_TRADING_EXCHANGE` to `hyperliquid` or `okx` and populate the respective credentials in `.env` (see [Configuration Guide](docs/CONFIGURATION_GUIDE.md)). -- Start the stack with `./start.sh --exchange hyperliquid --network testnet` or `./start.sh --exchange okx --network paper` so overrides reach the Auto Trading agent. -- Follow the dedicated setup guides ([Hyperliquid](docs/HYPERLIQUID_SETUP.md) and [OKX](docs/OKX_SETUP.md)) to source API keys, fund your demo wallets, and understand safety toggles. -- Keep the corresponding `*_ALLOW_LIVE_TRADING=false` flags until strategies pass paper trading validation and stakeholders approve mainnet deployment. +- Set `AUTO_TRADING_EXCHANGE=okx` and populate the required `OKX_*` credentials in `.env` (see [Configuration Guide](docs/CONFIGURATION_GUIDE.md) and [OKX Setup](docs/OKX_SETUP.md)). +- Start the stack with `./start.sh` after configuring the environment. +- Keep `OKX_ALLOW_LIVE_TRADING=false` until strategies pass paper trading validation and stakeholders approve mainnet deployment. --- diff --git a/docs/CONFIGURATION_GUIDE.md b/docs/CONFIGURATION_GUIDE.md index a8b079182..b4c804b3c 100644 --- a/docs/CONFIGURATION_GUIDE.md +++ b/docs/CONFIGURATION_GUIDE.md @@ -485,20 +485,6 @@ models: ### Pattern 3: Development vs Production -### Hyperliquid Trading - -| Variable | Default | Description | -| -------------------------------- | --------- | ----------------------------------------------------------------------------------------- | -| `HYPERLIQUID_NETWORK` | `testnet` | Select `testnet` for paper funds or `mainnet` for live trading. | -| `HYPERLIQUID_ACCOUNT_ADDRESS` | — | Wallet address used to sign orders. Required when `exchange=hyperliquid`. | -| `HYPERLIQUID_SECRET_KEY` | — | Private key that signs Hyperliquid orders. Load from a secure secret store in production. | -| `HYPERLIQUID_ALLOW_LIVE_TRADING` | `false` | Must be `true` to permit mainnet order placement. Acts as a safety toggle. | -| `AUTO_TRADING_EXCHANGE` | `paper` | Default exchange for the Auto Trading agent (`paper` or `hyperliquid`). | -| `HYPERLIQUID_DEFAULT_SLIPPAGE` | `0.02` | Slippage buffer applied to Hyperliquid market orders (0.0–0.5). | - -> [!WARNING] -> Never commit your Hyperliquid private key. Prefer OS keychains or environment injection in CI. Test new strategies on `testnet` before enabling `mainnet` funds. - ### OKX Trading | Variable | Default | Description | diff --git a/python/README.md b/python/README.md index 9ff85832c..fa0e82629 100644 --- a/python/README.md +++ b/python/README.md @@ -56,21 +56,6 @@ uv venv --python 3.12 && uv sync && uv pip list - Python >= 3.12 - Dependencies managed via `pyproject.toml` -## Hyperliquid Trading (Preview) - -The Auto Trading agent can submit orders to Hyperliquid via the official SDK. Install dependencies with `uv sync --group dev`, then populate the following environment variables: - -```bash -AUTO_TRADING_EXCHANGE=hyperliquid # switch from paper trading to Hyperliquid -HYPERLIQUID_NETWORK=testnet # switch to mainnet once validated -HYPERLIQUID_ACCOUNT_ADDRESS=0x... -HYPERLIQUID_SECRET_KEY=0x... # store securely! -HYPERLIQUID_ALLOW_LIVE_TRADING=false -HYPERLIQUID_DEFAULT_SLIPPAGE=0.02 -``` - -> Test new strategies on the Hyperliquid testnet before enabling `HYPERLIQUID_ALLOW_LIVE_TRADING=true`. Mainnet trading requires sufficient collateral in your Hyperliquid wallet. - ## OKX Trading (Preview) Add OKX credentials to `.env` (or export them before launch): From 8de09c65eecdbcf5041074f432a1aaf6f72f5d7b Mon Sep 17 00:00:00 2001 From: hazeone <709547807@qq.com> Date: Mon, 3 Nov 2025 22:39:42 +0800 Subject: [PATCH 5/8] modify env.example --- .env.example | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.env.example b/.env.example index c45b842b1..ff37fbbad 100644 --- a/.env.example +++ b/.env.example @@ -68,16 +68,10 @@ OPENAI_COMPATIBLE_BASE_URL= # You can set this to any valid email address. SEC_EMAIL= - # ============================================ -# Third-Party Agent Configurations +# Auto Trading Agent Configurations # ============================================ -# Finnhub API Key — Required for financial news and insider trading data. -# Get your free API key from: https://finnhub.io/register -# Required by the trading agent. -FINNHUB_API_KEY= - -# OKX Trading +# OKX exchange API. OKX_NETWORK=paper OKX_API_KEY= OKX_API_SECRET= @@ -85,7 +79,18 @@ OKX_API_PASSPHRASE= OKX_ALLOW_LIVE_TRADING=false OKX_MARGIN_MODE=cash OKX_USE_SERVER_TIME=false -======= + + + +# ============================================ +# Third-Party Agent Configurations +# ============================================ +# Finnhub API Key — Required for financial news and insider trading data. +# Get your free API key from: https://finnhub.io/register +# Required by the trading agent. +FINNHUB_API_KEY= + + # ============================================ # Additional Configurations # ============================================ From 8aa3c0b11a538629cedcbaeab9feeb3eba39a322 Mon Sep 17 00:00:00 2001 From: yah2er0ne Date: Mon, 3 Nov 2025 22:43:21 +0800 Subject: [PATCH 6/8] fmt Signed-off-by: yah2er0ne --- python/third_party/TradingAgents/uv.lock | 192 ++++++++++++++++- python/third_party/ai-hedge-fund/uv.lock | 201 +++++++++++++++++- .../agents/auto_trading_agent/agent.py | 6 +- .../agents/auto_trading_agent/market_data.py | 4 +- .../auto_trading_agent/technical_analysis.py | 2 +- .../auto_trading_agent/trading_executor.py | 5 +- .../valuecell/server/api/routers/trading.py | 30 ++- .../valuecell/server/api/schemas/trading.py | 2 - 8 files changed, 421 insertions(+), 21 deletions(-) diff --git a/python/third_party/TradingAgents/uv.lock b/python/third_party/TradingAgents/uv.lock index d311b4ee2..c48f8c8d1 100644 --- a/python/third_party/TradingAgents/uv.lock +++ b/python/third_party/TradingAgents/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12" resolution-markers = [ "python_full_version >= '3.13'", @@ -333,6 +333,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, ] +[[package]] +name = "automat" +version = "25.4.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977, upload-time = "2025-04-16T20:12:16.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, +] + [[package]] name = "backoff" version = "2.2.1" @@ -743,6 +752,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018, upload-time = "2021-06-11T10:22:42.561Z" }, ] +[[package]] +name = "constantly" +version = "23.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300, upload-time = "2023-10-28T23:18:24.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" }, +] + [[package]] name = "contourpy" version = "1.3.3" @@ -1705,6 +1723,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, ] +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -1735,6 +1765,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, ] +[[package]] +name = "incremental" +version = "24.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157, upload-time = "2024-07-29T20:03:55.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516, upload-time = "2024-07-29T20:03:53.677Z" }, +] + [[package]] name = "inflection" version = "0.5.1" @@ -1744,6 +1786,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1876,6 +1960,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.9" @@ -2598,6 +2699,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/67/7e8406a29b6c45be7af7740456f7f37025f0506ae2e05fb9009a53946860/monotonic-1.6-py2.py3-none-any.whl", hash = "sha256:68687e19a14f11f26d140dd5c86f3dba4bf5df58003000ed467e0e2a69bca96c", size = 8154, upload-time = "2021-04-09T21:58:05.122Z" }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + [[package]] name = "mpmath" version = "1.3.0" @@ -4364,6 +4474,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "python-okx" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "importlib-metadata" }, + { name = "keyring" }, + { name = "loguru" }, + { name = "pyopenssl" }, + { name = "requests" }, + { name = "twisted" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/07/07320f86d8c66bbbfc940cb4de1caf8699a211e8839e14b3b970a4132496/python_okx-0.4.0.tar.gz", hash = "sha256:c2a878dc2c2c3badac61dbb3f980b386dd89932a4b5d11e82fe1cce6619c92c2", size = 23474, upload-time = "2025-07-28T03:20:55.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/35/bb5f1cea2d3432cac1fe448f1b80d5f42712cc2ac7221449ec168af86a7f/python_okx-0.4.0-py3-none-any.whl", hash = "sha256:20e2df6a1ed1ea0d995eb31307ca8d76749b7b880db88f1252e98ad16242e5f2", size = 33189, upload-time = "2025-07-28T03:20:54.009Z" }, +] + [[package]] name = "python-oxmsg" version = "0.0.2" @@ -4416,6 +4544,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -4825,6 +4962,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, ] +[[package]] +name = "secretstorage" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, +] + [[package]] name = "setuptools" version = "80.9.0" @@ -5399,6 +5549,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/80/75/63810958023595b460f2a5ef6baf5a60ffd8166e5fc06a3c2f22e9ca7b34/tushare-1.4.24-py3-none-any.whl", hash = "sha256:778e3128262747cb0cdadac2e5a5e6cd1a520c239b4ffbde2776652424451b08", size = 143587, upload-time = "2025-08-25T02:02:03.554Z" }, ] +[[package]] +name = "twisted" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "automat" }, + { name = "constantly" }, + { name = "hyperlink" }, + { name = "incremental" }, + { name = "typing-extensions" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725, upload-time = "2025-06-07T09:52:24.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767, upload-time = "2025-06-07T09:52:21.428Z" }, +] + [[package]] name = "typer" version = "0.17.4" @@ -5598,6 +5766,7 @@ dependencies = [ { name = "markdown" }, { name = "pydantic" }, { name = "python-dateutil" }, + { name = "python-okx" }, { name = "pytz" }, { name = "requests" }, { name = "sqlalchemy" }, @@ -5624,6 +5793,7 @@ requires-dist = [ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, { name = "python-dateutil", specifier = ">=2.8.2" }, + { name = "python-okx", specifier = ">=0.4.0" }, { name = "pytz", specifier = ">=2023.3" }, { name = "requests", specifier = ">=2.32.5" }, { name = "ruff", marker = "extra == 'dev'" }, @@ -5972,6 +6142,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] +[[package]] +name = "zope-interface" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/3a/7fcf02178b8fad0a51e67e32765cd039ae505d054d744d76b8c2bbcba5ba/zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1", size = 253746, upload-time = "2025-09-25T05:55:51.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/a6/0f08713ddda834c428ebf97b2a7fd8dea50c0100065a8955924dbd94dae8/zope_interface-8.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:115f27c1cc95ce7a517d960ef381beedb0a7ce9489645e80b9ab3cbf8a78799c", size = 208609, upload-time = "2025-09-25T05:58:53.698Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/d423045f54dc81e0991ec655041e7a0eccf6b2642535839dd364b35f4d7f/zope_interface-8.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af655c573b84e3cb6a4f6fd3fbe04e4dc91c63c6b6f99019b3713ef964e589bc", size = 208797, upload-time = "2025-09-25T05:58:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/c6/43/39d4bb3f7a80ebd261446792493cfa4e198badd47107224f5b6fe1997ad9/zope_interface-8.0.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:23f82ef9b2d5370750cc1bf883c3b94c33d098ce08557922a3fbc7ff3b63dfe1", size = 259242, upload-time = "2025-09-25T05:58:21.602Z" }, + { url = "https://files.pythonhosted.org/packages/da/29/49effcff64ef30731e35520a152a9dfcafec86cf114b4c2aff942e8264ba/zope_interface-8.0.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35a1565d5244997f2e629c5c68715b3d9d9036e8df23c4068b08d9316dcb2822", size = 264696, upload-time = "2025-09-25T05:58:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/b947673ec9a258eeaa20208dd2f6127d9fbb3e5071272a674ebe02063a78/zope_interface-8.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:029ea1db7e855a475bf88d9910baab4e94d007a054810e9007ac037a91c67c6f", size = 264229, upload-time = "2025-09-25T06:26:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/eed6efd1fc3788d1bef7a814e0592d8173b7fe601c699b935009df035fc2/zope_interface-8.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0beb3e7f7dc153944076fcaf717a935f68d39efa9fce96ec97bafcc0c2ea6cab", size = 212270, upload-time = "2025-09-25T05:58:53.584Z" }, + { url = "https://files.pythonhosted.org/packages/5f/dc/3c12fca01c910c793d636ffe9c0984e0646abaf804e44552070228ed0ede/zope_interface-8.0.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:c7cc027fc5c61c5d69e5080c30b66382f454f43dc379c463a38e78a9c6bab71a", size = 208992, upload-time = "2025-09-25T05:58:40.712Z" }, + { url = "https://files.pythonhosted.org/packages/46/71/6127b7282a3e380ca927ab2b40778a9c97935a4a57a2656dadc312db5f30/zope_interface-8.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcf9097ff3003b7662299f1c25145e15260ec2a27f9a9e69461a585d79ca8552", size = 209051, upload-time = "2025-09-25T05:58:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/56/86/4387a9f951ee18b0e41fda77da77d59c33e59f04660578e2bad688703e64/zope_interface-8.0.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6d965347dd1fb9e9a53aa852d4ded46b41ca670d517fd54e733a6b6a4d0561c2", size = 259223, upload-time = "2025-09-25T05:58:23.191Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/ce60a114466abc067c68ed41e2550c655f551468ae17b4b17ea360090146/zope_interface-8.0.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a3b8bb77a4b89427a87d1e9eb969ab05e38e6b4a338a9de10f6df23c33ec3c2", size = 264690, upload-time = "2025-09-25T05:58:15.052Z" }, + { url = "https://files.pythonhosted.org/packages/36/9a/62a9ba3a919594605a07c34eee3068659bbd648e2fa0c4a86d876810b674/zope_interface-8.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:87e6b089002c43231fb9afec89268391bcc7a3b66e76e269ffde19a8112fb8d5", size = 264201, upload-time = "2025-09-25T06:26:27.797Z" }, + { url = "https://files.pythonhosted.org/packages/da/06/8fe88bd7edef60566d21ef5caca1034e10f6b87441ea85de4bbf9ea74768/zope_interface-8.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:64a43f5280aa770cbafd0307cb3d1ff430e2a1001774e8ceb40787abe4bb6658", size = 212273, upload-time = "2025-09-25T06:00:25.398Z" }, +] + [[package]] name = "zstandard" version = "0.25.0" diff --git a/python/third_party/ai-hedge-fund/uv.lock b/python/third_party/ai-hedge-fund/uv.lock index 663a65df2..75299bada 100644 --- a/python/third_party/ai-hedge-fund/uv.lock +++ b/python/third_party/ai-hedge-fund/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.12, <4" resolution-markers = [ "python_full_version >= '3.13'", @@ -354,6 +354,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, ] +[[package]] +name = "automat" +version = "25.4.16" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/0f/d40bbe294bbf004d436a8bcbcfaadca8b5140d39ad0ad3d73d1a8ba15f14/automat-25.4.16.tar.gz", hash = "sha256:0017591a5477066e90d26b0e696ddc143baafd87b588cfac8100bc6be9634de0", size = 129977, upload-time = "2025-04-16T20:12:16.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/ff/1175b0b7371e46244032d43a56862d0af455823b5280a50c63d99cc50f18/automat-25.4.16-py3-none-any.whl", hash = "sha256:04e9bce696a8d5671ee698005af6e5a9fa15354140a87f4870744604dcdd3ba1", size = 42842, upload-time = "2025-04-16T20:12:14.447Z" }, +] + [[package]] name = "backoff" version = "2.2.1" @@ -593,6 +602,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "constantly" +version = "23.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/6f/cb2a94494ff74aa9528a36c5b1422756330a75a8367bf20bd63171fc324d/constantly-23.10.4.tar.gz", hash = "sha256:aa92b70a33e2ac0bb33cd745eb61776594dc48764b06c35e0efd050b7f1c7cbd", size = 13300, upload-time = "2023-10-28T23:18:24.316Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b8/40/c199d095151addf69efdb4b9ca3a4f20f70e20508d6222bffb9b76f58573/constantly-23.10.4-py3-none-any.whl", hash = "sha256:3fd9b4d1c3dc1ec9757f3c52aef7e53ad9323dbe39f51dfd4c43853b68dfa3f9", size = 13547, upload-time = "2023-10-28T23:18:23.038Z" }, +] + [[package]] name = "contourpy" version = "1.3.3" @@ -1523,6 +1541,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, ] +[[package]] +name = "hyperlink" +version = "21.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/51/1947bd81d75af87e3bb9e34593a4cf118115a8feb451ce7a69044ef1412e/hyperlink-21.0.0.tar.gz", hash = "sha256:427af957daa58bc909471c6c40f74c5450fa123dd093fc53efd2e91d2705a56b", size = 140743, upload-time = "2021-01-08T05:51:20.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/aa/8caf6a0a3e62863cbb9dab27135660acba46903b703e224f14f447e57934/hyperlink-21.0.0-py2.py3-none-any.whl", hash = "sha256:e6b14c37ecb73e89c77d78cdb4c2cc8f3fb59a885c5b3f819ff4ed80f25af1b4", size = 74638, upload-time = "2021-01-08T05:51:22.906Z" }, +] + [[package]] name = "idna" version = "3.10" @@ -1544,6 +1574,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, ] +[[package]] +name = "incremental" +version = "24.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/87/156b374ff6578062965afe30cc57627d35234369b3336cf244b240c8d8e6/incremental-24.7.2.tar.gz", hash = "sha256:fb4f1d47ee60efe87d4f6f0ebb5f70b9760db2b2574c59c8e8912be4ebd464c9", size = 28157, upload-time = "2024-07-29T20:03:55.441Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/38/221e5b2ae676a3938c2c1919131410c342b6efc2baffeda395dd66eeca8f/incremental-24.7.2-py3-none-any.whl", hash = "sha256:8cb2c3431530bec48ad70513931a760f446ad6c25e8333ca5d95e24b0ed7b8fe", size = 20516, upload-time = "2024-07-29T20:03:53.677Z" }, +] + [[package]] name = "iniconfig" version = "2.1.0" @@ -1562,6 +1604,48 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/b3/8def84f539e7d2289a02f0524b944b15d7c75dab7628bedf1c4f0992029c/isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6", size = 92310, upload-time = "2023-12-13T20:37:23.244Z" }, ] +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f7/ed/1aa2d585304ec07262e1a83a9889880701079dde796ac7b1d1826f40c63d/jaraco_functools-4.3.0.tar.gz", hash = "sha256:cfd13ad0dd2c47a3600b439ef72d8615d482cedcff1632930d6f28924d92f294", size = 19755, upload-time = "2025-08-18T20:05:09.91Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/09/726f168acad366b11e420df31bf1c702a54d373a83f968d94141a8c3fde0/jaraco_functools-4.3.0-py3-none-any.whl", hash = "sha256:227ff8ed6f7b8f62c56deff101545fa7543cf2c8e7b82a7c2116e672f29c26e8", size = 10408, upload-time = "2025-08-18T20:05:08.69Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1685,6 +1769,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.9" @@ -2305,6 +2406,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5c/0e/a9943f90b4a8a6d3849b81a00a00d2db128d876365385af382a0e2caf191/mini_racer-0.12.4-py3-none-win_amd64.whl", hash = "sha256:9446e3bd6a4eb9fbedf1861326f7476080995a31c9b69308acef17e5b7ecaa1b", size = 13674040, upload-time = "2024-06-20T14:44:37.851Z" }, ] +[[package]] +name = "more-itertools" +version = "10.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/5d/38b681d3fce7a266dd9ab73c66959406d565b3e85f21d5e66e1181d93721/more_itertools-10.8.0.tar.gz", hash = "sha256:f638ddf8a1a0d134181275fb5d58b086ead7c6a72429ad725c67503f13ba30bd", size = 137431, upload-time = "2025-09-02T15:23:11.018Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667, upload-time = "2025-09-02T15:23:09.635Z" }, +] + [[package]] name = "multidict" version = "6.6.4" @@ -3147,6 +3257,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, ] +[[package]] +name = "python-okx" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "importlib-metadata" }, + { name = "keyring" }, + { name = "loguru" }, + { name = "pyopenssl" }, + { name = "requests" }, + { name = "twisted" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/07/07320f86d8c66bbbfc940cb4de1caf8699a211e8839e14b3b970a4132496/python_okx-0.4.0.tar.gz", hash = "sha256:c2a878dc2c2c3badac61dbb3f980b386dd89932a4b5d11e82fe1cce6619c92c2", size = 23474, upload-time = "2025-07-28T03:20:55.315Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/35/bb5f1cea2d3432cac1fe448f1b80d5f42712cc2ac7221449ec168af86a7f/python_okx-0.4.0-py3-none-any.whl", hash = "sha256:20e2df6a1ed1ea0d995eb31307ca8d76749b7b880db88f1252e98ad16242e5f2", size = 33189, upload-time = "2025-07-28T03:20:54.009Z" }, +] + [[package]] name = "python-oxmsg" version = "0.0.2" @@ -3170,6 +3298,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + [[package]] name = "pyyaml" version = "6.0.2" @@ -3571,6 +3708,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, ] +[[package]] +name = "secretstorage" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "jeepney" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/31/9f/11ef35cf1027c1339552ea7bfe6aaa74a8516d8b5caf6e7d338daf54fd80/secretstorage-3.4.0.tar.gz", hash = "sha256:c46e216d6815aff8a8a18706a2fbfd8d53fcbb0dce99301881687a1b0289ef7c", size = 19748, upload-time = "2025-09-09T16:42:13.859Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/ff/2e2eed29e02c14a5cb6c57f09b2d5b40e65d6cc71f45b52e0be295ccbc2f/secretstorage-3.4.0-py3-none-any.whl", hash = "sha256:0e3b6265c2c63509fb7415717607e4b2c9ab767b7f344a57473b779ca13bd02e", size = 15272, upload-time = "2025-09-09T16:42:12.744Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + [[package]] name = "shapely" version = "2.1.2" @@ -3873,6 +4032,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8e/bc/f36afd1ad03d09cd0d311be5951f2dd705a824f34dd871779d42c277d741/trimesh-4.8.3-py3-none-any.whl", hash = "sha256:f9f1622ecac5f3bed5b65d3a748bb536481f52cd4e4e120e0b637cabe6cf1542", size = 735464, upload-time = "2025-09-26T20:07:56.512Z" }, ] +[[package]] +name = "twisted" +version = "25.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "automat" }, + { name = "constantly" }, + { name = "hyperlink" }, + { name = "incremental" }, + { name = "typing-extensions" }, + { name = "zope-interface" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/13/0f/82716ed849bf7ea4984c21385597c949944f0f9b428b5710f79d0afc084d/twisted-25.5.0.tar.gz", hash = "sha256:1deb272358cb6be1e3e8fc6f9c8b36f78eb0fa7c2233d2dbe11ec6fee04ea316", size = 3545725, upload-time = "2025-06-07T09:52:24.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/66/ab7efd8941f0bc7b2bd555b0f0471bff77df4c88e0cc31120c82737fec77/twisted-25.5.0-py3-none-any.whl", hash = "sha256:8559f654d01a54a8c3efe66d533d43f383531ebf8d81d9f9ab4769d91ca15df7", size = 3204767, upload-time = "2025-06-07T09:52:21.428Z" }, +] + [[package]] name = "typer" version = "0.17.4" @@ -4072,6 +4249,7 @@ dependencies = [ { name = "markdown" }, { name = "pydantic" }, { name = "python-dateutil" }, + { name = "python-okx" }, { name = "pytz" }, { name = "requests" }, { name = "sqlalchemy" }, @@ -4098,6 +4276,7 @@ requires-dist = [ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" }, { name = "python-dateutil", specifier = ">=2.8.2" }, + { name = "python-okx", specifier = ">=0.4.0" }, { name = "pytz", specifier = ">=2023.3" }, { name = "requests", specifier = ">=2.32.5" }, { name = "ruff", marker = "extra == 'dev'" }, @@ -4447,6 +4626,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, ] +[[package]] +name = "zope-interface" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/88/3a/7fcf02178b8fad0a51e67e32765cd039ae505d054d744d76b8c2bbcba5ba/zope_interface-8.0.1.tar.gz", hash = "sha256:eba5610d042c3704a48222f7f7c6ab5b243ed26f917e2bc69379456b115e02d1", size = 253746, upload-time = "2025-09-25T05:55:51.285Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/a6/0f08713ddda834c428ebf97b2a7fd8dea50c0100065a8955924dbd94dae8/zope_interface-8.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:115f27c1cc95ce7a517d960ef381beedb0a7ce9489645e80b9ab3cbf8a78799c", size = 208609, upload-time = "2025-09-25T05:58:53.698Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/d423045f54dc81e0991ec655041e7a0eccf6b2642535839dd364b35f4d7f/zope_interface-8.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:af655c573b84e3cb6a4f6fd3fbe04e4dc91c63c6b6f99019b3713ef964e589bc", size = 208797, upload-time = "2025-09-25T05:58:56.258Z" }, + { url = "https://files.pythonhosted.org/packages/c6/43/39d4bb3f7a80ebd261446792493cfa4e198badd47107224f5b6fe1997ad9/zope_interface-8.0.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:23f82ef9b2d5370750cc1bf883c3b94c33d098ce08557922a3fbc7ff3b63dfe1", size = 259242, upload-time = "2025-09-25T05:58:21.602Z" }, + { url = "https://files.pythonhosted.org/packages/da/29/49effcff64ef30731e35520a152a9dfcafec86cf114b4c2aff942e8264ba/zope_interface-8.0.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:35a1565d5244997f2e629c5c68715b3d9d9036e8df23c4068b08d9316dcb2822", size = 264696, upload-time = "2025-09-25T05:58:13.351Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/b947673ec9a258eeaa20208dd2f6127d9fbb3e5071272a674ebe02063a78/zope_interface-8.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:029ea1db7e855a475bf88d9910baab4e94d007a054810e9007ac037a91c67c6f", size = 264229, upload-time = "2025-09-25T06:26:26.226Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ee/eed6efd1fc3788d1bef7a814e0592d8173b7fe601c699b935009df035fc2/zope_interface-8.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0beb3e7f7dc153944076fcaf717a935f68d39efa9fce96ec97bafcc0c2ea6cab", size = 212270, upload-time = "2025-09-25T05:58:53.584Z" }, + { url = "https://files.pythonhosted.org/packages/5f/dc/3c12fca01c910c793d636ffe9c0984e0646abaf804e44552070228ed0ede/zope_interface-8.0.1-cp313-cp313-macosx_10_9_x86_64.whl", hash = "sha256:c7cc027fc5c61c5d69e5080c30b66382f454f43dc379c463a38e78a9c6bab71a", size = 208992, upload-time = "2025-09-25T05:58:40.712Z" }, + { url = "https://files.pythonhosted.org/packages/46/71/6127b7282a3e380ca927ab2b40778a9c97935a4a57a2656dadc312db5f30/zope_interface-8.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fcf9097ff3003b7662299f1c25145e15260ec2a27f9a9e69461a585d79ca8552", size = 209051, upload-time = "2025-09-25T05:58:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/56/86/4387a9f951ee18b0e41fda77da77d59c33e59f04660578e2bad688703e64/zope_interface-8.0.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6d965347dd1fb9e9a53aa852d4ded46b41ca670d517fd54e733a6b6a4d0561c2", size = 259223, upload-time = "2025-09-25T05:58:23.191Z" }, + { url = "https://files.pythonhosted.org/packages/61/08/ce60a114466abc067c68ed41e2550c655f551468ae17b4b17ea360090146/zope_interface-8.0.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9a3b8bb77a4b89427a87d1e9eb969ab05e38e6b4a338a9de10f6df23c33ec3c2", size = 264690, upload-time = "2025-09-25T05:58:15.052Z" }, + { url = "https://files.pythonhosted.org/packages/36/9a/62a9ba3a919594605a07c34eee3068659bbd648e2fa0c4a86d876810b674/zope_interface-8.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:87e6b089002c43231fb9afec89268391bcc7a3b66e76e269ffde19a8112fb8d5", size = 264201, upload-time = "2025-09-25T06:26:27.797Z" }, + { url = "https://files.pythonhosted.org/packages/da/06/8fe88bd7edef60566d21ef5caca1034e10f6b87441ea85de4bbf9ea74768/zope_interface-8.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:64a43f5280aa770cbafd0307cb3d1ff430e2a1001774e8ceb40787abe4bb6658", size = 212273, upload-time = "2025-09-25T06:00:25.398Z" }, +] + [[package]] name = "zstandard" version = "0.24.0" diff --git a/python/valuecell/agents/auto_trading_agent/agent.py b/python/valuecell/agents/auto_trading_agent/agent.py index 82c84f468..03ed7136e 100644 --- a/python/valuecell/agents/auto_trading_agent/agent.py +++ b/python/valuecell/agents/auto_trading_agent/agent.py @@ -33,18 +33,18 @@ PaperTrading, ) from .formatters import MessageFormatter +from .market_data import MarketDataProvider, OkxMarketDataProvider from .models import ( - AutoTradingConfig, - TradingRequest, SUPPORTED_EXCHANGES, SUPPORTED_NETWORKS, + AutoTradingConfig, + TradingRequest, ) from .portfolio_decision_manager import ( AssetAnalysis, PortfolioDecisionManager, ) from .technical_analysis import AISignalGenerator, TechnicalAnalyzer -from .market_data import OkxMarketDataProvider, MarketDataProvider from .trading_executor import TradingExecutor # Configure logging diff --git a/python/valuecell/agents/auto_trading_agent/market_data.py b/python/valuecell/agents/auto_trading_agent/market_data.py index 711b062ca..3d30fec4a 100644 --- a/python/valuecell/agents/auto_trading_agent/market_data.py +++ b/python/valuecell/agents/auto_trading_agent/market_data.py @@ -1,14 +1,14 @@ """Market data and technical indicator retrieval - from a trader's perspective""" import logging +import os from datetime import datetime, timezone from typing import Dict, Optional import pandas as pd import yfinance as yf -from .exchanges.okx_exchange import OKXExchange -import os +from .exchanges.okx_exchange import OKXExchange from .models import TechnicalIndicators logger = logging.getLogger(__name__) diff --git a/python/valuecell/agents/auto_trading_agent/technical_analysis.py b/python/valuecell/agents/auto_trading_agent/technical_analysis.py index 990043aad..c715d4d77 100644 --- a/python/valuecell/agents/auto_trading_agent/technical_analysis.py +++ b/python/valuecell/agents/auto_trading_agent/technical_analysis.py @@ -6,7 +6,7 @@ from agno.agent import Agent -from .market_data import MarketDataProvider, SignalGenerator, OkxMarketDataProvider +from .market_data import MarketDataProvider, OkxMarketDataProvider, SignalGenerator from .models import TechnicalIndicators, TradeAction, TradeType logger = logging.getLogger(__name__) diff --git a/python/valuecell/agents/auto_trading_agent/trading_executor.py b/python/valuecell/agents/auto_trading_agent/trading_executor.py index 5a037bf21..256b4d871 100644 --- a/python/valuecell/agents/auto_trading_agent/trading_executor.py +++ b/python/valuecell/agents/auto_trading_agent/trading_executor.py @@ -135,7 +135,10 @@ async def _execute_buy( trade_type=trade_type, ) - if order is None or order.status in {OrderStatus.REJECTED, OrderStatus.CANCELLED}: + if order is None or order.status in { + OrderStatus.REJECTED, + OrderStatus.CANCELLED, + }: logger.warning("Exchange rejected open order for %s", symbol) return None diff --git a/python/valuecell/server/api/routers/trading.py b/python/valuecell/server/api/routers/trading.py index 1ece3b2cb..c27b982ba 100644 --- a/python/valuecell/server/api/routers/trading.py +++ b/python/valuecell/server/api/routers/trading.py @@ -10,12 +10,12 @@ from valuecell.agents.auto_trading_agent.exchanges.okx_exchange import ( OKXExchange, ) +from valuecell.server.api.schemas.base import SuccessResponse from valuecell.server.api.schemas.trading import ( OpenPositionsData, OpenPositionsResponse, PositionItem, ) -from valuecell.server.api.schemas.base import SuccessResponse def create_trading_router() -> APIRouter: @@ -37,7 +37,9 @@ async def get_open_positions( ) -> OpenPositionsResponse: try: # Resolve target exchange: query > env > default - resolved_exchange = (exchange or os.getenv("AUTO_TRADING_EXCHANGE", "paper")).lower() + resolved_exchange = ( + exchange or os.getenv("AUTO_TRADING_EXCHANGE", "paper") + ).lower() items: List[PositionItem] = [] @@ -48,7 +50,9 @@ async def get_open_positions( allow_live = ( os.getenv("OKX_ALLOW_LIVE_TRADING", "false").lower() == "true" ) - resolved_network = (network or os.getenv("OKX_NETWORK", "paper")).lower() + resolved_network = ( + network or os.getenv("OKX_NETWORK", "paper") + ).lower() okx = OKXExchange( api_key=api_key, @@ -58,7 +62,9 @@ async def get_open_positions( # default to contracts; margin_mode/inst_type internal defaults ) await okx.connect() - raw_positions: Dict[str, Dict[str, float]] = await okx.get_open_positions() + raw_positions: Dict[ + str, Dict[str, float] + ] = await okx.get_open_positions() for symbol, pos in raw_positions.items(): qty = float(pos.get("quantity", 0.0) or 0.0) @@ -80,13 +86,19 @@ async def get_open_positions( pnl_percent=pnl_pct, ) ) - data = OpenPositionsData(exchange="okx", network=resolved_network, positions=items) + data = OpenPositionsData( + exchange="okx", network=resolved_network, positions=items + ) return SuccessResponse.create(data=data, msg="Retrieved open positions") # Paper / unsupported exchange: return empty set gracefully - resolved_network = (network or os.getenv("OKX_NETWORK") or "paper") - data = OpenPositionsData(exchange=resolved_exchange, network=resolved_network, positions=[]) - return SuccessResponse.create(data=data, msg="No positions for selected exchange") + resolved_network = network or os.getenv("OKX_NETWORK") or "paper" + data = OpenPositionsData( + exchange=resolved_exchange, network=resolved_network, positions=[] + ) + return SuccessResponse.create( + data=data, msg="No positions for selected exchange" + ) except HTTPException: raise @@ -96,5 +108,3 @@ async def get_open_positions( ) return router - - diff --git a/python/valuecell/server/api/schemas/trading.py b/python/valuecell/server/api/schemas/trading.py index 51c72861c..455131056 100644 --- a/python/valuecell/server/api/schemas/trading.py +++ b/python/valuecell/server/api/schemas/trading.py @@ -34,5 +34,3 @@ class OpenPositionsResponse(SuccessResponse[OpenPositionsData]): """Success response wrapping open positions data.""" pass - - From a72a69cf091ce8a8c18640a0f6c3839b6e506704 Mon Sep 17 00:00:00 2001 From: yah2er0ne Date: Mon, 3 Nov 2025 22:47:04 +0800 Subject: [PATCH 7/8] Fmt Signed-off-by: yah2er0ne --- .../valuecell/agents/auto_trading_agent/technical_analysis.py | 2 +- python/valuecell/agents/auto_trading_agent/trading_executor.py | 2 +- python/valuecell/server/api/routers/trading.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/python/valuecell/agents/auto_trading_agent/technical_analysis.py b/python/valuecell/agents/auto_trading_agent/technical_analysis.py index c715d4d77..eda3b28f9 100644 --- a/python/valuecell/agents/auto_trading_agent/technical_analysis.py +++ b/python/valuecell/agents/auto_trading_agent/technical_analysis.py @@ -6,7 +6,7 @@ from agno.agent import Agent -from .market_data import MarketDataProvider, OkxMarketDataProvider, SignalGenerator +from .market_data import MarketDataProvider, SignalGenerator from .models import TechnicalIndicators, TradeAction, TradeType logger = logging.getLogger(__name__) diff --git a/python/valuecell/agents/auto_trading_agent/trading_executor.py b/python/valuecell/agents/auto_trading_agent/trading_executor.py index 256b4d871..ab0f584f5 100644 --- a/python/valuecell/agents/auto_trading_agent/trading_executor.py +++ b/python/valuecell/agents/auto_trading_agent/trading_executor.py @@ -4,7 +4,7 @@ from datetime import datetime, timezone from typing import Any, Dict, List, Optional -from .exchanges import ExchangeBase, ExchangeType, Order, OrderStatus, PaperTrading +from .exchanges import ExchangeBase, Order, OrderStatus, PaperTrading from .models import ( AutoTradingConfig, PortfolioValueSnapshot, diff --git a/python/valuecell/server/api/routers/trading.py b/python/valuecell/server/api/routers/trading.py index c27b982ba..99c92622b 100644 --- a/python/valuecell/server/api/routers/trading.py +++ b/python/valuecell/server/api/routers/trading.py @@ -47,7 +47,7 @@ async def get_open_positions( api_key = os.getenv("OKX_API_KEY", "").strip() api_secret = os.getenv("OKX_API_SECRET", "").strip() passphrase = os.getenv("OKX_API_PASSPHRASE", "").strip() - allow_live = ( + ( os.getenv("OKX_ALLOW_LIVE_TRADING", "false").lower() == "true" ) resolved_network = ( From b2f469e392feb9918c5128ca3b5c1e8d5ade50e0 Mon Sep 17 00:00:00 2001 From: yah2er0ne Date: Mon, 3 Nov 2025 22:49:54 +0800 Subject: [PATCH 8/8] fmt Signed-off-by: yah2er0ne --- python/valuecell/server/api/routers/trading.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/python/valuecell/server/api/routers/trading.py b/python/valuecell/server/api/routers/trading.py index 99c92622b..099dda31f 100644 --- a/python/valuecell/server/api/routers/trading.py +++ b/python/valuecell/server/api/routers/trading.py @@ -47,9 +47,7 @@ async def get_open_positions( api_key = os.getenv("OKX_API_KEY", "").strip() api_secret = os.getenv("OKX_API_SECRET", "").strip() passphrase = os.getenv("OKX_API_PASSPHRASE", "").strip() - ( - os.getenv("OKX_ALLOW_LIVE_TRADING", "false").lower() == "true" - ) + (os.getenv("OKX_ALLOW_LIVE_TRADING", "false").lower() == "true") resolved_network = ( network or os.getenv("OKX_NETWORK", "paper") ).lower()