Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
46577ab
feat: expand factor research and quick trade depth
tianhe Nov 1, 2026
bab1a3a
fix(quick-trade): normalize derivative order book units
tianhe Nov 1, 2026
235a5d7
docs(api): refresh OpenAPI export
tianhe Nov 1, 2026
5d64dac
security: harden secret handling and webhooks
tianhe Nov 1, 2026
aead75c
security: prevent exception detail exposure
tianhe Nov 1, 2026
05acff0
ci: satisfy quick trade size guardrail
tianhe Nov 1, 2026
1743af9
feat(research): add custom factor AI and crypto history paging
tianhe Nov 2, 2026
24a3f79
fix(research): avoid backtracking in factor JSON parsing
tianhe Nov 3, 2026
a1b97f6
chore: sync main into test
tianhe Nov 3, 2026
465f38d
feat: harden research, trading, and deployment flows
tianhe Nov 4, 2026
4dbc25a
fix(ci): restore backend quality gates
tianhe Nov 4, 2026
bf0cbaa
fix(market): preserve exact crypto product identity
tianhe Nov 4, 2026
f99ea50
Merge remote-tracking branch 'origin/main' into test
tianhe Nov 4, 2026
c89626e
security(api): hide internal exception details
tianhe Nov 4, 2026
a8c3971
feat(api): harden analysis reports and public errors
tianhe Nov 4, 2026
7ac898d
Merge remote-tracking branch 'origin/main' into pr349-dco-fix
tianhe Nov 4, 2026
1e7a409
fix(ci): keep fast analysis within quality baseline
tianhe Nov 4, 2026
7acfb09
feat(ai): modernize scheduled monitor reports
tianhe Nov 5, 2026
7f62304
Merge remote-tracking branch 'origin/main' into test
tianhe Nov 5, 2026
59cd27c
feat(market): generate localized share drafts
tianhe Nov 5, 2026
24b43fc
fix(security): sanitize share generation errors
tianhe Nov 5, 2026
35b1f59
fix(market): satisfy share draft quality checks
tianhe Nov 5, 2026
3ef8a2e
fix(runtime): harden marketplace metrics and evaluator capacity
tianhe Nov 5, 2026
d7291e2
fix(security): prevent exception detail exposure
tianhe Nov 5, 2026
d5e45bb
Merge remote-tracking branch 'origin/main' into test
tianhe Nov 5, 2026
ef6ef1f
Merge remote-tracking branch 'origin/main' into test
tianhe Nov 6, 2026
f787087
feat: harden asset identity and live recovery
tianhe Nov 6, 2026
6c24643
fix: restore backend CI quality gates
tianhe Nov 6, 2026
ec4f61d
fix: sanitize asset resolution errors
tianhe Nov 6, 2026
65705a9
fix: align agent contracts and runtime tests
tianhe Nov 6, 2026
7900f32
test: refresh hardened runtime fixtures
tianhe Nov 6, 2026
0e464f9
feat: harden strategy candle readiness and recovery
tianhe Nov 6, 2026
7c677e5
Merge remote-tracking branch 'origin/main' into test
tianhe Nov 6, 2026
bf5b56c
fix(backtest): align crypto candle coverage boundaries (#359)
brokermr810 Nov 8, 2026
4d43538
fix(trading): normalize Binance protection orders
tianhe Nov 10, 2026
b50e459
fix(trading): snap Bitget and Bybit native protection prices to the tick
aliassar Nov 10, 2026
c4bf901
fix(trading): normalize protection orders across venues
tianhe Nov 11, 2026
08dbf0c
Merge branch 'main' into test
brokermr810 Nov 11, 2026
a2c968c
fix(trading): enforce run-scoped liquidation risk stops
tianhe Nov 11, 2026
ed391bf
fix(grid): support legacy recovery engines
tianhe Nov 11, 2026
80fcf31
fix(runtime): separate strategy lifecycle generations
tianhe Nov 11, 2026
06564e3
Merge remote-tracking branch 'origin/main' into test
tianhe Nov 11, 2026
49276ac
Merge remote-tracking branch 'origin/main' into test
tianhe Nov 13, 2026
06d0f26
fix(trading): reconcile protection fills and spot residuals
tianhe Nov 13, 2026
464683e
fix(db): prevent runtime lock convoys
tianhe Nov 14, 2026
914d20d
Merge remote-tracking branch 'origin/main' into test
tianhe Nov 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ DB_POOL_MIN=5
DB_POOL_MAX=auto
DB_POOL_ACQUIRE_TIMEOUT=10
DB_POOL_HEALTH_CHECK=true
DB_IDLE_IN_TRANSACTION_TIMEOUT_MS=60000
DB_LOCK_TIMEOUT_MS=10000
DB_POOL_AUTO_CAP=true
DB_POOL_AUTO_DEFAULT_MAX=50
DB_POOL_RESERVE_FOR_OTHER_CLIENTS=20
Expand Down
79 changes: 0 additions & 79 deletions backend_api_python/app/services/billing_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def get_user_vip_status(self, user_id: int) -> Tuple[bool, Optional[datetime]]:
with get_db_connection() as db:
cur = db.cursor()
# Ensure lifetime membership monthly credits are granted (best-effort, silent on failure).
self._ensure_membership_schema_best_effort(cur)
self._grant_lifetime_monthly_credits_best_effort(cur, user_id)
try:
db.commit()
Expand Down Expand Up @@ -124,7 +123,6 @@ def get_membership_plans(self, include_inactive: bool = False) -> Dict[str, Any]
try:
with get_db_connection() as db:
cur = db.cursor()
self._ensure_plan_schema(cur)
self._seed_default_plans(cur)
sql = """
SELECT code, name, description, price_usd, duration_days,
Expand Down Expand Up @@ -187,7 +185,6 @@ def save_membership_plans(self, items) -> Tuple[bool, str, Dict[str, Any]]:
try:
with get_db_connection() as db:
cur = db.cursor()
self._ensure_plan_schema(cur)
for row in normalized:
cur.execute(
"""
Expand Down Expand Up @@ -232,7 +229,6 @@ def delete_membership_plan(self, code: str) -> Tuple[bool, str, Dict[str, Any]]:
try:
with get_db_connection() as db:
cur = db.cursor()
self._ensure_plan_schema(cur)
cur.execute("SELECT code, is_active FROM qd_billing_plans WHERE code=? FOR UPDATE", (code,))
plan = cur.fetchone() or {}
if not plan:
Expand Down Expand Up @@ -273,32 +269,6 @@ def _serialize_plan(row: Dict[str, Any]) -> Dict[str, Any]:
"stripe_price_id": row.get("stripe_price_id") or "",
}

