Skip to content

Working with Positions

Positions are the core domain objects in qlcore, representing your holdings in an instrument.

Position Types

SpotPosition

For spot markets and equities where you own the underlying asset.

import qlcore as qc
from decimal import Decimal

position = qc.SpotPosition(
    instrument_id="BTC-USD",
    side=qc.PositionSide.LONG,
    size=Decimal("1.0"),
    avg_entry_price=Decimal("45000"),
    realized_pnl=Decimal("0"),
    unrealized_pnl=Decimal("0"),
    total_fees=Decimal("45"),
)

PerpetualPosition

For perpetual futures with funding payments.

position = qc.PerpetualPosition(
    instrument_id="BTC-USDT-PERP",
    side=qc.PositionSide.LONG,
    size=Decimal("1.0"),
    avg_entry_price=Decimal("45000"),
    accumulated_funding=Decimal("-12.50"),  # Funding paid out
    realized_pnl=Decimal("0"),
    unrealized_pnl=Decimal("0"),
    total_fees=Decimal("45"),
)

# Apply funding
new_position = position.apply_funding(Decimal("-5.00"))
print(new_position.accumulated_funding)  # -17.50

# Total realized includes funding
print(new_position.total_realized_pnl)  # realized_pnl + accumulated_funding

FuturesPosition

For expiring futures contracts.

import time

expiry = int(time.time() * 1000) + (30 * 24 * 60 * 60 * 1000)  # 30 days

position = qc.FuturesPosition(
    instrument_id="BTC-USD-MAR25",
    side=qc.PositionSide.SHORT,
    size=Decimal("0.5"),
    avg_entry_price=Decimal("46000"),
    expiry_ms=expiry,
    realized_pnl=Decimal("0"),
    unrealized_pnl=Decimal("0"),
    total_fees=Decimal("23"),
)

# Check expiry status
print(position.is_expired())        # False
print(position.time_to_expiry())    # ~30 days in ms

# Settle at expiry
settled = position.settle(settlement_price=Decimal("47000"))

Creating Positions

Direct Construction

position = qc.SpotPosition(
    instrument_id="BTC-USD",
    side=qc.PositionSide.LONG,
    size=Decimal("1.0"),
    avg_entry_price=Decimal("45000"),
)

PositionBuilder (Fluent API)

position = (
    qc.PositionBuilder("BTC-PERP")
    .long()
    .with_size("1.5")
    .with_entry_price("45000")
    .with_realized_pnl("100")
    .with_fees("15")
    .as_perpetual()
    .build()
)

Builder methods:

Method Description
.long() Set side to LONG
.short() Set side to SHORT
.with_size(size) Set position size
.with_entry_price(price) Set average entry price
.with_realized_pnl(pnl) Set realized PnL
.with_unrealized_pnl(pnl) Set unrealized PnL
.with_fees(fees) Set accumulated fees
.as_perpetual() Build as PerpetualPosition
.as_futures(expiry_ms) Build as FuturesPosition
.build() Build the position (SpotPosition by default)

From Dictionary

data = {
    "instrument_id": "BTC-USD",
    "side": "LONG",
    "size": "1.0",
    "avg_entry_price": "45000",
}

# SpotPosition.from_dict()
position = qc.SpotPosition.from_dict(data)

Updating Positions

Positions are immutable. Use evolve() to create modified copies:

# Update single field
updated = position.evolve(size=Decimal("2.0"))

# Update multiple fields
updated = position.evolve(
    size=Decimal("2.0"),
    avg_entry_price=Decimal("46000"),
    realized_pnl=Decimal("500"),
)

Position Metrics

Mark to Market

Update unrealized PnL based on current price:

current_price = Decimal("46000")

mtm_position = qc.mark_to_market(position, mark_price=current_price)
print(mtm_position.unrealized_pnl)  # Updated to current price

Unrealized PnL

Calculate PnL without modifying position:

upnl = qc.unrealized_pnl(position, current_price=Decimal("46000"))
print(upnl)  # Decimal value

Leverage

For positions with margin:

leverage = qc.position_leverage(position, margin=Decimal("4500"))
print(leverage)  # 10x if notional is 45000

Position Summary

Get a REPL-friendly string representation:

print(position.summary())

Output:

SpotPosition(BTC-USD)
  Side: LONG
  Size: 1.0
  Entry: 45000
  Realized PnL: 0
  Unrealized PnL: 0
  Fees: 45


Serialization

To Dictionary

data = position.to_dict()
# Returns dict with all fields

To JSON

# Serialize
json_str = qc.to_json(position)

# Deserialize (type-aware)
position = qc.from_json(json_str, qc.PerpetualPosition)

# Or use generic deserialize
position = qc.deserialize_position(json_str)  # Infers type

Cost Basis Tracking

Track individual lots for FIFO/LIFO tax calculations:

from qlcore.positions import Lot

lot1 = Lot(
    quantity=Decimal("0.5"),
    price=Decimal("44000"),
    timestamp_ms=1701849600000,
)

lot2 = Lot(
    quantity=Decimal("0.5"),
    price=Decimal("46000"),
    timestamp_ms=1701936000000,
)

# SpotPosition can track lots
position = qc.SpotPosition(
    instrument_id="BTC-USD",
    side=qc.PositionSide.LONG,
    size=Decimal("1.0"),
    avg_entry_price=Decimal("45000"),
    lots=[lot1, lot2],
)