Add account-aware execution constraints

This commit is contained in:
root
2026-04-02 01:26:10 +08:00
parent fbe69ab70e
commit 5d2482c9ba
3 changed files with 514 additions and 141 deletions

View File

@@ -48,15 +48,24 @@ def _table_columns(conn: sqlite3.Connection, table: str) -> list[str]:
return []
def _ensure_column(conn: sqlite3.Connection, table: str, column: str, ddl: str) -> None:
columns = _table_columns(conn, table)
if columns and column not in columns:
conn.execute(f"ALTER TABLE {table} ADD COLUMN {ddl}")
def _migrate_schema(conn: sqlite3.Connection) -> None:
positions_cols = _table_columns(conn, "positions")
if positions_cols and "watchlist_id" not in positions_cols:
conn.execute("DROP TABLE positions")
positions_cols = []
if positions_cols:
_ensure_column(conn, "positions", "account_id", "account_id INTEGER")
def init_db() -> None:
with get_connection() as conn:
_migrate_schema(conn)
conn.executescript(
"""
CREATE TABLE IF NOT EXISTS watchlist (
@@ -83,6 +92,31 @@ def init_db() -> None:
CREATE INDEX IF NOT EXISTS idx_watchlist_market ON watchlist (market, code);
CREATE INDEX IF NOT EXISTS idx_watchlist_is_watched ON watchlist (is_watched, updated_at DESC);
CREATE TABLE IF NOT EXISTS accounts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
market TEXT,
currency TEXT,
cash_balance REAL NOT NULL DEFAULT 0,
available_cash REAL,
note TEXT DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_accounts_market_currency
ON accounts (market, currency);
CREATE TABLE IF NOT EXISTS stock_rules (
code TEXT PRIMARY KEY,
lot_size INTEGER,
tick_size REAL,
allows_odd_lot INTEGER NOT NULL DEFAULT 0,
source TEXT DEFAULT 'manual',
updated_at TEXT NOT NULL,
FOREIGN KEY (code) REFERENCES watchlist(code) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS kline_daily (
code TEXT NOT NULL,
trade_date TEXT NOT NULL,
@@ -102,13 +136,15 @@ def init_db() -> None:
CREATE TABLE IF NOT EXISTS positions (
watchlist_id INTEGER PRIMARY KEY,
account_id INTEGER,
buy_price REAL NOT NULL,
shares INTEGER NOT NULL,
buy_date TEXT,
note TEXT DEFAULT '',
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (watchlist_id) REFERENCES watchlist(id) ON DELETE CASCADE
FOREIGN KEY (watchlist_id) REFERENCES watchlist(id) ON DELETE CASCADE,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE SET NULL
);
CREATE TABLE IF NOT EXISTS analysis_cache (
@@ -142,6 +178,7 @@ def init_db() -> None:
ON aux_cache (code, category, created_at DESC);
"""
)
_migrate_schema(conn)
conn.commit()
@@ -408,6 +445,107 @@ def set_watch_status(code: str, watched: bool) -> dict | None:
return dict(row) if row else None
def list_accounts() -> list[dict]:
init_db()
with get_connection() as conn:
rows = conn.execute(
"""
SELECT id, name, market, currency, cash_balance, available_cash, note, created_at, updated_at
FROM accounts
ORDER BY market IS NULL, market, currency IS NULL, currency, name
"""
).fetchall()
return [dict(row) for row in rows]
def get_account(identifier: int | str) -> dict | None:
init_db()
with get_connection() as conn:
if isinstance(identifier, int) or (isinstance(identifier, str) and identifier.isdigit()):
row = conn.execute("SELECT * FROM accounts WHERE id = ?", (int(identifier),)).fetchone()
else:
row = conn.execute("SELECT * FROM accounts WHERE name = ?", (identifier,)).fetchone()
return dict(row) if row else None
def upsert_account(
*,
name: str,
market: str | None = None,
currency: str | None = None,
cash_balance: float | None = None,
available_cash: float | None = None,
note: str = "",
) -> dict:
init_db()
now = _utc_now_iso()
with get_connection() as conn:
existing = conn.execute("SELECT * FROM accounts WHERE name = ?", (name,)).fetchone()
created_at = existing["created_at"] if existing else now
conn.execute(
"""
INSERT INTO accounts (name, market, currency, cash_balance, available_cash, note, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
market = COALESCE(excluded.market, accounts.market),
currency = COALESCE(excluded.currency, accounts.currency),
cash_balance = COALESCE(excluded.cash_balance, accounts.cash_balance),
available_cash = COALESCE(excluded.available_cash, accounts.available_cash),
note = CASE WHEN excluded.note = '' THEN accounts.note ELSE excluded.note END,
updated_at = excluded.updated_at
""",
(
name,
market,
currency,
0 if cash_balance is None else cash_balance,
available_cash,
note,
created_at,
now,
),
)
conn.commit()
row = conn.execute("SELECT * FROM accounts WHERE name = ?", (name,)).fetchone()
return dict(row)
def upsert_stock_rule(
*,
code: str,
lot_size: int | None = None,
tick_size: float | None = None,
allows_odd_lot: bool = False,
source: str = "manual",
) -> dict:
init_db()
now = _utc_now_iso()
with get_connection() as conn:
conn.execute(
"""
INSERT INTO stock_rules (code, lot_size, tick_size, allows_odd_lot, source, updated_at)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(code) DO UPDATE SET
lot_size = COALESCE(excluded.lot_size, stock_rules.lot_size),
tick_size = COALESCE(excluded.tick_size, stock_rules.tick_size),
allows_odd_lot = excluded.allows_odd_lot,
source = excluded.source,
updated_at = excluded.updated_at
""",
(code, lot_size, tick_size, int(allows_odd_lot), source, now),
)
conn.commit()
row = conn.execute("SELECT * FROM stock_rules WHERE code = ?", (code,)).fetchone()
return dict(row)
def get_stock_rule(code: str) -> dict | None:
init_db()
with get_connection() as conn:
row = conn.execute("SELECT * FROM stock_rules WHERE code = ?", (code,)).fetchone()
return dict(row) if row else None
def get_latest_kline_date(code: str, adj_type: str = "qfq") -> str | None:
init_db()
with get_connection() as conn:
@@ -504,6 +642,12 @@ def list_positions() -> list[dict]:
"""
SELECT
p.watchlist_id,
p.account_id,
a.name AS account_name,
a.market AS account_market,
a.currency AS account_currency,
a.cash_balance AS account_cash_balance,
a.available_cash AS account_available_cash,
w.code,
w.market,
w.name,
@@ -513,9 +657,15 @@ def list_positions() -> list[dict]:
p.buy_date,
p.note,
p.created_at AS added_at,
p.updated_at
p.updated_at,
sr.lot_size,
sr.tick_size,
sr.allows_odd_lot,
sr.source AS lot_rule_source
FROM positions p
JOIN watchlist w ON w.id = p.watchlist_id
LEFT JOIN accounts a ON a.id = p.account_id
LEFT JOIN stock_rules sr ON sr.code = w.code
ORDER BY w.code ASC
"""
).fetchall()
@@ -529,6 +679,12 @@ def get_position(code: str) -> dict | None:
"""
SELECT
p.watchlist_id,
p.account_id,
a.name AS account_name,
a.market AS account_market,
a.currency AS account_currency,
a.cash_balance AS account_cash_balance,
a.available_cash AS account_available_cash,
w.code,
w.market,
w.name,
@@ -538,9 +694,15 @@ def get_position(code: str) -> dict | None:
p.buy_date,
p.note,
p.created_at AS added_at,
p.updated_at
p.updated_at,
sr.lot_size,
sr.tick_size,
sr.allows_odd_lot,
sr.source AS lot_rule_source
FROM positions p
JOIN watchlist w ON w.id = p.watchlist_id
LEFT JOIN accounts a ON a.id = p.account_id
LEFT JOIN stock_rules sr ON sr.code = w.code
WHERE w.code = ?
""",
(code,),
@@ -557,6 +719,7 @@ def upsert_position(
shares: int,
buy_date: str | None,
note: str = "",
account_id: int | None = None,
name: str | None = None,
currency: str | None = None,
meta: dict | None = None,
@@ -574,21 +737,23 @@ def upsert_position(
now = _utc_now_iso()
with get_connection() as conn:
existing = conn.execute(
"SELECT created_at FROM positions WHERE watchlist_id = ?", (watch["id"],)
"SELECT created_at, account_id FROM positions WHERE watchlist_id = ?", (watch["id"],)
).fetchone()
created_at = existing["created_at"] if existing else now
account_id_value = account_id if account_id is not None else (existing["account_id"] if existing else None)
conn.execute(
"""
INSERT INTO positions (watchlist_id, buy_price, shares, buy_date, note, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)
INSERT INTO positions (watchlist_id, account_id, buy_price, shares, buy_date, note, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(watchlist_id) DO UPDATE SET
account_id = excluded.account_id,
buy_price = excluded.buy_price,
shares = excluded.shares,
buy_date = excluded.buy_date,
note = excluded.note,
updated_at = excluded.updated_at
""",
(watch["id"], buy_price, shares, buy_date, note, created_at, now),
(watch["id"], account_id_value, buy_price, shares, buy_date, note, created_at, now),
)
conn.commit()
return get_position(code)
@@ -605,7 +770,13 @@ def remove_position(code: str) -> bool:
return cur.rowcount > 0
def update_position_fields(code: str, price: float | None = None, shares: int | None = None, note: str | None = None) -> dict | None:
def update_position_fields(
code: str,
price: float | None = None,
shares: int | None = None,
note: str | None = None,
account_id: int | None = None,
) -> dict | None:
current = get_position(code)
if not current:
return None
@@ -618,6 +789,7 @@ def update_position_fields(code: str, price: float | None = None, shares: int |
shares=shares if shares is not None else current["shares"],
buy_date=current.get("buy_date"),
note=note if note is not None else current.get("note", ""),
account_id=account_id if account_id is not None else current.get("account_id"),
name=watch.get("name"),
currency=watch.get("currency"),
meta=json.loads(watch["meta_json"]) if watch.get("meta_json") else None,

View File

@@ -4,10 +4,13 @@
用法:
python3 portfolio_manager.py list
python3 portfolio_manager.py add <代码> --price <买入价> --shares <数量> [--date <日期>] [--note <备注>]
python3 portfolio_manager.py add <代码> --price <买入价> --shares <数量> [--date <日期>] [--note <备注>] [--account <账户名或ID>]
python3 portfolio_manager.py remove <代码>
python3 portfolio_manager.py update <代码> [--price <价格>] [--shares <数量>] [--note <备注>]
python3 portfolio_manager.py update <代码> [--price <价格>] [--shares <数量>] [--note <备注>] [--account <账户名或ID>]
python3 portfolio_manager.py analyze [--output <输出文件>]
python3 portfolio_manager.py account-list
python3 portfolio_manager.py account-upsert <账户名> [--market <市场>] [--currency <币种>] [--cash <总现金>] [--available-cash <可用现金>] [--note <备注>]
python3 portfolio_manager.py rule-set <代码> [--lot-size <每手股数>] [--tick-size <最小价位>] [--odd-lot]
python3 portfolio_manager.py watch-list
python3 portfolio_manager.py watch-add <代码>
python3 portfolio_manager.py watch-remove <代码>
@@ -20,19 +23,24 @@ import json
import argparse
import os
import time
from collections import defaultdict
from datetime import datetime
try:
from db import (
DB_PATH,
get_account,
get_watchlist_item,
init_db,
list_accounts as db_list_accounts,
list_positions as db_list_positions,
list_watchlist as db_list_watchlist,
remove_position as db_remove_position,
set_watch_status,
update_position_fields,
upsert_account,
upsert_position,
upsert_stock_rule,
upsert_watchlist_item,
)
from analyze_stock import fetch_tencent_quote, normalize_stock_code, analyze_stock
@@ -41,14 +49,18 @@ except ImportError:
sys.path.insert(0, script_dir)
from db import (
DB_PATH,
get_account,
get_watchlist_item,
init_db,
list_accounts as db_list_accounts,
list_positions as db_list_positions,
list_watchlist as db_list_watchlist,
remove_position as db_remove_position,
set_watch_status,
update_position_fields,
upsert_account,
upsert_position,
upsert_stock_rule,
upsert_watchlist_item,
)
from analyze_stock import fetch_tencent_quote, normalize_stock_code, analyze_stock
@@ -58,6 +70,15 @@ def normalize_code(code: str) -> str:
return normalize_stock_code(code)["code"]
def resolve_account(account_ref: str | None):
if not account_ref:
return None
account = get_account(account_ref)
if not account:
raise ValueError(f"账户不存在: {account_ref}")
return account
def ensure_watch_item(code: str, watched: bool = False) -> dict:
stock = normalize_stock_code(code)
quote = fetch_tencent_quote(stock["code"])
@@ -81,6 +102,65 @@ def ensure_watch_item(code: str, watched: bool = False) -> dict:
)
def derive_execution_constraints(position: dict, current_price: float | None = None) -> dict:
shares = int(position.get("shares") or 0)
lot_size = position.get("lot_size")
allows_odd_lot = bool(position.get("allows_odd_lot") or False)
if lot_size is None or lot_size <= 0:
whole_lots = None
remainder = None
can_partial_sell = None
sellable_min_unit = 1 if allows_odd_lot else None
else:
whole_lots = shares // lot_size
remainder = shares % lot_size
can_partial_sell = allows_odd_lot or whole_lots >= 2 or remainder > 0
sellable_min_unit = 1 if allows_odd_lot else lot_size
estimated_cash_if_sell_all = round(shares * current_price, 2) if current_price is not None else None
return {
"lot_size": lot_size,
"allows_odd_lot": allows_odd_lot,
"sellable_min_unit": sellable_min_unit,
"whole_lots": whole_lots,
"odd_lot_remainder": remainder,
"can_partial_sell": can_partial_sell,
"estimated_cash_if_sell_all": estimated_cash_if_sell_all,
}
def derive_position_snapshot(position: dict, analysis: dict) -> dict:
current_price = analysis.get("current_price")
buy_price = position.get("buy_price")
shares = int(position.get("shares") or 0)
cost = round((buy_price or 0) * shares, 2)
market_value = round((current_price or 0) * shares, 2) if current_price is not None else None
pnl = round((current_price - buy_price) * shares, 2) if current_price is not None and buy_price is not None else None
pnl_pct = round((current_price - buy_price) / buy_price * 100, 2) if current_price is not None and buy_price not in (None, 0) else None
execution = derive_execution_constraints(position, current_price)
return {
"buy_price": buy_price,
"shares": shares,
"buy_date": position.get("buy_date"),
"cost": cost,
"market_value": market_value,
"pnl": pnl,
"pnl_pct": pnl_pct,
"note": position.get("note", ""),
"currency": position.get("currency"),
"market": position.get("market"),
"account": {
"id": position.get("account_id"),
"name": position.get("account_name"),
"market": position.get("account_market"),
"currency": position.get("account_currency"),
"cash_balance": position.get("account_cash_balance"),
"available_cash": position.get("account_available_cash"),
},
"execution_constraints": execution,
}
# ─────────────────────────────────────────────
# 持仓管理
# ─────────────────────────────────────────────
@@ -99,7 +179,7 @@ def list_positions():
}, ensure_ascii=False, indent=2))
def add_position(code: str, price: float, shares: int, date: str = None, note: str = ""):
def add_position(code: str, price: float, shares: int, date: str = None, note: str = "", account_ref: str = None):
init_db()
normalized = normalize_stock_code(code)
existing = next((p for p in db_list_positions() if p["code"] == normalized["code"]), None)
@@ -107,6 +187,7 @@ def add_position(code: str, price: float, shares: int, date: str = None, note: s
print(json.dumps({"error": f"{normalized['code']} 已在持仓中,请使用 update 命令更新"}, ensure_ascii=False))
return
account = resolve_account(account_ref)
watch = ensure_watch_item(normalized["code"], watched=True)
position = upsert_position(
code=normalized["code"],
@@ -116,6 +197,7 @@ def add_position(code: str, price: float, shares: int, date: str = None, note: s
shares=shares,
buy_date=date or datetime.now().strftime("%Y-%m-%d"),
note=note,
account_id=account.get("id") if account else None,
name=watch.get("name"),
currency=watch.get("currency"),
meta=json.loads(watch["meta_json"]) if watch.get("meta_json") else None,
@@ -133,16 +215,53 @@ def remove_position(code: str):
print(json.dumps({"message": f"已移除 {normalized_code}"}, ensure_ascii=False, indent=2))
def update_position(code: str, price: float = None, shares: int = None, note: str = None):
def update_position(code: str, price: float = None, shares: int = None, note: str = None, account_ref: str = None):
init_db()
normalized_code = normalize_code(code)
position = update_position_fields(normalized_code, price=price, shares=shares, note=note)
account = resolve_account(account_ref) if account_ref else None
position = update_position_fields(normalized_code, price=price, shares=shares, note=note, account_id=account.get("id") if account else None)
if not position:
print(json.dumps({"error": f"{normalized_code} 不在持仓中"}, ensure_ascii=False))
return
print(json.dumps({"message": f"已更新 {normalized_code}", "position": position}, ensure_ascii=False, indent=2))
# ─────────────────────────────────────────────
# 账户与交易规则管理
# ─────────────────────────────────────────────
def list_accounts():
init_db()
accounts = db_list_accounts()
print(json.dumps({
"total_accounts": len(accounts),
"accounts": accounts,
"portfolio_db": str(DB_PATH),
"updated_at": datetime.now().isoformat(),
}, ensure_ascii=False, indent=2))
def save_account(name: str, market: str = None, currency: str = None, cash: float = None, available_cash: float = None, note: str = ""):
init_db()
account = upsert_account(
name=name,
market=market,
currency=currency,
cash_balance=cash,
available_cash=available_cash,
note=note,
)
print(json.dumps({"message": f"已保存账户 {name}", "account": account}, ensure_ascii=False, indent=2))
def set_rule(code: str, lot_size: int = None, tick_size: float = None, odd_lot: bool = False):
init_db()
normalized_code = normalize_code(code)
ensure_watch_item(normalized_code, watched=False)
rule = upsert_stock_rule(code=normalized_code, lot_size=lot_size, tick_size=tick_size, allows_odd_lot=odd_lot)
print(json.dumps({"message": f"已设置 {normalized_code} 的交易规则", "rule": rule}, ensure_ascii=False, indent=2))
# ─────────────────────────────────────────────
# 关注池管理
# ─────────────────────────────────────────────
@@ -186,40 +305,44 @@ def analyze_portfolio(output_file: str = None):
return
results = []
account_totals = defaultdict(lambda: {"cost": 0.0, "market_value": 0.0})
market_currency_totals = defaultdict(lambda: {"cost": 0.0, "market_value": 0.0})
for i, pos in enumerate(positions):
code = pos["code"]
print(f"正在分析 {code} ({i+1}/{len(positions)})...", file=sys.stderr)
analysis = analyze_stock(code)
if analysis.get("current_price") and pos.get("buy_price"):
current = analysis["current_price"]
buy = pos["buy_price"]
shares = pos.get("shares", 0)
pnl = (current - buy) * shares
pnl_pct = (current - buy) / buy * 100
analysis["portfolio_info"] = {
"buy_price": buy,
"shares": shares,
"buy_date": pos.get("buy_date"),
"cost": round(buy * shares, 2),
"market_value": round(current * shares, 2),
"pnl": round(pnl, 2),
"pnl_pct": round(pnl_pct, 2),
"note": pos.get("note", ""),
"currency": pos.get("currency"),
"market": pos.get("market"),
}
analysis["portfolio_info"] = derive_position_snapshot(pos, analysis)
results.append(analysis)
snapshot = analysis["portfolio_info"]
market_value = snapshot.get("market_value") or 0.0
cost = snapshot.get("cost") or 0.0
account_name = snapshot.get("account", {}).get("name") or "未分配账户"
account_totals[account_name]["cost"] += cost
account_totals[account_name]["market_value"] += market_value
mc_key = f"{snapshot.get('market') or 'UNKNOWN'}:{snapshot.get('currency') or 'UNKNOWN'}"
market_currency_totals[mc_key]["cost"] += cost
market_currency_totals[mc_key]["market_value"] += market_value
if i < len(positions) - 1 and not analysis.get("_from_cache"):
time.sleep(2)
total_cost = sum(r.get("portfolio_info", {}).get("cost", 0) for r in results)
total_value = sum(r.get("portfolio_info", {}).get("market_value", 0) for r in results)
total_cost = sum(r.get("portfolio_info", {}).get("cost", 0) or 0 for r in results)
total_value = sum(r.get("portfolio_info", {}).get("market_value", 0) or 0 for r in results)
total_pnl = total_value - total_cost
for analysis in results:
snapshot = analysis.get("portfolio_info", {})
market_value = snapshot.get("market_value") or 0.0
account = snapshot.get("account") or {}
account_name = account.get("name") or "未分配账户"
account_total_value = account_totals[account_name]["market_value"]
snapshot["position_weight_of_portfolio_pct"] = round(market_value / total_value * 100, 2) if total_value > 0 else None
snapshot["position_weight_of_account_pct"] = round(market_value / account_total_value * 100, 2) if account_total_value > 0 else None
summary = {
"analysis_time": datetime.now().isoformat(),
"total_positions": len(results),
@@ -227,6 +350,24 @@ def analyze_portfolio(output_file: str = None):
"total_market_value": round(total_value, 2),
"total_pnl": round(total_pnl, 2),
"total_pnl_pct": round(total_pnl / total_cost * 100, 2) if total_cost > 0 else 0,
"accounts": [
{
"name": name,
"total_cost": round(v["cost"], 2),
"total_market_value": round(v["market_value"], 2),
"total_pnl": round(v["market_value"] - v["cost"], 2),
}
for name, v in sorted(account_totals.items())
],
"market_currency_breakdown": [
{
"market_currency": key,
"total_cost": round(v["cost"], 2),
"total_market_value": round(v["market_value"], 2),
"total_pnl": round(v["market_value"] - v["cost"], 2),
}
for key, v in sorted(market_currency_totals.items())
],
"positions": results,
}
@@ -245,6 +386,7 @@ def main():
subparsers = parser.add_subparsers(dest="command", help="子命令")
subparsers.add_parser("list", help="列出所有持仓")
subparsers.add_parser("account-list", help="列出账户")
add_parser = subparsers.add_parser("add", help="添加持仓")
add_parser.add_argument("code", help="股票代码")
@@ -252,6 +394,7 @@ def main():
add_parser.add_argument("--shares", type=int, required=True, help="持有数量")
add_parser.add_argument("--date", help="买入日期 (YYYY-MM-DD)")
add_parser.add_argument("--note", default="", help="备注")
add_parser.add_argument("--account", help="账户名或账户ID")
rm_parser = subparsers.add_parser("remove", help="移除持仓")
rm_parser.add_argument("code", help="股票代码")
@@ -261,11 +404,26 @@ def main():
up_parser.add_argument("--price", type=float, help="买入价格")
up_parser.add_argument("--shares", type=int, help="持有数量")
up_parser.add_argument("--note", help="备注")
up_parser.add_argument("--account", help="账户名或账户ID")
analyze_parser = subparsers.add_parser("analyze", help="批量分析持仓")
analyze_parser.add_argument("--output", help="输出JSON文件")
watch_list_parser = subparsers.add_parser("watch-list", help="列出关注池")
account_parser = subparsers.add_parser("account-upsert", help="新增或更新账户")
account_parser.add_argument("name", help="账户名")
account_parser.add_argument("--market", help="市场,例如 HK/CN/US")
account_parser.add_argument("--currency", help="币种,例如 HKD/CNY/USD")
account_parser.add_argument("--cash", type=float, help="账户总现金")
account_parser.add_argument("--available-cash", type=float, help="账户可用现金")
account_parser.add_argument("--note", default="", help="备注")
rule_parser = subparsers.add_parser("rule-set", help="设置股票交易规则")
rule_parser.add_argument("code", help="股票代码")
rule_parser.add_argument("--lot-size", type=int, help="一手股数")
rule_parser.add_argument("--tick-size", type=float, help="最小价位变动")
rule_parser.add_argument("--odd-lot", action="store_true", help="允许碎股")
subparsers.add_parser("watch-list", help="列出关注池")
watch_add_parser = subparsers.add_parser("watch-add", help="添加关注股票")
watch_add_parser.add_argument("code", help="股票代码")
watch_remove_parser = subparsers.add_parser("watch-remove", help="取消关注股票")
@@ -273,24 +431,34 @@ def main():
args = parser.parse_args()
if args.command == "list":
list_positions()
elif args.command == "add":
add_position(args.code, args.price, args.shares, args.date, args.note)
elif args.command == "remove":
remove_position(args.code)
elif args.command == "update":
update_position(args.code, args.price, args.shares, args.note)
elif args.command == "analyze":
analyze_portfolio(args.output)
elif args.command == "watch-list":
list_watchlist()
elif args.command == "watch-add":
add_watch(args.code)
elif args.command == "watch-remove":
remove_watch(args.code)
else:
parser.print_help()
try:
if args.command == "list":
list_positions()
elif args.command == "add":
add_position(args.code, args.price, args.shares, args.date, args.note, args.account)
elif args.command == "remove":
remove_position(args.code)
elif args.command == "update":
update_position(args.code, args.price, args.shares, args.note, args.account)
elif args.command == "analyze":
analyze_portfolio(args.output)
elif args.command == "account-list":
list_accounts()
elif args.command == "account-upsert":
save_account(args.name, args.market, args.currency, args.cash, args.available_cash, args.note)
elif args.command == "rule-set":
set_rule(args.code, args.lot_size, args.tick_size, args.odd_lot)
elif args.command == "watch-list":
list_watchlist()
elif args.command == "watch-add":
add_watch(args.code)
elif args.command == "watch-remove":
remove_watch(args.code)
else:
parser.print_help()
except ValueError as e:
print(json.dumps({"error": str(e)}, ensure_ascii=False, indent=2))
sys.exit(1)
if __name__ == "__main__":