def _ensure_plan_schema(self, cur) -> None:
cur.execute(
"""
CREATE TABLE IF NOT EXISTS qd_billing_plans (
code VARCHAR(64) PRIMARY KEY,
name VARCHAR(120) NOT NULL,
description TEXT NOT NULL DEFAULT '',
price_usd DECIMAL(12,2) NOT NULL DEFAULT 0,
duration_days INTEGER NOT NULL DEFAULT 0,
credits_once INTEGER NOT NULL DEFAULT 0,
credits_monthly INTEGER NOT NULL DEFAULT 0,
strategy_limit INTEGER NOT NULL DEFAULT 10,
referral_eligible BOOLEAN NOT NULL DEFAULT FALSE,
is_lifetime BOOLEAN NOT NULL DEFAULT FALSE,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
is_popular BOOLEAN NOT NULL DEFAULT FALSE,
sort_order INTEGER NOT NULL DEFAULT 0,
stripe_price_id VARCHAR(255) NOT NULL DEFAULT '',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
"""
)
cur.execute("ALTER TABLE qd_billing_plans ADD COLUMN IF NOT EXISTS strategy_limit INTEGER NOT NULL DEFAULT 10")
cur.execute("ALTER TABLE qd_billing_plans ADD COLUMN IF NOT EXISTS referral_eligible BOOLEAN NOT NULL DEFAULT FALSE")

