Skip to content

Commit 0bc58d0

Browse files
v3.0.6
Signed-off-by: Dinger <quantdinger@gmail.com>
1 parent 67cc662 commit 0bc58d0

45 files changed

Lines changed: 2009 additions & 197 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend_api_python/app/__init__.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,46 @@ def create_app(config_name='default'):
277277
).split(",")
278278
if o.strip()
279279
]
280-
CORS(app, origins=_cors_origins)
280+
281+
# ------------------------------------------------------------------
282+
# Capacitor / Cordova / Ionic mobile app origins.
283+
#
284+
# When the H5 frontend is packaged as a native app via Capacitor, the
285+
# WebView loads pages from a synthetic origin (NOT from your real
286+
# domain), and every API request to the production backend is treated
287+
# as cross-origin by the WebView. The exact origin depends on the
288+
# Capacitor server config:
289+
# - Android (capacitor 6, androidScheme="https") → https://localhost
290+
# - Android (capacitor 5 or scheme="http") → http://localhost
291+
# - iOS (capacitor 6, iosScheme="https") → capacitor://localhost
292+
# - iOS legacy (ionic://) → ionic://localhost
293+
# - Cordova / file:// loaded apps → null (Origin: null)
294+
#
295+
# We always allow these so a packaged QuantDinger mobile app can call
296+
# the backend without each user editing FRONTEND_URL. They are *fixed
297+
# synthetic origins controlled by the OS / Capacitor*, not user input,
298+
# so this does not widen exposure to real third-party sites.
299+
_capacitor_origins = [
300+
"https://localhost", # Android, Capacitor 6, androidScheme=https
301+
"http://localhost", # Android legacy
302+
"capacitor://localhost", # iOS, Capacitor 6, iosScheme=capacitor
303+
"ionic://localhost", # iOS legacy / Ionic
304+
"https://localhost:*", # rare custom port
305+
"http://localhost:*",
306+
]
307+
for origin in _capacitor_origins:
308+
if origin not in _cors_origins:
309+
_cors_origins.append(origin)
310+
311+
# send_wildcard=False + supports_credentials=False is the safe default
312+
# for token-in-Authorization-header auth (which is what the mobile app
313+
# uses; see api/index.js → `Authorization: Bearer ${token}`).
314+
CORS(
315+
app,
316+
origins=_cors_origins,
317+
supports_credentials=False,
318+
send_wildcard=False,
319+
)
281320
logger.info(f"CORS allowed origins: {_cors_origins}")
282321

283322
setup_logger()

backend_api_python/app/data_sources/factory.py

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,41 @@ class DataSourceFactory:
3838

3939
_sources: Dict[str, BaseDataSource] = {}
4040

41+
# Markets that pass through normalize_market unchanged.
42+
_CANONICAL_MARKETS = ("Crypto", "Forex", "Futures", "USStock", "CNStock", "HKStock", "MOEX")
43+
4144
@classmethod
4245
def normalize_market(cls, market: str) -> str:
43-
"""统一市场枚举大小写与别名,供路由与数据源入口使用。"""
46+
"""
47+
Normalize a market category string.
48+
49+
IMPORTANT: empty / unknown input used to silently degrade to "Crypto",
50+
which made stock symbols like TSLA quietly route to CCXT/Coinbase. We
51+
keep that fallback for backward compatibility (some callers still rely
52+
on it) but emit a loud WARNING so the misroute is no longer invisible.
53+
Always pass a real market category from the caller.
54+
"""
4455
if not market:
56+
logger.warning(
57+
"DataSourceFactory.normalize_market(): empty market category — "
58+
"falling back to 'Crypto'. Caller MUST supply an explicit market "
59+
"(USStock / Forex / Futures / Crypto / CNStock / HKStock / MOEX). "
60+
"This fallback is deprecated and will become a hard error.",
61+
stack_info=False,
62+
)
4563
return "Crypto"
4664
raw = str(market).strip()
47-
if raw in ("Crypto", "Forex", "Futures", "USStock", "CNStock", "HKStock", "MOEX"):
65+
if raw in cls._CANONICAL_MARKETS:
4866
return raw
4967
key = raw.lower().replace(" ", "").replace("-", "_")
50-
return _MARKET_ALIASES.get(key, raw)
68+
if key in _MARKET_ALIASES:
69+
return _MARKET_ALIASES[key]
70+
logger.warning(
71+
"DataSourceFactory.normalize_market(): unknown market %r — "
72+
"passing through as-is; downstream get_source() will likely fail.",
73+
raw,
74+
)
75+
return raw
5176

