ZhuLinsen/daily_stock_analysis · error · SystemConfigConflictError

配置版本冲突

Error message

配置版本冲突

What it means

Raised during full account replay (portfolio_service.py:883) when a trade event's side is neither 'buy' nor 'sell' (compared after lower().strip()). The account-level replay (cash balances, FIFO/avg cost) cannot classify the event, so the snapshot fails with validation_error.

Source

Thrown at apps/dsa-web/src/api/systemConfig.ts:329

      return toCamelCase<UpdateSystemConfigResponse>(response.data);
    } catch (error: unknown) {
      const parsed = getParsedApiError(error);
      if (error && typeof error === 'object' && 'response' in error) {
        const status = (error as { response?: { status?: number } }).response?.status;
        const payloadData = (error as { response?: { data?: unknown } }).response?.data;

        if (status === 400) {
          const validationError = toCamelCase<SystemConfigValidationErrorResponse>(payloadData ?? {});
          throw new SystemConfigValidationError(
            parsed.message || validationError.message || '配置校验失败',
            validationError.issues || [],
            parsed,
          );
        }

        if (status === 409) {
          const conflict = toCamelCase<SystemConfigConflictResponse>(payloadData ?? {});
          throw new SystemConfigConflictError(
            parsed.message || conflict.message || '配置版本冲突',
            conflict.currentConfigVersion,
            parsed,
          );
        }
      }

      throw error;
    }
  },

  /**
   * 获取自选队列股票代码列表
   */
  getWatchlist: async (): Promise<string[]> => {
    const response = await apiClient.get<Record<string, unknown>>('/api/v1/stocks/watchlist');
    const data = toCamelCase<{ stockCodes: string[] }>(response.data);
    return data.stockCodes || [];

View on GitHub (pinned to 5159bd72e8)

Solutions

  1. Audit trades for the account: SELECT ... WHERE side IS NULL OR LOWER(TRIM(side)) NOT IN ('buy','sell') and repair
  2. Map legacy values (B/S, long/short) to buy/sell in a migration script
  3. Insert future trades only via add_trade
  4. Add a DB CHECK constraint on side to stop drift at the storage layer

Example fix

# before
raw_row.side = "long"
# after
svc.add_trade(account_id=1, symbol="AAPL", side="buy", quantity=10, price=185.50, ...)
Defensive patterns

Strategy: validation

Validate before calling

VALID_SIDES = {"buy", "sell"}

def stored_side_ok(side):
    return bool(side) and side.strip().lower() in VALID_SIDES

Type guard

from typing import Literal, TypeGuard

StoredSide = Literal["buy", "sell"]

def is_stored_side(value: str | None) -> TypeGuard[StoredSide]:
    return bool(value) and value.strip().lower() in {"buy", "sell"}

Try / catch

try:
    svc.get_snapshot(account_id=a)
except ValueError as exc:
    if "Unsupported trade side" in str(exc):
        normalize_sides_in_db(a); svc.get_snapshot(account_id=a)
    else:
        raise

Prevention

When it happens

Trigger: A trades row with side='LONG', side='B', side=NULL, or an unexpected enum reaching _replay_account. Same class of corruption as the quantity-replay variant at portfolio_service.py:763, but hit while building the full account snapshot including cash and cost basis.

Common situations: Rows written by external ETL or manual SQL that bypass add_trade's VALID_SIDES check; enum drift after migrating data from another portfolio tool; partially failed imports leaving rows with NULL side.

Related errors


AI-assisted analysis of ZhuLinsen/daily_stock_analysis@5159bd72e8 (2026-08-15). Data as JSON: /api/errors/4466059e94ba8486. Report an issue: GitHub.