Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const step2Schema = z
exchange_id: z.string(),
api_key: z.string(),
secret_key: z.string(),
passphrase: z.string(), // Required string, but can be empty for non-OKX exchanges
})
.superRefine((data, ctx) => {
// Only validate exchange credentials when live trading is selected
Expand Down Expand Up @@ -86,6 +87,15 @@ const step2Schema = z
});
}
}

// OKX requires passphrase
if (data.exchange_id === "okx" && !data.passphrase?.trim()) {
ctx.addIssue({
code: "custom",
message: "Password is required for OKX",
path: ["passphrase"],
});
}
}
// Virtual trading mode: no validation needed for exchange fields
});
Expand Down Expand Up @@ -214,6 +224,7 @@ const CreateStrategyModal: FC<CreateStrategyModalProps> = ({ children }) => {
exchange_id: "okx",
api_key: "",
secret_key: "",
passphrase: "",
},
validators: {
onSubmit: step2Schema,
Expand Down Expand Up @@ -543,6 +554,37 @@ const CreateStrategyModal: FC<CreateStrategyModalProps> = ({ children }) => {
</Field>
)}
</form2.Field>

{/* Password field - only shown for OKX */}
<form2.Field name="exchange_id">
{(exchangeField) =>
exchangeField.state.value === "okx" ? (
<form2.Field name="passphrase">
{(field) => (
<Field>
<FieldLabel className="font-medium text-base text-gray-950">
Password
</FieldLabel>
<Input
value={field.state.value}
onChange={(e) =>
field.handleChange(e.target.value)
}
onBlur={field.handleBlur}
placeholder="Enter Password (Required for OKX)"
/>
{field.state.meta.errors.length >
0 && (
<FieldError
errors={field.state.meta.errors}
/>
)}
</Field>
)}
</form2.Field>
) : null
}
</form2.Field>
</>
)}
</>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/types/strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface CreateStrategyRequest {
trading_mode: "live" | "virtual";
api_key?: string;
secret_key?: string;
passphrase?: string; // Required for some exchanges like OKX
};

// Trading Strategy Configuration
Expand Down
4 changes: 2 additions & 2 deletions python/valuecell/agents/strategy_agent/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
StrategyStatusContent,
UserRequest,
)
from .runtime import create_strategy_runtime
from .runtime import create_strategy_runtime_async


class StrategyAgent(BaseAgent):
Expand Down Expand Up @@ -122,7 +122,7 @@ async def stream(
yield streaming.done()
return

runtime = create_strategy_runtime(request)
runtime = await create_strategy_runtime_async(request)
strategy_id = runtime.strategy_id
logger.info(
"Created runtime for strategy_id={} conversation={} task={}",
Expand Down
29 changes: 29 additions & 0 deletions python/valuecell/agents/strategy_agent/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from datetime import datetime, timezone
from typing import Callable, Dict, List

from loguru import logger

from valuecell.utils.uuid import generate_uuid

from .data.interfaces import MarketDataSource
Expand All @@ -24,6 +26,7 @@
TradeSide,
TradeType,
TxResult,
TxStatus,
UserRequest,
)
from .portfolio.interfaces import PortfolioService
Expand Down Expand Up @@ -148,10 +151,27 @@ async def run_once(self) -> DecisionCycleResult:
)

instructions = await self._composer.compose(context)
logger.info(f"🔍 Composer returned {len(instructions)} instructions")
for idx, inst in enumerate(instructions):
logger.info(
f" 📝 Instruction {idx}: {inst.instrument.symbol} {inst.side.value} qty={inst.quantity}"
)

# Execute instructions via async gateway to obtain execution results
logger.info(
f"🚀 Calling execution_gateway.execute() with {len(instructions)} instructions"
)
logger.info(
f" ExecutionGateway type: {type(self._execution_gateway).__name__}"
)
tx_results = await self._execution_gateway.execute(
instructions, market_snapshot
)
logger.info(f"✅ ExecutionGateway returned {len(tx_results)} results")
for idx, tx in enumerate(tx_results):
logger.info(
f" 📊 TxResult {idx}: {tx.instrument.symbol} status={tx.status.value} filled_qty={tx.filled_qty}"
)