5277
@classmethod
5378
def get_source(cls, market: str) -> BaseDataSource:
@@ -74,13 +99,22 @@ def get_data_source(cls, name: str) -> BaseDataSource:
7499
In the localized Python backend we primarily use `get_source("Crypto")`.
75100
"""
76101
key = (name or "").strip().lower()
77-
if key in ("crypto", "binance", "okx", "bybit", "bitget", "kucoin", "gate", "mexc", "kraken", "coinbase"):
102+
if key in ("crypto", "binance", "okx", "bybit", "bitget", "kucoin", "gate", "mexc", "kraken", "coinbase", "alpaca_crypto"):
78103
return cls.get_source("Crypto")
79104
if key in ("futures",):
80105
return cls.get_source("Futures")
81-
if key in ("forex", "fx"):
106+
if key in ("forex", "fx", "mt5"):
82107
return cls.get_source("Forex")
83-
# Default to Crypto for safety (most callers want a ticker for crypto pairs).
108+
if key in ("usstock", "us_stocks", "stock", "stocks", "ibkr", "alpaca"):
109+
return cls.get_source("USStock")
110+
# Unknown alias — log and default to Crypto (legacy behavior). Callers
111+
# should migrate to the explicit `get_source(market)` API.
112+
logger.warning(
113+
"DataSourceFactory.get_data_source(%r): unknown alias — falling back "
114+
"to Crypto. Migrate caller to get_source(market) with an explicit "
115+
"market category.",
116+
name,
117+
)
84118
return cls.get_source("Crypto")
85119

86120
@classmethod

backend_api_python/app/routes/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def register_routes(app: Flask):
2828
from app.routes.billing import billing_bp
2929
from app.routes.quick_trade import quick_trade_bp
3030
from app.routes.experiment import experiment_bp
31+
from app.routes.policy import policy_bp
3132

3233
app.register_blueprint(health_bp)
3334
app.register_blueprint(auth_bp, url_prefix='/api/auth') # Auth routes
@@ -51,6 +52,7 @@ def register_routes(app: Flask):
5152
app.register_blueprint(billing_bp, url_prefix='/api/billing')
5253
app.register_blueprint(quick_trade_bp, url_prefix='/api/quick-trade')
5354
app.register_blueprint(experiment_bp, url_prefix='/api/experiment')
55+
app.register_blueprint(policy_bp, url_prefix='/api/policy')
5456

5557
# Agent Gateway (/api/agent/v1) — versioned, scoped surface for AI agents.
5658
# See docs/agent/AI_INTEGRATION_DESIGN.md.
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""
2+
Policy / capability discovery routes.
3+
4+
Read-only endpoints that expose backend policy matrices to the frontend so
5+
the UI does not have to hard-code its own copy of broker x market rules.
6+
The frontend fetches these once at app boot and caches them in
7+
sessionStorage; nothing here is per-user, so caching is safe.
8+
"""
9+
from flask import Blueprint, jsonify
10+
11+
from app.services.broker_market_policy import to_dict as broker_market_policy_dict
12+
13+
14+
policy_bp = Blueprint('policy', __name__)
15+
16+
17+
@policy_bp.route('/broker-market', methods=['GET'])
18+
def get_broker_market_policy():
19+
"""Return the full broker x market x market_type compatibility matrix.
20+
21+
Response shape (kept stable for the frontend):
22+
{
23+
"code": 1,
24+
"data": {
25+
"broker_markets": {
26+
"ibkr": {"USStock": ["spot"]},
27+
"mt5": {"Forex": ["spot"]},
28+
"alpaca": {"USStock": ["spot"], "Crypto": ["spot"]},
29+
"binance": {"Crypto": ["spot", "swap"]},
30+
...
31+
},
32+
"long_only_brokers": ["alpaca", "ibkr"],
33+
"bot_type_markets": {
34+
"grid": ["Crypto", "Forex"],
35+
"martingale": ["Crypto"],
36+
"dca": ["Crypto", "Forex", "USStock"],
37+
"trend": ["Crypto", "Forex", "USStock"]
38+
},
39+
"live_market_categories": ["Crypto", "Forex", "USStock"]
40+
}
41+
}
42+
"""
43+
return jsonify({"code": 1, "data": broker_market_policy_dict()})

0 commit comments

Comments
 (0)