迁移 psycopg3 并修复 Postgres 18 兼容性问题
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:18.3-alpine3.23
|
image: postgres:18.3-alpine3.23
|
||||||
|
container_name: trade-postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: trade
|
POSTGRES_USER: trade
|
||||||
POSTGRES_PASSWORD: trade
|
POSTGRES_PASSWORD: trade
|
||||||
POSTGRES_DB: futures
|
POSTGRES_DB: futures
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U trade -d futures"]
|
test: ["CMD-SHELL", "pg_isready -U trade -d futures"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
@@ -15,6 +16,7 @@ services:
|
|||||||
|
|
||||||
tushare:
|
tushare:
|
||||||
build: ./tushare
|
build: ./tushare
|
||||||
|
container_name: trade-tushare
|
||||||
env_file: ./tushare/.env
|
env_file: ./tushare/.env
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=postgresql://trade:trade@postgres:5432/futures
|
- DATABASE_URL=postgresql://trade:trade@postgres:5432/futures
|
||||||
@@ -29,6 +31,7 @@ services:
|
|||||||
build:
|
build:
|
||||||
context: ./web
|
context: ./web
|
||||||
dockerfile: backend/Dockerfile
|
dockerfile: backend/Dockerfile
|
||||||
|
container_name: trade-web
|
||||||
env_file: ./web/backend/.env
|
env_file: ./web/backend/.env
|
||||||
environment:
|
environment:
|
||||||
- LISTEN_ADDR=:8080
|
- LISTEN_ADDR=:8080
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 运行时依赖 + 时区 + libpq(psycopg2)
|
# 时区(psycopg[binary] wheel 自带 libpq,不再需要系统装 libpq)
|
||||||
RUN apk add --no-cache tzdata libpq \
|
RUN apk add --no-cache tzdata \
|
||||||
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||||
&& echo "Asia/Shanghai" > /etc/timezone
|
&& echo "Asia/Shanghai" > /etc/timezone
|
||||||
|
|
||||||
|
|||||||
@@ -3,4 +3,4 @@ pandas>=2.2.0
|
|||||||
requests>=2.31.0
|
requests>=2.31.0
|
||||||
fastapi>=0.115.0
|
fastapi>=0.115.0
|
||||||
uvicorn[standard]>=0.34.0
|
uvicorn[standard]>=0.34.0
|
||||||
psycopg2-binary>=2.9.10
|
psycopg[binary]>=3.2.0
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import os
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import psycopg2
|
import psycopg
|
||||||
from psycopg2.extras import RealDictCursor
|
from psycopg.rows import dict_row
|
||||||
|
|
||||||
from .models import ScoreResult
|
from .models import ScoreResult
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ DEFAULT_DB_URL = os.environ.get("DATABASE_URL", "postgresql://trade:trade@postgr
|
|||||||
|
|
||||||
|
|
||||||
def _get_conn(db_url: str = DEFAULT_DB_URL):
|
def _get_conn(db_url: str = DEFAULT_DB_URL):
|
||||||
return psycopg2.connect(db_url)
|
return psycopg.connect(db_url)
|
||||||
|
|
||||||
|
|
||||||
def init_db(db_url: str = DEFAULT_DB_URL):
|
def init_db(db_url: str = DEFAULT_DB_URL):
|
||||||
@@ -38,7 +38,7 @@ def init_db(db_url: str = DEFAULT_DB_URL):
|
|||||||
""")
|
""")
|
||||||
cur.execute("""
|
cur.execute("""
|
||||||
CREATE TABLE IF NOT EXISTS scores (
|
CREATE TABLE IF NOT EXISTS scores (
|
||||||
id UUID DEFAULT gen_random_uuid_v7() PRIMARY KEY,
|
id UUID DEFAULT uuidv7() PRIMARY KEY,
|
||||||
ts_code TEXT NOT NULL,
|
ts_code TEXT NOT NULL,
|
||||||
trade_date TEXT NOT NULL,
|
trade_date TEXT NOT NULL,
|
||||||
close REAL,
|
close REAL,
|
||||||
@@ -143,7 +143,7 @@ def get_latest_score(ts_code: str, db_url: str = DEFAULT_DB_URL) -> Optional[dic
|
|||||||
"""查询最新打分记录。"""
|
"""查询最新打分记录。"""
|
||||||
conn = _get_conn(db_url)
|
conn = _get_conn(db_url)
|
||||||
try:
|
try:
|
||||||
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
with conn.cursor(row_factory=dict_row) as cur:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"SELECT * FROM scores WHERE ts_code = %s ORDER BY trade_date DESC LIMIT 1",
|
"SELECT * FROM scores WHERE ts_code = %s ORDER BY trade_date DESC LIMIT 1",
|
||||||
(ts_code,),
|
(ts_code,),
|
||||||
|
|||||||
@@ -55,23 +55,25 @@ func (s *FuturesStore) ListScores(f ScoreFilter) ([]Score, error) {
|
|||||||
q := `SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term, long_term,
|
q := `SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term, long_term,
|
||||||
composite, signal, created_at FROM scores WHERE 1=1`
|
composite, signal, created_at FROM scores WHERE 1=1`
|
||||||
args := []any{}
|
args := []any{}
|
||||||
|
n := 0
|
||||||
|
next := func() string { n++; return fmt.Sprintf("$%d", n) }
|
||||||
if f.TsCode != "" {
|
if f.TsCode != "" {
|
||||||
q += " AND ts_code = ?"
|
q += " AND ts_code = " + next()
|
||||||
args = append(args, f.TsCode)
|
args = append(args, f.TsCode)
|
||||||
}
|
}
|
||||||
if f.Start != "" {
|
if f.Start != "" {
|
||||||
q += " AND trade_date >= ?"
|
q += " AND trade_date >= " + next()
|
||||||
args = append(args, f.Start)
|
args = append(args, f.Start)
|
||||||
}
|
}
|
||||||
if f.End != "" {
|
if f.End != "" {
|
||||||
q += " AND trade_date <= ?"
|
q += " AND trade_date <= " + next()
|
||||||
args = append(args, f.End)
|
args = append(args, f.End)
|
||||||
}
|
}
|
||||||
q += " ORDER BY trade_date DESC, id DESC"
|
q += " ORDER BY trade_date DESC, id DESC"
|
||||||
if f.Limit <= 0 || f.Limit > 500 {
|
if f.Limit <= 0 || f.Limit > 500 {
|
||||||
f.Limit = 200
|
f.Limit = 200
|
||||||
}
|
}
|
||||||
q += " LIMIT ?"
|
q += " LIMIT " + next()
|
||||||
args = append(args, f.Limit)
|
args = append(args, f.Limit)
|
||||||
|
|
||||||
rows, err := s.db.Query(q, args...)
|
rows, err := s.db.Query(q, args...)
|
||||||
@@ -93,7 +95,7 @@ func (s *FuturesStore) ListScores(f ScoreFilter) ([]Score, error) {
|
|||||||
|
|
||||||
func (s *FuturesStore) GetScore(id string) (*Score, error) {
|
func (s *FuturesStore) GetScore(id string) (*Score, error) {
|
||||||
row := s.db.QueryRow(`SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term,
|
row := s.db.QueryRow(`SELECT id, ts_code, trade_date, close, oi, oi_chg, short_term, medium_term,
|
||||||
long_term, composite, signal, detail_json, created_at FROM scores WHERE id = ?`, id)
|
long_term, composite, signal, detail_json, created_at FROM scores WHERE id = $1`, id)
|
||||||
var x Score
|
var x Score
|
||||||
var detail sql.NullString
|
var detail sql.NullString
|
||||||
if err := row.Scan(&x.ID, &x.TsCode, &x.TradeDate, &x.Close, &x.OI, &x.OIChg,
|
if err := row.Scan(&x.ID, &x.TsCode, &x.TradeDate, &x.Close, &x.OI, &x.OIChg,
|
||||||
@@ -145,14 +147,16 @@ func (s *FuturesStore) ListCandles(tsCode, start, end string) ([]Candle, error)
|
|||||||
COALESCE(open, 0), COALESCE(high, 0), COALESCE(low, 0), COALESCE(close, 0),
|
COALESCE(open, 0), COALESCE(high, 0), COALESCE(low, 0), COALESCE(close, 0),
|
||||||
COALESCE(vol, 0), COALESCE(amount, 0),
|
COALESCE(vol, 0), COALESCE(amount, 0),
|
||||||
COALESCE(oi, 0), COALESCE(oi_chg, 0), COALESCE(pre_close, 0)
|
COALESCE(oi, 0), COALESCE(oi_chg, 0), COALESCE(pre_close, 0)
|
||||||
FROM candles WHERE ts_code = ?`
|
FROM candles WHERE ts_code = $1`
|
||||||
args := []any{tsCode}
|
args := []any{tsCode}
|
||||||
|
n := 1
|
||||||
|
next := func() string { n++; return fmt.Sprintf("$%d", n) }
|
||||||
if start != "" {
|
if start != "" {
|
||||||
q += " AND trade_date >= ?"
|
q += " AND trade_date >= " + next()
|
||||||
args = append(args, start)
|
args = append(args, start)
|
||||||
}
|
}
|
||||||
if end != "" {
|
if end != "" {
|
||||||
q += " AND trade_date <= ?"
|
q += " AND trade_date <= " + next()
|
||||||
args = append(args, end)
|
args = append(args, end)
|
||||||
}
|
}
|
||||||
q += " ORDER BY trade_date ASC LIMIT 1000"
|
q += " ORDER BY trade_date ASC LIMIT 1000"
|
||||||
|
|||||||
Reference in New Issue
Block a user