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>
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>
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.
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.
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.
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
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.
- 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.
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.
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.
- 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
- 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
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
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
- 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>
- 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>