trades = self._create_trades(tx_results, compose_id, timestamp_ms)
self._portfolio_service.apply_trades(trades, market_snapshot)
Expand Down Expand Up @@ -197,7 +217,16 @@ def _create_trades(
pre_view = None

for tx in tx_results:
# Skip failed or rejected trades - only create history entries for successful fills
# (including partial fills which may still have filled_qty > 0)
if tx.status in (TxStatus.ERROR, TxStatus.REJECTED):
continue

qty = float(tx.filled_qty or 0.0)
# Skip trades with zero filled quantity
if qty == 0:
continue

price = float(tx.avg_exec_price or 0.0)
notional = (price * qty) if price and qty else None
# Immediate realized effect: fees are costs (negative PnL). Slippage already baked into exec price.
Expand Down
34 changes: 28 additions & 6 deletions python/valuecell/agents/strategy_agent/decision/composer.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ def _normalize_quantity(
qty = quantity

# Step 1: per-order filters (step size, min notional, max order qty)
logger.debug(f"_normalize_quantity Step 1: {symbol} qty={qty} before filters")
qty = self._apply_quantity_filters(
symbol,
qty,
Expand All @@ -203,13 +204,11 @@ def _normalize_quantity(
constraints.min_notional,
price_map,
)
logger.debug(f"_normalize_quantity Step 1: {symbol} qty={qty} after filters")

if qty <= self._quantity_precision:
logger.debug(
"Post-filter quantity for {} is {} <= precision {} -> skipping",
symbol,
qty,
self._quantity_precision,
logger.warning(
f"Post-filter quantity for {symbol} is {qty} <= precision {self._quantity_precision} -> returning 0"
)
return 0.0, 0.0

Expand Down Expand Up @@ -402,6 +401,7 @@ def _count_active(pos_map: Dict[str, float]) -> int:
quantity = abs(delta)

# Normalize quantity through all guardrails
logger.debug(f"Before normalize: {symbol} quantity={quantity}")
quantity, consumed_bp = self._normalize_quantity(
symbol,
quantity,
Expand All @@ -413,8 +413,14 @@ def _count_active(pos_map: Dict[str, float]) -> int:
projected_gross,
price_map,
)
logger.debug(
f"After normalize: {symbol} quantity={quantity}, consumed_bp={consumed_bp}"
)

if quantity <= self._quantity_precision:
logger.warning(
f"SKIPPED: {symbol} quantity={quantity} <= precision={self._quantity_precision} after normalization"
)
continue

# Update projected positions for subsequent guardrails
Expand Down Expand Up @@ -529,24 +535,40 @@ def _apply_quantity_filters(
market_snapshot: Dict[str, float],
) -> float:
qty = quantity
logger.debug(f"Filtering {symbol}: initial qty={qty}")

if max_order_qty is not None:
qty = min(qty, float(max_order_qty))
logger.debug(f"After max_order_qty filter: qty={qty}")

if quantity_step > 0:
qty = math.floor(qty / quantity_step) * quantity_step
logger.debug(f"After quantity_step filter: qty={qty}")

if qty <= 0:
logger.warning(f"FILTERED: {symbol} qty={qty} <= 0")
return 0.0

if qty < min_trade_qty:
logger.warning(
f"FILTERED: {symbol} qty={qty} < min_trade_qty={min_trade_qty}"
)
return 0.0

if min_notional is not None:
price = market_snapshot.get(symbol)
if price is None:
logger.warning(f"FILTERED: {symbol} no price in market_snapshot")
return 0.0
if qty * price < float(min_notional):
notional = qty * price
if notional < float(min_notional):
logger.warning(
f"FILTERED: {symbol} notional={notional:.4f} < min_notional={min_notional}"
)
return 0.0
logger.debug(
f"Passed min_notional check: notional={notional:.4f} >= {min_notional}"
)

logger.debug(f"Final qty for {symbol}: {qty}")
return qty
15 changes: 15 additions & 0 deletions python/valuecell/agents/strategy_agent/execution/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Execution adapters for trading instructions."""

from .ccxt_trading import CCXTExecutionGateway, create_ccxt_gateway
from .factory import create_execution_gateway, create_execution_gateway_sync
from .interfaces import ExecutionGateway
from .paper_trading import PaperExecutionGateway

__all__ = [
"ExecutionGateway",
"PaperExecutionGateway",
"CCXTExecutionGateway",
"create_ccxt_gateway",
"create_execution_gateway",
"create_execution_gateway_sync",
]
Loading