feat: refactor fee management by removing deprecated pair fee handling and updating dashboard to display equity
This commit is contained in:
@@ -39,7 +39,6 @@ def create_app(settings: Settings) -> FastAPI:
|
|||||||
run_fee_sync_loop(
|
run_fee_sync_loop(
|
||||||
kraken_client,
|
kraken_client,
|
||||||
db,
|
db,
|
||||||
lambda: _active_pairings(app),
|
|
||||||
fee_sync_stop_event,
|
fee_sync_stop_event,
|
||||||
),
|
),
|
||||||
name="fee_sync_loop",
|
name="fee_sync_loop",
|
||||||
@@ -66,24 +65,6 @@ def create_app(settings: Settings) -> FastAPI:
|
|||||||
await kraken_client.close()
|
await kraken_client.close()
|
||||||
await graceful_shutdown(app)
|
await graceful_shutdown(app)
|
||||||
|
|
||||||
def _active_pairings(app: FastAPI) -> list[str]:
|
|
||||||
ctl = app.state.dashboard_controls
|
|
||||||
cfg_svc = app.state.configuration_service
|
|
||||||
pairs: list[str] = []
|
|
||||||
if ctl.tradable_pairs:
|
|
||||||
pairs = [p.upper() for p in ctl.tradable_pairs]
|
|
||||||
elif cfg_svc is not None:
|
|
||||||
try:
|
|
||||||
all_pairings = cfg_svc.list_pairings()
|
|
||||||
pairs = [
|
|
||||||
f"{p.base_asset}/{p.quote_asset}"
|
|
||||||
for p in all_pairings
|
|
||||||
if getattr(p, "enabled", True)
|
|
||||||
]
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
return pairs
|
|
||||||
|
|
||||||
app = FastAPI(title="arbitrade", version="0.1.0", lifespan=lifespan)
|
app = FastAPI(title="arbitrade", version="0.1.0", lifespan=lifespan)
|
||||||
app.state.settings = settings
|
app.state.settings = settings
|
||||||
app.state.store = db
|
app.state.store = db
|
||||||
|
|||||||
+39
-86
@@ -127,12 +127,49 @@ def _dashboard_overview(request: Request) -> dict[str, object]:
|
|||||||
|
|
||||||
balances_value = "—"
|
balances_value = "—"
|
||||||
total_value = "—"
|
total_value = "—"
|
||||||
|
equity_value = "—"
|
||||||
if portfolio_row is not None:
|
if portfolio_row is not None:
|
||||||
balances_raw, total_value_raw = portfolio_row
|
balances_raw, total_value_raw = portfolio_row
|
||||||
balances_value = str(balances_raw) if balances_raw is not None else "—"
|
if isinstance(balances_raw, str) and balances_raw:
|
||||||
|
try:
|
||||||
|
parsed = json.loads(balances_raw)
|
||||||
|
if isinstance(parsed, dict):
|
||||||
|
# Filter out zero balances, show non-zero as "AMT ASSET"
|
||||||
|
non_zero = {k: float(v)
|
||||||
|
for k, v in parsed.items() if float(v) > 0.0}
|
||||||
|
if non_zero:
|
||||||
|
balances_value = ", ".join(
|
||||||
|
f"{v:.6g} {k}" for k, v in sorted(non_zero.items())
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
balances_value = "No balances"
|
||||||
|
else:
|
||||||
|
balances_value = str(balances_raw)
|
||||||
|
except (json.JSONDecodeError, ValueError, TypeError):
|
||||||
|
balances_value = str(balances_raw)
|
||||||
|
elif balances_raw is not None:
|
||||||
|
balances_value = str(balances_raw)
|
||||||
if total_value_raw is not None:
|
if total_value_raw is not None:
|
||||||
total_value = f"{float(total_value_raw):.2f} USD"
|
total_value = f"{float(total_value_raw):.2f} USD"
|
||||||
|
|
||||||
|
# Query equity from kraken_account_snapshots
|
||||||
|
try:
|
||||||
|
equity_row = conn.execute("""
|
||||||
|
SELECT trade_balance_raw
|
||||||
|
FROM kraken_account_snapshots
|
||||||
|
ORDER BY snapshot_at DESC
|
||||||
|
LIMIT 1
|
||||||
|
""").fetchone()
|
||||||
|
if equity_row is not None and equity_row[0] is not None:
|
||||||
|
tb_raw = equity_row[0]
|
||||||
|
if isinstance(tb_raw, str):
|
||||||
|
tb_raw = json.loads(tb_raw)
|
||||||
|
if isinstance(tb_raw, dict):
|
||||||
|
eb = tb_raw.get("eb")
|
||||||
|
equity_value = f"{float(eb):.2f} USD" if eb is not None else "—"
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
open_trade_rows = [
|
open_trade_rows = [
|
||||||
{
|
{
|
||||||
"trade_ref": str(row[0]),
|
"trade_ref": str(row[0]),
|
||||||
@@ -177,6 +214,7 @@ def _dashboard_overview(request: Request) -> dict[str, object]:
|
|||||||
"generated_at": datetime.now(UTC).isoformat(),
|
"generated_at": datetime.now(UTC).isoformat(),
|
||||||
"balances": balances_value,
|
"balances": balances_value,
|
||||||
"total_value": total_value,
|
"total_value": total_value,
|
||||||
|
"equity": equity_value,
|
||||||
"open_trade_count": len(open_trade_rows),
|
"open_trade_count": len(open_trade_rows),
|
||||||
"open_trades": open_trade_rows,
|
"open_trades": open_trade_rows,
|
||||||
"realized_pnl_total": f"{float(rpnl[0]):.2f} USD" if rpnl else "—",
|
"realized_pnl_total": f"{float(rpnl[0]):.2f} USD" if rpnl else "—",
|
||||||
@@ -460,11 +498,6 @@ def _dashboard_config_context(request: Request) -> dict[str, object]:
|
|||||||
"strategy_stat_arb_max_holding_seconds": f"{rs.strategy_stat_arb_max_holding_seconds:.0f}",
|
"strategy_stat_arb_max_holding_seconds": f"{rs.strategy_stat_arb_max_holding_seconds:.0f}",
|
||||||
# UI
|
# UI
|
||||||
"config_endpoint": "/dashboard/control/config",
|
"config_endpoint": "/dashboard/control/config",
|
||||||
"config_fees_endpoint": "/dashboard/control/config/fees",
|
|
||||||
"config_fees_fragment": "/dashboard/fragment/config/fees",
|
|
||||||
# Fee data (for config page)
|
|
||||||
"pair_fees": [],
|
|
||||||
"pairings": [],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -850,8 +883,6 @@ async def dashboard_config_page(request: Request) -> HTMLResponse:
|
|||||||
"title": "Arbitrade Configuration",
|
"title": "Arbitrade Configuration",
|
||||||
"request": request,
|
"request": request,
|
||||||
"config_endpoint": "/dashboard/control/config",
|
"config_endpoint": "/dashboard/control/config",
|
||||||
"config_fees_endpoint": "/dashboard/control/config/fees",
|
|
||||||
"config_fees_fragment": "/dashboard/fragment/config/fees",
|
|
||||||
**_dashboard_config_context(request),
|
**_dashboard_config_context(request),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -866,84 +897,6 @@ async def dashboard_config_fragment(request: Request) -> HTMLResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/dashboard/fragment/config/fees", response_class=HTMLResponse)
|
|
||||||
async def dashboard_config_fees_fragment(request: Request) -> HTMLResponse:
|
|
||||||
svc = getattr(request.app.state, "configuration_service", None)
|
|
||||||
if svc is None:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request,
|
|
||||||
name="partials/config_fees.html",
|
|
||||||
context={"request": request, "pair_fees": [], "pairings": [],
|
|
||||||
"config_fees_endpoint": "/dashboard/control/config/fees",
|
|
||||||
"error": "Configuration service unavailable"},
|
|
||||||
)
|
|
||||||
pair_fees = svc.list_pair_fees_with_details()
|
|
||||||
pairings = svc.list_pairings()
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request,
|
|
||||||
name="partials/config_fees.html",
|
|
||||||
context={"request": request, "pair_fees": pair_fees,
|
|
||||||
"pairings": pairings,
|
|
||||||
"config_fees_endpoint": "/dashboard/control/config/fees",
|
|
||||||
"error": None},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/dashboard/control/config/fees", response_class=HTMLResponse)
|
|
||||||
async def dashboard_config_fees_control(request: Request) -> HTMLResponse:
|
|
||||||
svc = getattr(request.app.state, "configuration_service", None)
|
|
||||||
if svc is None:
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request,
|
|
||||||
name="partials/config_fees.html",
|
|
||||||
context={"request": request, "pair_fees": [], "pairings": [],
|
|
||||||
"error": "Configuration service unavailable"},
|
|
||||||
)
|
|
||||||
form = _parse_form_body(await request.body())
|
|
||||||
action = form.get("action", "")
|
|
||||||
try:
|
|
||||||
if action == "add" or action == "update":
|
|
||||||
base_asset = form.get("base_asset", "").strip().upper()
|
|
||||||
quote_asset = form.get("quote_asset", "").strip().upper()
|
|
||||||
market_type = form.get("market_type", "crypto_crypto").strip()
|
|
||||||
maker_rate = float(form.get("maker_fee_rate", 0))
|
|
||||||
taker_rate = float(form.get("taker_fee_rate", 0))
|
|
||||||
if not base_asset or not quote_asset:
|
|
||||||
raise ValueError("base_asset and quote_asset are required")
|
|
||||||
result = svc.upsert_pair_fee(
|
|
||||||
base_asset, quote_asset, market_type, maker_rate, taker_rate)
|
|
||||||
elif action == "delete":
|
|
||||||
base_asset = form.get("base_asset", "").strip().upper()
|
|
||||||
quote_asset = form.get("quote_asset", "").strip().upper()
|
|
||||||
market_type = form.get("market_type", "crypto_crypto").strip()
|
|
||||||
if not base_asset or not quote_asset:
|
|
||||||
raise ValueError("base_asset and quote_asset are required")
|
|
||||||
svc.delete_pair_fee(base_asset, quote_asset, market_type)
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown action: {action}")
|
|
||||||
except ValueError as exc:
|
|
||||||
pair_fees = svc.list_pair_fees_with_details()
|
|
||||||
pairings = svc.list_pairings()
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request,
|
|
||||||
name="partials/config_fees.html",
|
|
||||||
context={"request": request, "pair_fees": pair_fees,
|
|
||||||
"pairings": pairings,
|
|
||||||
"config_fees_endpoint": "/dashboard/control/config/fees",
|
|
||||||
"error": str(exc)},
|
|
||||||
)
|
|
||||||
pair_fees = svc.list_pair_fees_with_details()
|
|
||||||
pairings = svc.list_pairings()
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
request=request,
|
|
||||||
name="partials/config_fees.html",
|
|
||||||
context={"request": request, "pair_fees": pair_fees,
|
|
||||||
"pairings": pairings,
|
|
||||||
"config_fees_endpoint": "/dashboard/control/config/fees",
|
|
||||||
"error": None},
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/dashboard/api/alerts/status", response_class=JSONResponse)
|
@router.get("/dashboard/api/alerts/status", response_class=JSONResponse)
|
||||||
async def dashboard_alert_status(request: Request) -> JSONResponse:
|
async def dashboard_alert_status(request: Request) -> JSONResponse:
|
||||||
return JSONResponse(_alert_status_snapshot(request))
|
return JSONResponse(_alert_status_snapshot(request))
|
||||||
|
|||||||
@@ -39,15 +39,6 @@ class ConfigPairing(BaseModel):
|
|||||||
updated_at: datetime | None = None
|
updated_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
class ConfigPairFee(BaseModel):
|
|
||||||
pairing_id: int
|
|
||||||
market_type: str # 'crypto_crypto' or 'crypto_fiat'
|
|
||||||
maker_fee_rate: float
|
|
||||||
taker_fee_rate: float
|
|
||||||
source: str = "manual" # 'manual' or 'kraken_api'
|
|
||||||
updated_at: datetime | None = None
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigBacktestingDefaults(BaseModel):
|
class ConfigBacktestingDefaults(BaseModel):
|
||||||
starting_balances: dict[str, float] | None = None
|
starting_balances: dict[str, float] | None = None
|
||||||
trade_capital: float | None = None
|
trade_capital: float | None = None
|
||||||
@@ -216,10 +207,6 @@ class ConfigurationService:
|
|||||||
from arbitrade.storage.repositories import ConfigPairingRepository
|
from arbitrade.storage.repositories import ConfigPairingRepository
|
||||||
return ConfigPairingRepository(self._store)
|
return ConfigPairingRepository(self._store)
|
||||||
|
|
||||||
def _fee_repo(self):
|
|
||||||
from arbitrade.storage.repositories import ConfigPairFeeRepository
|
|
||||||
return ConfigPairFeeRepository(self._store)
|
|
||||||
|
|
||||||
def list_pairings(self) -> list[ConfigPairing]:
|
def list_pairings(self) -> list[ConfigPairing]:
|
||||||
"""List all currency pairings."""
|
"""List all currency pairings."""
|
||||||
return self._pairing_repo().list_pairings()
|
return self._pairing_repo().list_pairings()
|
||||||
@@ -232,65 +219,3 @@ class ConfigurationService:
|
|||||||
pairing = ConfigPairing(
|
pairing = ConfigPairing(
|
||||||
base_asset=base_asset, quote_asset=quote_asset, enabled=True, source=source)
|
base_asset=base_asset, quote_asset=quote_asset, enabled=True, source=source)
|
||||||
return self._pairing_repo().create_pairing(pairing)
|
return self._pairing_repo().create_pairing(pairing)
|
||||||
|
|
||||||
def list_pair_fees_with_details(self) -> list[dict[str, Any]]:
|
|
||||||
"""List all pair fees with pairing details (base/quote asset)."""
|
|
||||||
pairings = self._pairing_repo().list_pairings()
|
|
||||||
result: list[dict[str, Any]] = []
|
|
||||||
for pairing in pairings:
|
|
||||||
fees = self._fee_repo().list_pair_fees(pairing.id or 0)
|
|
||||||
for fee in fees:
|
|
||||||
result.append({
|
|
||||||
"pairing_id": pairing.id,
|
|
||||||
"base_asset": pairing.base_asset,
|
|
||||||
"quote_asset": pairing.quote_asset,
|
|
||||||
"pair_display": f"{pairing.base_asset}/{pairing.quote_asset}",
|
|
||||||
"market_type": fee.market_type,
|
|
||||||
"maker_fee_rate": fee.maker_fee_rate,
|
|
||||||
"taker_fee_rate": fee.taker_fee_rate,
|
|
||||||
"source": fee.source,
|
|
||||||
"updated_at": fee.updated_at.isoformat() if fee.updated_at else None,
|
|
||||||
})
|
|
||||||
return result
|
|
||||||
|
|
||||||
def upsert_pair_fee(
|
|
||||||
self,
|
|
||||||
base_asset: str,
|
|
||||||
quote_asset: str,
|
|
||||||
market_type: str,
|
|
||||||
maker_fee_rate: float,
|
|
||||||
taker_fee_rate: float,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Create or update a pair fee. Creates pairing if it doesn't exist."""
|
|
||||||
pairing = self._pairing_repo().get_pairing(base_asset, quote_asset)
|
|
||||||
if not pairing:
|
|
||||||
pairing = self.create_pairing(base_asset, quote_asset)
|
|
||||||
fee = ConfigPairFee(
|
|
||||||
pairing_id=pairing.id or 0,
|
|
||||||
market_type=market_type,
|
|
||||||
maker_fee_rate=maker_fee_rate,
|
|
||||||
taker_fee_rate=taker_fee_rate,
|
|
||||||
)
|
|
||||||
existing = self._fee_repo().get_pair_fee(pairing.id or 0, market_type)
|
|
||||||
if existing:
|
|
||||||
updated = self._fee_repo().update_pair_fee(pairing.id or 0, market_type, fee)
|
|
||||||
else:
|
|
||||||
updated = self._fee_repo().create_pair_fee(fee)
|
|
||||||
return {
|
|
||||||
"pairing_id": pairing.id,
|
|
||||||
"base_asset": pairing.base_asset,
|
|
||||||
"quote_asset": pairing.quote_asset,
|
|
||||||
"pair_display": f"{pairing.base_asset}/{pairing.quote_asset}",
|
|
||||||
"market_type": updated.market_type,
|
|
||||||
"maker_fee_rate": updated.maker_fee_rate,
|
|
||||||
"taker_fee_rate": updated.taker_fee_rate,
|
|
||||||
"source": updated.source,
|
|
||||||
"updated_at": updated.updated_at.isoformat() if updated.updated_at else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
def delete_pair_fee(self, base_asset: str, quote_asset: str, market_type: str) -> bool:
|
|
||||||
"""Delete a pair fee."""
|
|
||||||
pairing = self._pairing_repo().get_pairing(base_asset, quote_asset)
|
|
||||||
if not pairing or not pairing.id:
|
|
||||||
return False
|
|
||||||
return self._fee_repo().delete_pair_fee(pairing.id, market_type)
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
|
|
||||||
|
import orjson
|
||||||
|
|
||||||
from arbitrade.exchange.kraken_rest import KrakenRestClient
|
from arbitrade.exchange.kraken_rest import KrakenRestClient
|
||||||
from arbitrade.storage.db import DuckDBStore
|
from arbitrade.storage.db import DuckDBStore
|
||||||
from arbitrade.storage.repositories import (
|
from arbitrade.storage.repositories import (
|
||||||
@@ -94,86 +96,37 @@ async def fetch_and_store_account_snapshot(
|
|||||||
maker_fee=maker_fee,
|
maker_fee=maker_fee,
|
||||||
taker_fee=taker_fee,
|
taker_fee=taker_fee,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Fetch wallet balances and write to portfolio_snapshots
|
||||||
|
try:
|
||||||
|
wallet_balances = await client.balances()
|
||||||
|
total_value = 0.0
|
||||||
|
if isinstance(balance_data, dict):
|
||||||
|
eb = balance_data.get("eb")
|
||||||
|
total_value = float(eb) if eb is not None else 0.0
|
||||||
|
with store.connect() as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO portfolio_snapshots (snapshot_at, balances, total_value_usd) VALUES (?, ?, ?)",
|
||||||
|
(
|
||||||
|
datetime.now(timezone.utc),
|
||||||
|
orjson.dumps(wallet_balances).decode(
|
||||||
|
"utf-8") if wallet_balances else None,
|
||||||
|
total_value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
_LOG.info("portfolio_snapshot_stored", total_value_usd=total_value)
|
||||||
|
except Exception:
|
||||||
|
_LOG.exception("balances_fetch_or_store_failed")
|
||||||
|
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
async def fetch_and_store_pair_fees(
|
|
||||||
client: KrakenRestClient,
|
|
||||||
store: DuckDBStore,
|
|
||||||
pairings: list[str],
|
|
||||||
) -> dict[str, dict[str, object]]:
|
|
||||||
"""Query TradeVolume per pair, upsert maker/taker into config_pair_fees.
|
|
||||||
|
|
||||||
Returns dict of pair -> fee info for successfully fetched pairs.
|
|
||||||
"""
|
|
||||||
results: dict[str, dict[str, object]] = {}
|
|
||||||
|
|
||||||
for pair in pairings:
|
|
||||||
try:
|
|
||||||
volume_data = await client.trade_volume(pair=pair)
|
|
||||||
except Exception:
|
|
||||||
_LOG.exception("trade_volume_pair_failed", pair=pair)
|
|
||||||
continue
|
|
||||||
|
|
||||||
if not isinstance(volume_data, dict):
|
|
||||||
continue
|
|
||||||
|
|
||||||
fees_dict = volume_data.get("fees")
|
|
||||||
fee_tier = volume_data.get("fee_tier")
|
|
||||||
fee_tier_str = str(fee_tier) if fee_tier is not None else None
|
|
||||||
|
|
||||||
maker_fee = None
|
|
||||||
taker_fee = None
|
|
||||||
if isinstance(fees_dict, dict) and fee_tier_str is not None:
|
|
||||||
tier_fees = fees_dict.get(fee_tier_str)
|
|
||||||
if isinstance(tier_fees, dict):
|
|
||||||
maker_fee = tier_fees.get("maker")
|
|
||||||
taker_fee = tier_fees.get("taker")
|
|
||||||
|
|
||||||
if maker_fee is not None and taker_fee is not None:
|
|
||||||
base, quote = pair.split("/", 1) if "/" in pair else (pair, "")
|
|
||||||
market_type = "crypto_fiat" if quote.upper(
|
|
||||||
) in {"USD", "EUR", "GBP", "JPY", "CAD", "CHF", "AUD"} else "crypto_crypto"
|
|
||||||
|
|
||||||
with store.connect() as conn:
|
|
||||||
# Find pairing_id
|
|
||||||
pairing_row = conn.execute(
|
|
||||||
"SELECT id FROM config_pairings WHERE base_asset = ? AND quote_asset = ?",
|
|
||||||
(base.upper() if "/" in pair else pair,
|
|
||||||
quote.upper() if "/" in pair else ""),
|
|
||||||
).fetchone()
|
|
||||||
|
|
||||||
if pairing_row is not None:
|
|
||||||
pairing_id = pairing_row[0]
|
|
||||||
conn.execute(
|
|
||||||
"""
|
|
||||||
INSERT OR REPLACE INTO config_pair_fees
|
|
||||||
(pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, source)
|
|
||||||
VALUES (?, ?, ?, ?, current_timestamp, 'kraken_api')
|
|
||||||
""",
|
|
||||||
(pairing_id, market_type, float(
|
|
||||||
maker_fee), float(taker_fee)),
|
|
||||||
)
|
|
||||||
|
|
||||||
results[pair] = {
|
|
||||||
"market_type": market_type,
|
|
||||||
"maker_fee": float(maker_fee),
|
|
||||||
"taker_fee": float(taker_fee),
|
|
||||||
"fee_tier": fee_tier_str,
|
|
||||||
}
|
|
||||||
_LOG.info("pair_fee_synced", pair=pair,
|
|
||||||
maker=maker_fee, taker=taker_fee)
|
|
||||||
|
|
||||||
return results
|
|
||||||
|
|
||||||
|
|
||||||
async def run_fee_sync_loop(
|
async def run_fee_sync_loop(
|
||||||
client: KrakenRestClient,
|
client: KrakenRestClient,
|
||||||
store: DuckDBStore,
|
store: DuckDBStore,
|
||||||
get_active_pairings: callable[[], list[str]],
|
|
||||||
stop_event: asyncio.Event,
|
stop_event: asyncio.Event,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Periodic loop: fetch account snapshot + sync pair fees every hour.
|
"""Periodic loop: fetch account snapshot every hour.
|
||||||
|
|
||||||
Runs until stop_event is set.
|
Runs until stop_event is set.
|
||||||
"""
|
"""
|
||||||
@@ -183,9 +136,6 @@ async def run_fee_sync_loop(
|
|||||||
while not stop_event.is_set():
|
while not stop_event.is_set():
|
||||||
try:
|
try:
|
||||||
await fetch_and_store_account_snapshot(client, store)
|
await fetch_and_store_account_snapshot(client, store)
|
||||||
pairings = get_active_pairings()
|
|
||||||
if pairings:
|
|
||||||
await fetch_and_store_pair_fees(client, store, pairings)
|
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOG.exception("fee_sync_loop_iteration_failed")
|
_LOG.exception("fee_sync_loop_iteration_failed")
|
||||||
|
|
||||||
|
|||||||
@@ -46,15 +46,6 @@ CREATE TABLE IF NOT EXISTS config_pairings (
|
|||||||
UNIQUE(base_asset, quote_asset)
|
UNIQUE(base_asset, quote_asset)
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS config_pair_fees (
|
|
||||||
pairing_id INTEGER NOT NULL,
|
|
||||||
market_type VARCHAR NOT NULL, -- 'crypto_crypto' or 'crypto_fiat'
|
|
||||||
maker_fee_rate DOUBLE NOT NULL,
|
|
||||||
taker_fee_rate DOUBLE NOT NULL,
|
|
||||||
updated_at TIMESTAMP DEFAULT current_timestamp,
|
|
||||||
FOREIGN KEY (pairing_id) REFERENCES config_pairings(id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS config_backtesting_defaults (
|
CREATE TABLE IF NOT EXISTS config_backtesting_defaults (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY,
|
||||||
starting_balances JSON,
|
starting_balances JSON,
|
||||||
@@ -260,8 +251,7 @@ class DuckDBStore:
|
|||||||
_LOG.info("migration_applied", version=1)
|
_LOG.info("migration_applied", version=1)
|
||||||
|
|
||||||
if current_version < 2:
|
if current_version < 2:
|
||||||
# Migration v2: Ensure config_pair_fees FK and new tables
|
# Migration v2: Ensure config_backtesting_defaults table
|
||||||
# config_pair_fees and config_pairings already created by SCHEMA_SQL
|
|
||||||
# config_backtesting_defaults already created by SCHEMA_SQL
|
# config_backtesting_defaults already created by SCHEMA_SQL
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO schema_migrations (version) VALUES (2)")
|
"INSERT OR IGNORE INTO schema_migrations (version) VALUES (2)")
|
||||||
@@ -280,9 +270,6 @@ class DuckDBStore:
|
|||||||
fee_schedule_raw JSON
|
fee_schedule_raw JSON
|
||||||
)
|
)
|
||||||
""")
|
""")
|
||||||
# Add source column to config_pair_fees
|
|
||||||
conn.execute(
|
|
||||||
"ALTER TABLE config_pair_fees ADD COLUMN IF NOT EXISTS source VARCHAR DEFAULT 'manual'")
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT OR IGNORE INTO schema_migrations (version) VALUES (3)")
|
"INSERT OR IGNORE INTO schema_migrations (version) VALUES (3)")
|
||||||
_LOG.info("migration_applied", version=3)
|
_LOG.info("migration_applied", version=3)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from typing import Any
|
|||||||
|
|
||||||
import orjson
|
import orjson
|
||||||
|
|
||||||
from arbitrade.config.service import ConfigBacktestingDefaults, ConfigPairing, ConfigSection, ConfigSetting, ConfigPairFee
|
from arbitrade.config.service import ConfigBacktestingDefaults, ConfigPairing, ConfigSection, ConfigSetting
|
||||||
from arbitrade.storage.db import DuckDBStore
|
from arbitrade.storage.db import DuckDBStore
|
||||||
|
|
||||||
|
|
||||||
@@ -729,129 +729,6 @@ class ConfigPairingRepository:
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class ConfigPairFeeRepository:
|
|
||||||
def __init__(self, store: DuckDBStore) -> None:
|
|
||||||
self._store = store
|
|
||||||
|
|
||||||
def create_pair_fee(self, pair_fee: ConfigPairFee) -> ConfigPairFee:
|
|
||||||
"""Create a new pairing fee record."""
|
|
||||||
with self._store.connect() as conn:
|
|
||||||
cursor = conn.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO config_pair_fees (pairing_id, market_type, maker_fee_rate, taker_fee_rate, source)
|
|
||||||
VALUES (?, ?, ?, ?, ?)
|
|
||||||
RETURNING pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, source
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
pair_fee.pairing_id,
|
|
||||||
pair_fee.market_type,
|
|
||||||
pair_fee.maker_fee_rate,
|
|
||||||
pair_fee.taker_fee_rate,
|
|
||||||
pair_fee.source,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row:
|
|
||||||
return ConfigPairFee(
|
|
||||||
pairing_id=row[0],
|
|
||||||
market_type=row[1],
|
|
||||||
maker_fee_rate=row[2],
|
|
||||||
taker_fee_rate=row[3],
|
|
||||||
updated_at=row[4],
|
|
||||||
source=row[5],
|
|
||||||
)
|
|
||||||
raise ValueError("Failed to create pair fee")
|
|
||||||
|
|
||||||
def get_pair_fee(self, pairing_id: int, market_type: str) -> ConfigPairFee | None:
|
|
||||||
"""Get a pairing fee by pairing ID and market type."""
|
|
||||||
with self._store.connect() as conn:
|
|
||||||
cursor = conn.execute(
|
|
||||||
"""
|
|
||||||
SELECT pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, COALESCE(source, 'manual') AS source
|
|
||||||
FROM config_pair_fees
|
|
||||||
WHERE pairing_id = ? AND market_type = ?
|
|
||||||
""",
|
|
||||||
(pairing_id, market_type),
|
|
||||||
)
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row:
|
|
||||||
return ConfigPairFee(
|
|
||||||
pairing_id=row[0],
|
|
||||||
market_type=row[1],
|
|
||||||
maker_fee_rate=row[2],
|
|
||||||
taker_fee_rate=row[3],
|
|
||||||
updated_at=row[4],
|
|
||||||
source=row[5],
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def update_pair_fee(self, pairing_id: int, market_type: str, pair_fee: ConfigPairFee) -> ConfigPairFee:
|
|
||||||
"""Update an existing pairing fee."""
|
|
||||||
with self._store.connect() as conn:
|
|
||||||
cursor = conn.execute(
|
|
||||||
"""
|
|
||||||
UPDATE config_pair_fees
|
|
||||||
SET maker_fee_rate = ?, taker_fee_rate = ?, source = ?
|
|
||||||
WHERE pairing_id = ? AND market_type = ?
|
|
||||||
RETURNING pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, source
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
pair_fee.maker_fee_rate,
|
|
||||||
pair_fee.taker_fee_rate,
|
|
||||||
pair_fee.source,
|
|
||||||
pairing_id,
|
|
||||||
market_type,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
row = cursor.fetchone()
|
|
||||||
if row:
|
|
||||||
return ConfigPairFee(
|
|
||||||
pairing_id=row[0],
|
|
||||||
market_type=row[1],
|
|
||||||
maker_fee_rate=row[2],
|
|
||||||
taker_fee_rate=row[3],
|
|
||||||
updated_at=row[4],
|
|
||||||
source=row[5],
|
|
||||||
)
|
|
||||||
raise ValueError("Failed to update pair fee")
|
|
||||||
|
|
||||||
def delete_pair_fee(self, pairing_id: int, market_type: str) -> bool:
|
|
||||||
"""Delete a pairing fee."""
|
|
||||||
with self._store.connect() as conn:
|
|
||||||
cursor = conn.execute(
|
|
||||||
"""
|
|
||||||
DELETE FROM config_pair_fees
|
|
||||||
WHERE pairing_id = ? AND market_type = ?
|
|
||||||
""",
|
|
||||||
(pairing_id, market_type),
|
|
||||||
)
|
|
||||||
return cursor.rowcount > 0
|
|
||||||
|
|
||||||
def list_pair_fees(self, pairing_id: int) -> list[ConfigPairFee]:
|
|
||||||
"""List all fees for a pairing."""
|
|
||||||
with self._store.connect() as conn:
|
|
||||||
cursor = conn.execute(
|
|
||||||
"""
|
|
||||||
SELECT pairing_id, market_type, maker_fee_rate, taker_fee_rate, updated_at, COALESCE(source, 'manual') AS source
|
|
||||||
FROM config_pair_fees
|
|
||||||
WHERE pairing_id = ?
|
|
||||||
ORDER BY market_type
|
|
||||||
""",
|
|
||||||
(pairing_id,),
|
|
||||||
)
|
|
||||||
return [
|
|
||||||
ConfigPairFee(
|
|
||||||
pairing_id=row[0],
|
|
||||||
market_type=row[1],
|
|
||||||
maker_fee_rate=row[2],
|
|
||||||
taker_fee_rate=row[3],
|
|
||||||
updated_at=row[4],
|
|
||||||
source=row[5],
|
|
||||||
)
|
|
||||||
for row in cursor.fetchall()
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigBacktestingDefaultsRepository:
|
class ConfigBacktestingDefaultsRepository:
|
||||||
def __init__(self, store: DuckDBStore) -> None:
|
def __init__(self, store: DuckDBStore) -> None:
|
||||||
self._store = store
|
self._store = store
|
||||||
|
|||||||
@@ -4,16 +4,6 @@ page_title="Configuration", page_subtitle="Runtime settings, alerts, exchange,
|
|||||||
risk, and strategy." %} {% include "_header.html" %} {% endwith %} {% endblock
|
risk, and strategy." %} {% include "_header.html" %} {% endwith %} {% endblock
|
||||||
%} {% block content %}
|
%} {% block content %}
|
||||||
|
|
||||||
<section
|
|
||||||
id="fees-shell"
|
|
||||||
hx-get="{{ config_fees_fragment }}"
|
|
||||||
hx-target="this"
|
|
||||||
hx-trigger="load"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
>
|
|
||||||
{% include "partials/config_fees.html" %}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section
|
<section
|
||||||
id="config-shell"
|
id="config-shell"
|
||||||
hx-get="/dashboard/fragment/config"
|
hx-get="/dashboard/fragment/config"
|
||||||
|
|||||||
@@ -1,106 +0,0 @@
|
|||||||
{% extends "base.html" %} {% block title %}Fee Configuration{% endblock %} {%
|
|
||||||
block content %}
|
|
||||||
<div class="container mt-4">
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<h2>Fee Configuration</h2>
|
|
||||||
|
|
||||||
{% if message %}
|
|
||||||
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
|
||||||
{{ message }}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn-close"
|
|
||||||
data-bs-dismiss="alert"
|
|
||||||
aria-label="Close"
|
|
||||||
></button>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header">
|
|
||||||
<h5>Configure Pairing Fees</h5>
|
|
||||||
</div>
|
|
||||||
<div class="card-body">
|
|
||||||
<form method="post" action="/dashboard/config/fees/save">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Pairing</th>
|
|
||||||
<th>Crypto/Crypto Maker</th>
|
|
||||||
<th>Crypto/Crypto Taker</th>
|
|
||||||
<th>Crypto/Fiat Maker</th>
|
|
||||||
<th>Crypto/Fiat Taker</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for pairing in pairings %}
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
{{ pairing.base_asset }}/{{ pairing.quote_asset }}
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="pairing_id_{{ pairing.id }}"
|
|
||||||
value="{{ pairing.id }}"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.0001"
|
|
||||||
min="0"
|
|
||||||
max="0.05"
|
|
||||||
class="form-control"
|
|
||||||
name="maker_fee_{{ pairing.id }}"
|
|
||||||
placeholder="0.0010"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.0001"
|
|
||||||
min="0"
|
|
||||||
max="0.05"
|
|
||||||
class="form-control"
|
|
||||||
name="taker_fee_{{ pairing.id }}"
|
|
||||||
placeholder="0.0020"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.0001"
|
|
||||||
min="0"
|
|
||||||
max="0.05"
|
|
||||||
class="form-control"
|
|
||||||
name="maker_fee_{{ pairing.id }}_fiat"
|
|
||||||
placeholder="0.0010"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
step="0.0001"
|
|
||||||
min="0"
|
|
||||||
max="0.05"
|
|
||||||
class="form-control"
|
|
||||||
name="taker_fee_{{ pairing.id }}_fiat"
|
|
||||||
placeholder="0.0020"
|
|
||||||
readonly
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Save Fees</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
<div id="config-fees-panel" class="panel" style="margin-top: 16px">
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<h2 style="margin: 0; font-size: 1.3rem">Pair Fees</h2>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="button"
|
|
||||||
onclick="
|
|
||||||
document.getElementById('fee-form-section').style.display =
|
|
||||||
document.getElementById('fee-form-section').style.display === 'none'
|
|
||||||
? 'block'
|
|
||||||
: 'none'
|
|
||||||
"
|
|
||||||
>
|
|
||||||
Add / Edit Fee
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if error %}
|
|
||||||
<div
|
|
||||||
style="
|
|
||||||
background: rgba(186, 61, 79, 0.2);
|
|
||||||
border: 1px solid #ba3d4f;
|
|
||||||
border-radius: 10px;
|
|
||||||
padding: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
{{ error }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Fee Table -->
|
|
||||||
{% if pair_fees %}
|
|
||||||
<div style="overflow-x: auto">
|
|
||||||
<table style="width: 100%; border-collapse: collapse; font-size: 0.9rem">
|
|
||||||
<thead>
|
|
||||||
<tr style="border-bottom: 1px solid rgba(255, 255, 255, 0.14)">
|
|
||||||
<th style="text-align: left; padding: 10px; color: #9fb2d0">Pair</th>
|
|
||||||
<th style="text-align: left; padding: 10px; color: #9fb2d0">
|
|
||||||
Market Type
|
|
||||||
</th>
|
|
||||||
<th style="text-align: left; padding: 10px; color: #9fb2d0">
|
|
||||||
Maker Rate
|
|
||||||
</th>
|
|
||||||
<th style="text-align: left; padding: 10px; color: #9fb2d0">
|
|
||||||
Taker Rate
|
|
||||||
</th>
|
|
||||||
<th style="text-align: left; padding: 10px; color: #9fb2d0">
|
|
||||||
Updated
|
|
||||||
</th>
|
|
||||||
<th style="text-align: left; padding: 10px; color: #9fb2d0">
|
|
||||||
Actions
|
|
||||||
</th>
|
|
||||||
<th style="text-align: left; padding: 10px; color: #9fb2d0">
|
|
||||||
Source
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for fee in pair_fees %}
|
|
||||||
<tr style="border-bottom: 1px solid rgba(255, 255, 255, 0.06)">
|
|
||||||
<td style="padding: 10px">{{ fee.pair_display }}</td>
|
|
||||||
<td style="padding: 10px">{{ fee.market_type }}</td>
|
|
||||||
<td style="padding: 10px">{{ "%.4f"|format(fee.maker_fee_rate) }}</td>
|
|
||||||
<td style="padding: 10px">{{ "%.4f"|format(fee.taker_fee_rate) }}</td>
|
|
||||||
<td style="padding: 10px; color: #7f95b7">
|
|
||||||
{{ fee.updated_at or "—" }}
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px">
|
|
||||||
<form
|
|
||||||
hx-post="{{ config_fees_endpoint }}"
|
|
||||||
hx-target="#config-fees-panel"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
style="display: inline"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="action" value="update" />
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="base_asset"
|
|
||||||
value="{{ fee.base_asset }}"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="quote_asset"
|
|
||||||
value="{{ fee.quote_asset }}"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="market_type"
|
|
||||||
value="{{ fee.market_type }}"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="maker_fee_rate"
|
|
||||||
value="{{ fee.maker_fee_rate }}"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="taker_fee_rate"
|
|
||||||
value="{{ fee.taker_fee_rate }}"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="button secondary"
|
|
||||||
style="padding: 4px 10px; font-size: 0.8rem"
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
<form
|
|
||||||
hx-post="{{ config_fees_endpoint }}"
|
|
||||||
hx-target="#config-fees-panel"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
style="display: inline"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="action" value="delete" />
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="base_asset"
|
|
||||||
value="{{ fee.base_asset }}"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="quote_asset"
|
|
||||||
value="{{ fee.quote_asset }}"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="hidden"
|
|
||||||
name="market_type"
|
|
||||||
value="{{ fee.market_type }}"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
class="button danger"
|
|
||||||
style="padding: 4px 10px; font-size: 0.8rem"
|
|
||||||
onclick="return confirm('Delete this fee?');"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
</td>
|
|
||||||
<td style="padding: 10px; color: #7f95b7">
|
|
||||||
{{ fee.source if fee.source else "manual" }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
<div style="color: #7f95b7; padding: 20px; text-align: center">
|
|
||||||
No pair fees configured. Click "Add / Edit Fee" to create one.
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<!-- Add/Edit Form -->
|
|
||||||
<div id="fee-form-section" style="display: none; margin-top: 20px">
|
|
||||||
<div class="card">
|
|
||||||
<div class="label">Add / Edit Fee</div>
|
|
||||||
<form
|
|
||||||
class="form-grid"
|
|
||||||
hx-post="{{ config_fees_endpoint }}"
|
|
||||||
hx-target="#config-fees-panel"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
style="
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<input type="hidden" name="action" value="add" />
|
|
||||||
<label class="field">
|
|
||||||
<span>Base Asset</span>
|
|
||||||
<input name="base_asset" type="text" placeholder="BTC" required />
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Quote Asset</span>
|
|
||||||
<input name="quote_asset" type="text" placeholder="USD" required />
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Market Type</span>
|
|
||||||
<select name="market_type">
|
|
||||||
<option value="crypto_crypto">crypto_crypto</option>
|
|
||||||
<option value="crypto_fiat">crypto_fiat</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Maker Fee Rate</span>
|
|
||||||
<input
|
|
||||||
name="maker_fee_rate"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.0001"
|
|
||||||
placeholder="API default"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>Taker Fee Rate</span>
|
|
||||||
<input
|
|
||||||
name="taker_fee_rate"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.0001"
|
|
||||||
placeholder="API default"
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span> </span>
|
|
||||||
<button type="submit" class="button">Save Fee</button>
|
|
||||||
</label>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -54,6 +54,7 @@
|
|||||||
{{ balances }}
|
{{ balances }}
|
||||||
</div>
|
</div>
|
||||||
<div class="meta">Total value {{ total_value }}</div>
|
<div class="meta">Total value {{ total_value }}</div>
|
||||||
|
<div class="meta">Equity {{ equity }}</div>
|
||||||
</article>
|
</article>
|
||||||
<article class="card">
|
<article class="card">
|
||||||
<div class="label">Opportunity Feed</div>
|
<div class="label">Opportunity Feed</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user