def _seed_default_plans(self, cur) -> None:
cur.execute("SELECT COUNT(*) AS count FROM qd_billing_plans")
if int((cur.fetchone() or {}).get("count") or 0) > 0:
Expand Down Expand Up @@ -342,13 +312,8 @@ def purchase_membership(
try:
with get_db_connection() as db:
cur = db.cursor()
self._ensure_membership_schema_best_effort(cur)
if record_membership_order:
self._ensure_membership_orders_table_best_effort(cur)

fulfillment_ref = (fulfillment_ref or "").strip()
if fulfillment_ref:
self._ensure_fulfillment_schema(cur)
cur.execute(
"""
INSERT INTO qd_membership_fulfillments (provider_ref, user_id, plan, created_at)
Expand Down Expand Up @@ -525,49 +490,6 @@ def purchase_membership(
logger.error(f"purchase_membership failed: {e}", exc_info=True)
return False, f"error:{str(e)}", {}

def _ensure_membership_schema_best_effort(self, cur):
"""Best-effort schema upgrade for membership fields on qd_users."""
try:
# vip_plan / vip_is_lifetime / vip_monthly_credits_last_grant
cur.execute("ALTER TABLE qd_users ADD COLUMN IF NOT EXISTS vip_plan VARCHAR(64) DEFAULT ''")
cur.execute("ALTER TABLE qd_users ALTER COLUMN vip_plan TYPE VARCHAR(64)")
cur.execute("ALTER TABLE qd_users ADD COLUMN IF NOT EXISTS vip_is_lifetime BOOLEAN DEFAULT FALSE")
cur.execute("ALTER TABLE qd_users ADD COLUMN IF NOT EXISTS vip_monthly_credits_last_grant TIMESTAMP")
except Exception:
# Ignore schema upgrade failures (e.g., insufficient privileges)
pass

def _ensure_membership_orders_table_best_effort(self, cur):
"""Best-effort create membership orders table (legacy; optional row per mock checkout)."""
try:
cur.execute(
"""
CREATE TABLE IF NOT EXISTS qd_membership_orders (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
plan VARCHAR(64) NOT NULL,
price_usd DECIMAL(10,2) DEFAULT 0,
status VARCHAR(20) DEFAULT 'paid',
created_at TIMESTAMP DEFAULT NOW(),
paid_at TIMESTAMP
)
"""
)
except Exception:
pass

def _ensure_fulfillment_schema(self, cur):
cur.execute(
"""
CREATE TABLE IF NOT EXISTS qd_membership_fulfillments (
provider_ref VARCHAR(255) PRIMARY KEY,
user_id INTEGER NOT NULL,
plan VARCHAR(64) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
)
"""
)

def _add_credits_in_tx(self, cur, user_id: int, amount: int, action: str, remark: str, reference_id: str = ''):
"""Add credits within an existing DB transaction and write qd_credits_log."""
try:
Expand Down Expand Up @@ -600,7 +522,6 @@ def _grant_lifetime_monthly_credits_best_effort(self, cur, user_id: int):
return
# Reuse the caller's transaction. Opening another pooled connection
# here can deadlock a small deployment when all connections are busy.
self._ensure_plan_schema(cur)
self._seed_default_plans(cur)
cur.execute(
"SELECT credits_monthly FROM qd_billing_plans WHERE code = ?",
Expand Down
15 changes: 1 addition & 14 deletions backend_api_python/app/services/user_preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,7 @@


def ensure_chart_templates_column() -> None:
"""Add qd_users.chart_templates when upgrading existing databases."""
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
ALTER TABLE qd_users
ADD COLUMN IF NOT EXISTS chart_templates TEXT DEFAULT ''
"""
)
db.commit()
cur.close()
except Exception as exc:
logger.warning("ensure chart_templates column skipped: %s", exc)
"""Compatibility hook; canonical schema changes run in migrations."""


def get_notification_settings(user_id: int) -> dict | None:
Expand Down
75 changes: 24 additions & 51 deletions backend_api_python/app/services/user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,36 +92,8 @@ def normalize_user_record(cls, user: Optional[Dict[str, Any]]) -> Optional[Dict[
return normalized

def ensure_password_changed_column(self) -> None:
"""Add password_changed_at if missing (idempotent)."""
if UserService._password_changed_column_ready:
return
try:
with get_db_connection() as db:
cur = db.cursor()
cur.execute(
"""
SELECT 1 FROM information_schema.columns
WHERE table_name = 'qd_users' AND column_name = 'password_changed_at'
"""
)
if not cur.fetchone():
cur.execute(
"ALTER TABLE qd_users ADD COLUMN password_changed_at TIMESTAMP NULL"
)
# Existing accounts: assume they already passed initial setup.
cur.execute(
"""
UPDATE qd_users
SET password_changed_at = COALESCE(updated_at, created_at, NOW())
WHERE password_changed_at IS NULL
"""
)
db.commit()
logger.info("Added password_changed_at column to qd_users (existing rows backfilled)")
cur.close()
UserService._password_changed_column_ready = True
except Exception as e:
logger.warning(f"ensure_password_changed_column failed: {e}")
"""Compatibility hook; canonical schema changes run in migrations."""
UserService._password_changed_column_ready = True

def mark_password_changed(self, user_id: int) -> None:
"""Record that the user has set a non-initial password."""
Expand Down Expand Up @@ -1245,27 +1217,28 @@ def ensure_admin_exists(self):
cur.execute("SELECT COUNT(*) as count FROM qd_users")
count = cur.fetchone()['count']
cur.close()

if count == 0:
# Create admin using env credentials
from app.config.settings import Config
admin_user = os.getenv('ADMIN_USER', Config.ADMIN_USER)
admin_password = os.getenv('ADMIN_PASSWORD', Config.ADMIN_PASSWORD)
admin_email = os.getenv('ADMIN_EMAIL', 'admin@example.com')

self.create_user({
'username': admin_user,
'password': admin_password,
'email': admin_email,
'nickname': 'Administrator',
'role': 'admin',
'status': 'active',
'email_verified': True # Admin email is pre-verified
})
logger.info(f"Created admin user: {admin_user} ({admin_email})")
else:
self.sync_bootstrap_admin_credentials_from_env()
self.sync_admin_email_from_config(overwrite_existing=False)

# Do not call helpers that acquire another pooled connection while
# the startup read transaction is still open.
if count == 0:
from app.config.settings import Config
admin_user = os.getenv('ADMIN_USER', Config.ADMIN_USER)
admin_password = os.getenv('ADMIN_PASSWORD', Config.ADMIN_PASSWORD)
admin_email = os.getenv('ADMIN_EMAIL', 'admin@example.com')

self.create_user({
'username': admin_user,
'password': admin_password,
'email': admin_email,
'nickname': 'Administrator',
'role': 'admin',
'status': 'active',
'email_verified': True
})
logger.info(f"Created admin user: {admin_user} ({admin_email})")
else:
self.sync_bootstrap_admin_credentials_from_env()
self.sync_admin_email_from_config(overwrite_existing=False)
except Exception as e:
logger.error(f"ensure_admin_exists failed: {e}")

Expand Down
65 changes: 57 additions & 8 deletions backend_api_python/app/utils/db_postgres.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
DB_POOL_MAX maxconn or "auto" default auto
DB_POOL_ACQUIRE_TIMEOUT seconds to wait on exhaustion default 10
DB_POOL_HEALTH_CHECK "true" / "false" default "true"
DB_IDLE_IN_TRANSACTION_TIMEOUT_MS server-side timeout default 60000
DB_LOCK_TIMEOUT_MS server-side lock wait timeout default 10000
"""
import os
import time
Expand Down Expand Up @@ -81,6 +83,8 @@ def _env_bool(key: str, default: bool) -> bool:
DB_POOL_HEALTH_CHECK = _env_bool("DB_POOL_HEALTH_CHECK", True)
DB_POOL_AUTO_CAP = _env_bool("DB_POOL_AUTO_CAP", True)
DB_POOL_RESERVE_FOR_OTHER_CLIENTS = _env_int("DB_POOL_RESERVE_FOR_OTHER_CLIENTS", 20)
DB_IDLE_IN_TRANSACTION_TIMEOUT_MS = _env_int("DB_IDLE_IN_TRANSACTION_TIMEOUT_MS", 60_000)
DB_LOCK_TIMEOUT_MS = _env_int("DB_LOCK_TIMEOUT_MS", 10_000)
DB_APPLICATION_NAME = os.getenv("DB_APPLICATION_NAME", "quantdinger_api").strip() or "quantdinger_api"


Expand Down Expand Up @@ -172,7 +176,11 @@ def _get_connection_pool():
# "idle in transaction" state when no explicit commit/rollback
# followed). keepalives keep dead sockets from lingering in
# the pool when the PG side or a NAT drops them.
options="-c timezone=UTC",
options=(
"-c timezone=UTC "
f"-c idle_in_transaction_session_timeout={DB_IDLE_IN_TRANSACTION_TIMEOUT_MS} "
f"-c lock_timeout={DB_LOCK_TIMEOUT_MS}"
),
keepalives=1,
keepalives_idle=30,
keepalives_interval=10,
Expand Down Expand Up @@ -317,6 +325,23 @@ def _pool_max_config_label() -> str:
return str(DB_POOL_MAX_CONFIGURED) if DB_POOL_MAX_CONFIGURED is not None else "auto"


def _rollback_open_transaction(conn) -> bool:
"""Return a pooled connection to PostgreSQL's idle transaction state."""
if conn is None or getattr(conn, "closed", 0):
return False

try:
status_reader = getattr(conn, "get_transaction_status", None)
if callable(status_reader):
idle_status = psycopg2.extensions.TRANSACTION_STATUS_IDLE
if status_reader() == idle_status:
return True
conn.rollback()
return True
except Exception:
return False


def _is_connection_healthy(conn) -> bool:
"""Quick health check: make sure the connection is not closed and can
actually round-trip a trivial query. Used only when DB_POOL_HEALTH_CHECK
Expand All @@ -327,14 +352,28 @@ def _is_connection_healthy(conn) -> bool:
# psycopg2 sets .closed to nonzero when the connection is closed.
if getattr(conn, "closed", 0):
return False
if not _rollback_open_transaction(conn):
return False

probe_ok = False
cur = None
try:
cur = conn.cursor()
cur.execute("SELECT 1")
cur.fetchone()
cur.close()
return True
probe_ok = True
except Exception:
return False
probe_ok = False
finally:
if cur is not None:
try:
cur.close()
except Exception:
probe_ok = False

# psycopg2 starts a transaction even for SELECT 1. Never let the health
# probe itself put a pooled connection into idle-in-transaction state.
return _rollback_open_transaction(conn) and probe_ok


def _acquire_conn_with_wait(pg_pool):
Expand Down Expand Up @@ -413,7 +452,11 @@ def _acquire_conn_with_wait(pg_pool):
backoff = min(backoff * 2, 0.5)
continue

if DB_POOL_HEALTH_CHECK and not _is_connection_healthy(conn):
connection_ready = _rollback_open_transaction(conn)
if connection_ready and DB_POOL_HEALTH_CHECK:
connection_ready = _is_connection_healthy(conn)

if not connection_ready:
# Drop the dead connection and let the pool create a new one on
# next attempt. putconn(close=True) asks the pool to discard it.
try:
Expand Down Expand Up @@ -658,10 +701,14 @@ def close(self):
"""Return connection to pool. Broken connections are discarded so
we don't poison the pool with closed sockets.
"""
if self._pool and self._conn:
conn = self._conn
self._conn = None
if self._pool and conn:
try:
broken = bool(getattr(self._conn, "closed", 0))
self._pool.putconn(self._conn, close=broken)
broken = bool(getattr(conn, "closed", 0))
if not broken and not _rollback_open_transaction(conn):
broken = True
self._pool.putconn(conn, close=broken)
_observe_pool_stats(self._pool)
except Exception as e:
logger.warning(f"Failed to return connection to pool: {e}")
Expand Down Expand Up @@ -772,6 +819,8 @@ def get_pg_connection():
finally:
if conn is not None:
try:
if not broken and not _rollback_open_transaction(conn):
broken = True
pg_pool.putconn(conn, close=broken)
_observe_pool_stats(pg_pool)
except Exception:
Expand Down
2 changes: 2 additions & 0 deletions backend_api_python/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,8 @@ DB_POOL_MIN=5
DB_POOL_MAX=auto
DB_POOL_ACQUIRE_TIMEOUT=10
DB_POOL_HEALTH_CHECK=true
DB_IDLE_IN_TRANSACTION_TIMEOUT_MS=60000
DB_LOCK_TIMEOUT_MS=10000
DB_POOL_AUTO_CAP=true
DB_POOL_AUTO_DEFAULT_MAX=50
DB_POOL_RESERVE_FOR_OTHER_CLIENTS=20
Expand Down
Loading
Loading