Commit Graph
275 Commits
Author SHA1 Message Date
Dinger 7d029a5789 v3.0.5
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-13 22:44:49 +08:00
Dinger 11e0139e2a v3.0.5
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-13 14:12:38 +08:00
Dinger 14129668a1 v3.0.5
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-13 00:16:48 +08:00
Dinger cc59ad6d90 v3.0.5
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-12 22:49:27 +08:00
Dinger 24f87daab7 Merge PR #104: resample crypto OHLCV on backend when exchange lacks timeframe 2026-05-12 22:31:10 +08:00
DingerandCursor 1f68b7d255 feat(broker): isolate live broker sessions per user
Replace process-wide global _client in routes/alpaca.py and routes/ibkr.py
with a per-(user_id, broker) BrokerSessionRegistry. Previously every
authenticated user shared one IBKR/Alpaca connection, which is a
multi-tenancy bug in SaaS deployments (user B's order could route
through user A's account).

- Add app/utils/broker_session.py with thread-safe BrokerSessionRegistry
- Refactor routes/alpaca.py to use registry and inline _require_connected_client
- Refactor routes/ibkr.py to use registry and inline _require_connected_client
- /status returns a placeholder when no client exists for the current user
- Replacing a session disposes the old client via disconnect() best-effort

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 22:25:39 +08:00
DingerandCursor fad4ed13fb fix(alpaca): register alpaca_bp at /api/alpaca
PR #101 introduced the Alpaca routes module but missed wiring it up in
app/routes/__init__.py, leaving all 9 endpoints unreachable. Mirror the
ibkr_bp registration so /api/alpaca/* becomes addressable.

Smoke-tested: app boots, all 9 Alpaca rules visible in url_map.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-12 21:28:08 +08:00
Dinger 16588529a3 Merge PR #101: Alpaca Markets adapter (stocks + ETFs + crypto, paper/live) 2026-05-12 21:26:21 +08:00
Dinger 914ad62f16 v3.0.5
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-12 21:04:32 +08:00
Quantdinger dbdadf3c3f Merge pull request #103 from Yeadon8888/fix/polymarket-slug-lookup
fix(polymarket): resolve URL slugs precisely, drop silent popular-market fallback
2026-05-12 17:05:18 +08:00
Dinger 9cbec4f096 v3.0.4
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-12 17:03:36 +08:00
Yeadon8888 80a124ca9a fix(crypto): resample on backend when exchange lacks the requested timeframe
When the configured CCXT exchange doesn't natively support the requested
timeframe — e.g. Coinbase Advanced Trade, which is the only Binance fallback
reachable from regions where Binance is geo-blocked (HTTP 451), and which
exposes only {1m,5m,15m,30m,1h,2h,6h,1d} — `fetch_ohlcv(..., '1w', ...)`
fails with `parsing field "granularity": "1w" is not a valid value` and the
indicator IDE chart shows "数据加载失败: No data found".

This affects 1W, 4H, and 3m on Coinbase out of the box.

Fix: detect at the data-source layer that `ccxt_timeframe not in exchange.timeframes`,
pick the finest supported source granularity, fetch that, and aggregate to the
requested period before returning. No frontend change required.

- New class constant `_RESAMPLE_CANDIDATES` declaring per-target source→bucket
  preferences (3m → 1m×3, 4h → 2h×2 or 1h×4, 1w → 1d×7).
- New `_pick_resample_source(target, exchange.timeframes)` chooses the first
  supported candidate; returns None when nothing finer is available so the
  caller can log and return [] cleanly instead of hitting the upstream 422.
- New `_resample_ohlcv(ohlcv, bucket)` aggregates OHLCV rows: bucket open =
  first.open, high = max, low = min, close = last.close, volume = sum,
  timestamp = first.timestamp. Drops the trailing partial bucket so every
  returned candle represents a full period.
- New `_ccxt_to_qd_timeframe` inverts `CCXTConfig.TIMEFRAME_MAP` for the
  internal call to `_fetch_ohlcv`, keeping its time-range/limit accounting
  consistent with the source granularity actually being fetched.
- `get_kline` now switches into the resample path when needed; fetch limit
  is capped at 300 (CCXT/Coinbase single-call max) to avoid silent truncation.
  For 1W this yields ~42 weekly candles per request, enough for chart display.

Tests: `tests/test_crypto_timeframe_resample.py` — 13 cases covering helper
selection (1W, 4H pref 2h, 4H fallback 1h, 3m, no candidate, unknown target),
OHLCV aggregation math (full bucket, multiple buckets, drop trailing partial,
bucket=1 passthrough, empty input), and TIMEFRAME_MAP round-trip. All pass.

Verified against running Coinbase-backed backend: before the fix,
`GET /api/indicator/kline?market=Crypto&symbol=BTC/USDT&timeframe=1W` returned
55 bytes (empty array + envelope) and logs showed the granularity-422.
2026-05-12 16:57:21 +08:00
Yeadon8888 397d681a09 fix(polymarket): accept localized event URLs (polymarket.com/zh/event/...)
The URL regex `polymarket\.com/event/...` did not match localized variants
served by Polymarket when a user's browser is set to a non-English locale,
e.g. `https://polymarket.com/zh/event/us-x-iran-permanent-peace-deal-by`.
The route then fell through to the fuzzy title-search path with the full URL
as a keyword, which scored unrelated markets — and after the previous fix
turned the silent fallback into a 409, users now see "分析失败" instead.

- Allow an optional 2-letter ISO 639-1 locale (and BCP47 zh-CN style) before
  /event/, /markets/, /market/ in all three regex patterns.
- Tighten `[^/?]+` to `[^/?#]+` so URL fragments don't bleed into the slug.
- When `polymarket.com` appears in the input but no slug/id could be
  extracted, return a clean 400 explaining the expected URL shape instead
  of running fuzzy keyword search on the URL string.
- Add `tests/test_polymarket_url_parsing.py` pinning: plain URL, /zh/ and
  /en/ locales, zh-CN style locale, query string, fragment, numeric
  /markets/ id, /market/ singular path, non-polymarket URL → None,
  three-letter segment (not a real Polymarket path) → None.
2026-05-12 15:57:06 +08:00
Yeadon8888 eef88f769a fix(polymarket): resolve URL slugs precisely, drop silent popular-market fallback
Pasting different Polymarket event URLs in the analysis modal always produced
the same (low-volume, unrelated) market. Root cause was two-fold:

1. `_fetch_market_by_slug` only queried `/markets?slug=xxx`. That works when the
   URL slug is a market slug (single-market events like Como Serie A) but
   returns 0 results for multi-market events like MicroStrategy / Kraken IPO,
   whose URL slug is an *event* slug. The fallback path then fetched the top
   100 active events without using the slug filter and tried to find a match by
   iteration — almost never succeeded for the user's input.

2. The `/api/polymarket/analyze` route, when given a slug, called
   `search_markets(slug)` which performs fuzzy keyword scoring against
   ~100 cached events. When it found no exact match, it silently fell back to
   `search_results[0]` — i.e. the highest-volume market sharing any generic
   word ("2025", "season", "top") with the user's slug. This is why one small
   $1.63K market kept showing up regardless of what URL was pasted.

Changes:

- `data_sources/polymarket.py`: rewrite `_fetch_market_by_slug` to query
  `/markets?slug=xxx` first, then `/events?slug=xxx`. From the event response,
  pick the sub-market whose slug equals the event slug (the "primary" market).
  Return None when neither endpoint matches — no more top-100 scan.

- `routes/polymarket.py`: when the request input contains a Polymarket URL,
  call `get_market_details(slug)` directly and 404 if not found. For non-URL
  title input, only accept the search result when the input is a literal
  substring of the result's question/slug; otherwise return 409 with up to 5
  candidates so the user can pick the right URL — no more silent first-result
  substitution.

- Add regression test `test_polymarket_slug_lookup.py` covering: market-slug
  routing, event-slug routing with sub-market selection, not-found returning
  None (no silent fallback), and empty-input short-circuit.

Smoke-tested against the real Gamma API for four representative slugs
(Como single-market event, MicroStrategy + Kraken IPO multi-market events,
nonsense slug) — all behave correctly.
2026-05-12 15:32:59 +08:00
VicJayandClaude Opus 4.7 160cc6c00f feat(broker): add Alpaca Markets adapter for US stocks, ETFs, and crypto
Mirrors the existing ibkr_trading/ module structure for consistency.
Alpaca complements IBKR for users who prefer:
- Zero commission on stocks/ETFs/crypto
- Stateless REST auth (no TWS/Gateway process to run)
- Unified client for stocks AND crypto
- Built-in paper trading at paper-api.alpaca.markets

What's included:
- backend_api_python/app/services/alpaca_trading/
  - __init__.py, client.py, symbols.py, README.md
  - AlpacaClient mirrors IBKRClient surface (connect, place_market_order,
    place_limit_order, cancel_order, get_account_summary, get_positions,
    get_open_orders, get_quote, get_connection_status)
  - Uses alpaca-py SDK (added to requirements.txt)
- backend_api_python/app/routes/alpaca.py
  - Endpoints mirror routes/ibkr.py: /status, /connect, /disconnect,
    /account, /positions, /orders, /order (POST + DELETE), /quote/<symbol>
- backend_api_python/app/routes/__init__.py
  - Registers alpaca_bp at /api/alpaca
- backend_api_python/env.example
  - ALPACA_API_KEY, ALPACA_SECRET_KEY, ALPACA_PAPER vars added

Tested locally against Alpaca paper account: round-trip BUY/SELL 1 SPY
filled in 5 seconds, $0.04 slippage cost, no commission.

Known gaps (planned for follow-up PRs):
- No bracket / stop / stop-limit order types yet
- No historical bar fetching helpers
- No WebSocket streaming
- No options trading

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:42:46 -07:00
Quantdinger 84d56f27a8 Merge pull request #96 from smc1263/security-and-agent-fix
Security: gate broker routes + CORS allow-list; fix agent token crash
2026-05-09 19:46:41 +08:00
Sean Cunningham f18d72d7a6 fix(agent): unbreak async-job endpoints + document code contract
The async-job submit endpoints (`/backtests`, `/experiments/*`,
`/quick-trade/orders` idempotent replay) all return a 500 with
`TypeError: The view function did not return a valid response. ...
but it was a tuple.` because `envelope()` already returns a
`(jsonify, 200)` tuple, and the routes wrap it AGAIN as
`return envelope(...), 202` — producing a nested
`((Response, 200), 202)` Flask cannot decode.

Fix:
- Add `status=` kwarg to `envelope()` (defaults to 200), so callers
  pass status through the helper instead of wrapping the result.
- Update 5 call sites to use `status=202` / drop the redundant `,200`.

Verified end-to-end: a SMA(10/30) crossover backtest on BTC/USDT 1D
now submits (HTTP 202), runs the indicator (`buy=2, sell=1` over 97
candles), reaches `_simulate_trading_new_format`, and returns a
`succeeded` job with totalReturn=6.39%, sharpeRatio=0.73,
maxDrawdown=-14.53%, 2 trades.

Also: replace the existing AGENT_QUICKSTART.md backtest example
(which used `output = {"signal": ...}` — never a supported shape)
with a working SMA-crossover script, plus a new "code parameter
contract" subsection that documents:
- the pre-bound exec environment (`df`, `np`, `pd`, `params`,
  `call_indicator`, technical-indicator helpers like
  SMA/EMA/RSI/MACD/BOLL/ATR/CROSSOVER/CROSSUNDER)
- the two supported signal shapes (2-way `df['buy']`/`df['sell']`,
  4-way `df['open_long']`/`close_long`/`open_short`/`close_short`)
- a parameterized trend-pullback example with `# @param` declarations
2026-05-08 18:39:30 -04:00
Sean Cunningham 365555c750 fix(agent): re-fetch row after INSERT in admin token issuance
The PostgresCursor wrapper in app/utils/db_postgres.py silently
consumes RETURNING rows into its internal `_last_insert_id` attribute
when handling INSERT statements (db_postgres.py:330-336), so
`cur.fetchone()` afterwards returns None and crashes on subscripting.

Reproduce: POST /api/agent/v1/admin/tokens with a valid admin JWT
panics with `TypeError: 'NoneType' object is not subscriptable` at
admin.py:145.  The token row IS inserted, but the plaintext is lost
because the handler crashes before returning the response — so the
caller has no way to use the issued token.

Fix locally by issuing a SELECT on the unique token_hash after
COMMIT to recover id + created_at.  This is a workaround; a proper
upstream fix is to either stop having the wrapper consume caller-
supplied RETURNING clauses, or expose a `cur.fetchone_after_insert()`
that returns the consumed row.  Filing as an upstream issue.
2026-05-08 17:56:19 -04:00
Sean Cunningham 6942b8f2b3 security: gate broker routes with auth + pin CORS allow-list
- Add @login_required to all 9 IBKR + 11 MT5 routes (status, connect,
  disconnect, account, positions, orders, order POST/DELETE, quote,
  symbols, close). Unauthenticated requests now return 401.
- Replace CORS(app) wildcard with origins read from FRONTEND_URL env
  (defaults to localhost dev origins). Live response confirmed:
  Access-Control-Allow-Origin: <specific origin>, never '*'.

Verified end-to-end: broker routes return 401 without a Bearer token
and 200 / business response with one.

Note: privacy fixes for the USDT QR fetch and affiliate redirect URLs
are NOT included here. Those require a Vue rebuild from working
source, but the upstream Vue 2 -> Vue 3 migration is incomplete and
the rebuilt bundle has runtime errors (ant-design-vue 1.x is Vue 2
only and doesn't render cleanly under @vue/compat). The privacy fixes
are preserved on the migration-wip branch in QuantDinger-Vue and can
land once the migration is buildable.
2026-05-08 12:36:32 -04:00
Dinger 191edbc601 safe
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-08 20:53:47 +08:00
Dinger be615f65ab safe
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-08 20:45:29 +08:00
Dinger f81d576c6c safe
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-08 20:32:10 +08:00
Dinger 7d7774c24a mt5
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-08 18:54:05 +08:00
Dinger cb6ba08daa safe update
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-08 13:41:36 +08:00
Dinger 12d7fbfc9e v3.0.3
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-07 17:01:53 +08:00
Claude 7460e1d92c ci(railway): add railway.json so Metal builder uses backend Dockerfile
Railway's Metal builder (Railpack v0.23) was rejecting backend_api_python/Dockerfile because the platform-level "acceptChildOfRepoRoot" guard refuses to build a Dockerfile that sits below the service Root Directory, and Railpack itself has no Dockerfile provider — only language autodetection — so the build fell through to "could not determine how to build the app".

Setting Root Directory = backend_api_python in the Railway service makes this file the effective build manifest: it pins the DOCKERFILE builder against the existing Dockerfile, with the same /api/health probe docker-compose already uses.
2026-05-06 22:49:10 +00:00
Bortlesboat d0f6152e3a add three minute kline timeframe 2026-05-05 13:42:59 -04:00
Dinger ec81c74ea3 v3.0.3
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-04 12:36:01 +08:00
Dinger 361c214a0b v3.0.3
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-03 18:07:59 +08:00
Dinger 7426138fe5 mcp
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-02 16:15:30 +08:00
Dinger cecbe3ddb0 v3.0.3
Signed-off-by: Dinger <quantdinger@gmail.com>
2026-05-02 15:35:55 +08:00
dinger 2e59568494 fix(trading): merge bot_params into script ctx so martingale TP/SL params apply
Made-with: Cursor
2026-04-29 16:11:24 +08:00
dinger 361f0c8c6b security(moex): validate ticker/board for ISS URL path; add tests
Made-with: Cursor
2026-04-29 14:17:59 +08:00
Claude Code eee8b02af2 feat(data): add MOEX (Moscow Exchange) equities data source for analysis/backtest
Adds a read-only MOEX data source backed by the public MOEX ISS HTTP API
(https://iss.moex.com/iss). Targets the TQBR equities board so common Russian
tickers (SBER, GAZP, LKOH, ...) work out of the box.

What's new
- app/data_sources/moex.py: MOEXDataSource with get_kline / get_ticker
  - QuantDinger timeframes mapped to ISS intervals; 5m/15m/30m/4H are
    resampled from the nearest finer native interval (1m or 60m)
  - ISS naive timestamps treated as Europe/Moscow (UTC+3) and converted to
    Unix UTC seconds
  - Pagination via ISS start parameter
  - Symbol normalization handles SBER, SBER.ME, GAZP:MOEX, etc.
- app/data_sources/factory.py: registers MOEX market + aliases
- app/routes/market.py: MOEX in /market/types between HKStock and Crypto
- app/services/symbol_name.py: resolves MOEX names from ISS securities endpoint
- app/services/strategy.py: explicit guard - MOEX cannot be used as a live
  trading market_category (create / batch / update paths)
- tests/test_moex_data_source.py: 11 offline unit tests (mocked HTTP)
- scripts/verify_moex.py: live ISS smoke-test script

Limitations
- Analysis & backtesting only. No live order placement on MOEX.
- TQBR board only by default. Other boards can be enabled by constructing
  MOEXDataSource(board=...) explicitly.
- Sub-hour non-native timeframes (5m/15m/30m) and 4H are resampled
  client-side; large limits at fine timeframes pull more data.
2026-04-28 13:40:39 +00:00
dinger 0f5b09ae74 fix: demo flags for exchange test, strategy start errors, STRATEGY_MAX_THREADS docs
- Merge root-level demo/testnet/base_url into exchange_config for test-connection.
- Centralize exchange_demo_mode_enabled and use in factory, credentials, Binance hints.
- Surface TradingExecutor start failures (thread limit, etc.) in API response.
- Document STRATEGY_MAX_THREADS in env.example.

Made-with: Cursor
2026-04-27 16:20:36 +08:00
dinger caaf294867 chore(security): 升级 Python 依赖并启用 Dependabot
- Flask 3.1.3、Werkzeug>=3.1.6、flask-cors 5.0.1(修复已知 GHSA)
- PyJWT>=2.12、cryptography>=43、requests>=2.32
- 新增 .github/dependabot.yml 每周检查 backend_api_python pip 依赖

Made-with: Cursor
2026-04-25 03:05:55 +08:00
dinger 5f60f419e6 feat: 数据源与指标 IDE 体验,同步前端 dist
后端:
- DataSourceFactory:市场枚举 normalize;K 线/报价按请求 market 选源
- 外汇:符号紧凑化、Tiingo ticker 429 分支 cache_key 修复
- 回测路由:strip + normalize_market
- 多数据源模块同步调整(base/stocks/crypto/futures 等)

前端:
- 更新 frontend/dist 构建产物(IDE 工作区 Tab、自选 market、点击名称上 K 线等)
- 说明:QuantDinger-Vue-src 在本仓库 .gitignore,源码需在单独前端仓库提交

Made-with: Cursor
2026-04-25 02:53:16 +08:00
dinger 72f5b269a5 feat(brokers): IBKR/MT5 API fixes, cloud policy, desktop-brokers endpoint; chore(frontend): sync dist
Made-with: Cursor
2026-04-24 22:33:55 +08:00
dinger 5c3b4f6149 chore(deps): drop unused pymysql and SQLAlchemy (PostgreSQL-only stack)
Made-with: Cursor
2026-04-24 11:35:18 +08:00
dinger e6091cf281 refactor(docker): auto apt/pip mirror with fallback; drop extra build env
- Try Aliyun Debian sources; restore official if apt-get update fails.
- Try Aliyun PyPI; retry pip with default index on failure.
- Remove USE_CN_MIRROR / PIP_INDEX_URL compose args and .env knobs.

Made-with: Cursor
2026-04-24 11:19:50 +08:00
dinger 70880eaf27 chore(docker): optional CN mirrors for apt and pip during backend build
- USE_CN_MIRROR=1 switches Debian bookworm sources to mirrors.aliyun.com.
- PIP_INDEX_URL passes through to pip -i for requirements install.
- Document IMAGE_PREFIX, registry-mirrors, and compose args in .env.example and DEVELOPMENT.md.

Made-with: Cursor
2026-04-24 11:16:20 +08:00
dinger b91436bcd6 feat(billing): USDT-only membership; disable mock purchase and admin mock rows
- Return 403 on POST /api/billing/purchase (use usdt/create).
- purchase_membership: optional record_membership_order; USDT activation skips qd_membership_orders.
- Admin orders list: qd_usdt_orders only.
- Clarify membership price settings copy (no mock wording).

Made-with: Cursor
2026-04-23 12:49:57 +08:00
dinger ebf30cf67d fix(llm): reject Custom provider when CUSTOM_API_URL is empty
Made-with: Cursor
2026-04-23 12:32:21 +08:00
dinger 263b69872f Merge pull request #57 from octo-patch/feat/minimax-m2-7-provider
Resolve conflicts with main: keep Custom LLM (CUSTOM_*) and add MiniMax
(MINIMAX_*), settings schema, config_loader mappings, and env.example.

Made-with: Cursor
2026-04-23 12:24:19 +08:00
dinger b134817d47 fix(llm): wire CUSTOM_API_URL into config cache and base URL resolution
PR #56 added Custom provider keys but get_base_url only read CUSTOM_BASE_URL;
map CUSTOM_* env vars in config_loader and fall back to CUSTOM_API_URL/APIKeys.

Made-with: Cursor
2026-04-23 12:18:16 +08:00
octo-patch 72d5c35299 feat(llm): add MiniMax-M2.7 provider support
Add MiniMax as a supported LLM provider alongside OpenRouter, OpenAI,
Google Gemini, DeepSeek, and Grok. MiniMax uses an OpenAI-compatible
REST API, so the existing _call_openai_compatible() handles it.

- LLMProvider.MINIMAX enum value with base_url/default_model/fallback
- MINIMAX_API_KEY metaclass property in APIKeys
- MINIMAX_API_KEY/MODEL/BASE_URL env var mappings in config_loader
- MiniMax option in settings route LLM provider selector
- MINIMAX_API_KEY, MINIMAX_MODEL, MINIMAX_BASE_URL in env.example
2026-04-23 00:35:56 +08:00
santbabaq-opsandClaude Opus 4.7 4bab7a1ff0 feat: add custom LLM provider support for third-party AI models
- Add CUSTOM provider to LLMProvider enum
- Support custom API URL, API key, and model name configuration
- Add new settings UI for custom AI endpoint setup
- Compatible with any OpenAI-API-compatible endpoint

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-22 21:48:27 +08:00
dinger 65d8f1a214 fix(indicator): flatten AI prompt newlines in fallback template
- Use real newline replacement and guard empty prompt (Python 3.12+ f-string).
- chore(binance_spot): append actionable hint when Binance returns -2015.

Made-with: Cursor
2026-04-22 14:35:55 +08:00
QuantdingerandCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> 9caa3aca82 Potential fix for code scanning alert no. 253: Clear-text logging of sensitive information
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-22 14:18:42 +08:00
QuantdingerandCopilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> ecce15b74b Potential fix for pull request finding 'CodeQL / Information exposure through an exception'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-22 14:03:48 +08:00