Symbol & data refactoring for Nautilus
This commit is contained in:
@@ -243,6 +243,15 @@ generatorOptions:
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ See [[protocol]] for detailed ZMQ patterns and message formats.
|
|||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
- CCXT-based exchange adapters
|
- CCXT-based exchange adapters
|
||||||
- Subscribes to work queue via exchange prefix (e.g., `BINANCE:`)
|
- Subscribes to work queue via exchange suffix (e.g., `.BINANCE`)
|
||||||
- Writes raw data to Kafka only (no direct client responses)
|
- Writes raw data to Kafka only (no direct client responses)
|
||||||
- Supports realtime ticks and historical OHLC
|
- Supports realtime ticks and historical OHLC
|
||||||
|
|
||||||
@@ -324,7 +324,7 @@ See [[protocol#Historical Data Query Flow]] for details.
|
|||||||
4. Relay → Clients (XPUB/SUB): Fanout to subscribers
|
4. Relay → Clients (XPUB/SUB): Fanout to subscribers
|
||||||
```
|
```
|
||||||
|
|
||||||
**Topic Format:** `{ticker}|{data_type}` (e.g., `BINANCE:BTC/USDT|tick`)
|
**Topic Format:** `{ticker}|{data_type}` (e.g., `BTC/USDT.BINANCE|tick`)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ All sockets bind on **Relay** (well-known endpoint). Components connect to relay
|
|||||||
- **Socket Type**: Relay uses PUB (bind), Ingestors use SUB (connect)
|
- **Socket Type**: Relay uses PUB (bind), Ingestors use SUB (connect)
|
||||||
- **Endpoint**: `tcp://*:5555` (Relay binds)
|
- **Endpoint**: `tcp://*:5555` (Relay binds)
|
||||||
- **Message Types**: `DataRequest` (historical or realtime)
|
- **Message Types**: `DataRequest` (historical or realtime)
|
||||||
- **Topic Prefix**: Exchange name (e.g., `BINANCE:`, `COINBASE:`)
|
- **Topic Prefix**: Market name (e.g., `BTC/USDT.`, `ETH/BTC.`)
|
||||||
- **Behavior**:
|
- **Behavior**:
|
||||||
- Relay publishes work with exchange prefix from ticker
|
- Relay publishes work with exchange prefix from ticker
|
||||||
- Ingestors subscribe only to exchanges they support
|
- Ingestors subscribe only to exchanges they support
|
||||||
@@ -100,7 +100,7 @@ All sockets bind on **Relay** (well-known endpoint). Components connect to relay
|
|||||||
- Relay XSUB (connect) → Flink PUB (bind) - Port 5557
|
- Relay XSUB (connect) → Flink PUB (bind) - Port 5557
|
||||||
- **Message Types**: `Tick`, `OHLC`, `HistoryReadyNotification`, `SymbolMetadataUpdated`
|
- **Message Types**: `Tick`, `OHLC`, `HistoryReadyNotification`, `SymbolMetadataUpdated`
|
||||||
- **Topic Formats**:
|
- **Topic Formats**:
|
||||||
- Market data: `{ticker}|{data_type}` (e.g., `BINANCE:BTC/USDT|tick`)
|
- Market data: `{ticker}|{data_type}` (e.g., `BTC/USDT.BINANCE|tick`)
|
||||||
- Notifications: `RESPONSE:{client_id}` or `HISTORY_READY:{request_id}`
|
- Notifications: `RESPONSE:{client_id}` or `HISTORY_READY:{request_id}`
|
||||||
- System notifications: `METADATA_UPDATE` (for symbol metadata updates)
|
- System notifications: `METADATA_UPDATE` (for symbol metadata updates)
|
||||||
- **Behavior**:
|
- **Behavior**:
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ async def get_conversation_summary() -> str:
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"currentChart": {
|
"currentChart": {
|
||||||
"ticker": "BINANCE:BTC/USDT",
|
"ticker": "BTC/USDT.BINANCE",
|
||||||
"timeframe": "1h",
|
"timeframe": "1h",
|
||||||
"indicators": ["SMA(20)", "RSI(14)", "MACD(12,26,9)"]
|
"indicators": ["SMA(20)", "RSI(14)", "MACD(12,26,9)"]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -81,7 +81,8 @@ public class SchemaInitializer {
|
|||||||
// Bump this when the schema changes. Tables with a different (or missing) version
|
// Bump this when the schema changes. Tables with a different (or missing) version
|
||||||
// will be dropped and recreated. Increment by 1 for each incompatible change.
|
// will be dropped and recreated. Increment by 1 for each incompatible change.
|
||||||
// v2: open/high/low/close changed from required to optional to support null gap bars
|
// v2: open/high/low/close changed from required to optional to support null gap bars
|
||||||
private static final String OHLC_SCHEMA_VERSION = "2";
|
// v3: timestamps changed from microseconds to nanoseconds; ticker format changed to BTC/USDT.BINANCE
|
||||||
|
private static final String OHLC_SCHEMA_VERSION = "3";
|
||||||
private static final String SCHEMA_VERSION_PROP = "app.schema.version";
|
private static final String SCHEMA_VERSION_PROP = "app.schema.version";
|
||||||
|
|
||||||
private void initializeOhlcTable() {
|
private void initializeOhlcTable() {
|
||||||
@@ -124,9 +125,9 @@ public class SchemaInitializer {
|
|||||||
// so that GenericRowData.setField() accepts a plain Long value.
|
// so that GenericRowData.setField() accepts a plain Long value.
|
||||||
Schema schema = new Schema(
|
Schema schema = new Schema(
|
||||||
// Primary key fields
|
// Primary key fields
|
||||||
required(1, "ticker", Types.StringType.get(), "Market identifier (e.g., BINANCE:BTC/USDT)"),
|
required(1, "ticker", Types.StringType.get(), "Market identifier in Nautilus format (e.g., BTC/USDT.BINANCE)"),
|
||||||
required(2, "period_seconds", Types.IntegerType.get(), "OHLC period in seconds"),
|
required(2, "period_seconds", Types.IntegerType.get(), "OHLC period in seconds"),
|
||||||
required(3, "timestamp", Types.LongType.get(), "Candle timestamp in microseconds since epoch"),
|
required(3, "timestamp", Types.LongType.get(), "Candle timestamp in nanoseconds since epoch"),
|
||||||
|
|
||||||
// OHLC price data — optional to support gap bars (null = no trades that period)
|
// OHLC price data — optional to support gap bars (null = no trades that period)
|
||||||
optional(4, "open", Types.LongType.get(), "Opening price"),
|
optional(4, "open", Types.LongType.get(), "Opening price"),
|
||||||
@@ -150,7 +151,7 @@ public class SchemaInitializer {
|
|||||||
|
|
||||||
// Metadata fields
|
// Metadata fields
|
||||||
optional(16, "request_id", Types.StringType.get(), "Request ID that generated this data"),
|
optional(16, "request_id", Types.StringType.get(), "Request ID that generated this data"),
|
||||||
required(17, "ingested_at", Types.LongType.get(), "Timestamp when data was ingested by Flink")
|
required(17, "ingested_at", Types.LongType.get(), "Timestamp when data was ingested by Flink (nanoseconds since epoch)")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the table with partitioning and properties
|
// Create the table with partitioning and properties
|
||||||
@@ -176,7 +177,10 @@ public class SchemaInitializer {
|
|||||||
/**
|
/**
|
||||||
* Initialize the symbol_metadata table if it doesn't exist.
|
* Initialize the symbol_metadata table if it doesn't exist.
|
||||||
*/
|
*/
|
||||||
private static final String SYMBOL_METADATA_SCHEMA_VERSION = "1";
|
// v2: removed tick_denom/base_denom/quote_denom; added Nautilus instrument fields
|
||||||
|
// (price_precision, size_precision, tick_size, lot_size, min_notional,
|
||||||
|
// margin_init, margin_maint, maker_fee, taker_fee, contract_multiplier)
|
||||||
|
private static final String SYMBOL_METADATA_SCHEMA_VERSION = "2";
|
||||||
|
|
||||||
private void initializeSymbolMetadataTable() {
|
private void initializeSymbolMetadataTable() {
|
||||||
TableIdentifier tableId = TableIdentifier.of(namespace, "symbol_metadata");
|
TableIdentifier tableId = TableIdentifier.of(namespace, "symbol_metadata");
|
||||||
@@ -220,24 +224,31 @@ public class SchemaInitializer {
|
|||||||
required(2, "market_id", Types.StringType.get(), "Market symbol (e.g., BTC/USDT)"),
|
required(2, "market_id", Types.StringType.get(), "Market symbol (e.g., BTC/USDT)"),
|
||||||
|
|
||||||
// Market information
|
// Market information
|
||||||
optional(3, "market_type", Types.StringType.get(), "Market type (spot, futures, swap)"),
|
optional(3, "market_type", Types.StringType.get(), "Market type (spot, futures, swap, CryptoPerpetual)"),
|
||||||
optional(4, "description", Types.StringType.get(), "Human-readable description"),
|
optional(4, "description", Types.StringType.get(), "Human-readable description"),
|
||||||
optional(5, "base_asset", Types.StringType.get(), "Base asset (e.g., BTC)"),
|
optional(5, "base_asset", Types.StringType.get(), "Base asset (e.g., BTC)"),
|
||||||
optional(6, "quote_asset", Types.StringType.get(), "Quote asset (e.g., USDT)"),
|
optional(6, "quote_asset", Types.StringType.get(), "Quote asset (e.g., USDT)"),
|
||||||
|
|
||||||
// Precision/denominator information
|
|
||||||
optional(7, "tick_denom", Types.LongType.get(), "Tick price denominator (10^n for n decimals)"),
|
|
||||||
optional(8, "base_denom", Types.LongType.get(), "Base asset denominator"),
|
|
||||||
optional(9, "quote_denom", Types.LongType.get(), "Quote asset denominator"),
|
|
||||||
|
|
||||||
// Supported timeframes
|
// Supported timeframes
|
||||||
optional(10, "supported_period_seconds", Types.ListType.ofRequired(11, Types.IntegerType.get()), "Supported OHLC periods in seconds"),
|
optional(10, "supported_period_seconds", Types.ListType.ofRequired(11, Types.IntegerType.get()), "Supported OHLC periods in seconds"),
|
||||||
|
|
||||||
// Optional timing information
|
// Optional timing information
|
||||||
optional(12, "earliest_time", Types.LongType.get(), "Earliest available data timestamp (microseconds)"),
|
optional(12, "earliest_time", Types.LongType.get(), "Earliest available data timestamp (nanoseconds since epoch)"),
|
||||||
|
|
||||||
// Metadata
|
// Metadata
|
||||||
required(13, "updated_at", Types.LongType.get(), "Timestamp when metadata was last updated (microseconds)")
|
required(13, "updated_at", Types.LongType.get(), "Timestamp when metadata was last updated (nanoseconds since epoch)"),
|
||||||
|
|
||||||
|
// Nautilus Instrument fields — required for constructing Instrument objects in the sandbox bridge
|
||||||
|
optional(14, "price_precision", Types.IntegerType.get(), "Decimal places for prices (e.g., 2 means $0.01 resolution)"),
|
||||||
|
optional(15, "size_precision", Types.IntegerType.get(), "Decimal places for quantities"),
|
||||||
|
optional(16, "tick_size", Types.DoubleType.get(), "Minimum price increment (e.g., 0.01)"),
|
||||||
|
optional(17, "lot_size", Types.DoubleType.get(), "Minimum order size"),
|
||||||
|
optional(18, "min_notional", Types.DoubleType.get(), "Minimum order value in quote currency"),
|
||||||
|
optional(19, "margin_init", Types.DoubleType.get(), "Initial margin requirement (futures/perps only)"),
|
||||||
|
optional(20, "margin_maint", Types.DoubleType.get(), "Maintenance margin (futures/perps only)"),
|
||||||
|
optional(21, "maker_fee", Types.DoubleType.get(), "Maker fee rate (e.g., 0.001 = 0.1%)"),
|
||||||
|
optional(22, "taker_fee", Types.DoubleType.get(), "Taker fee rate"),
|
||||||
|
optional(23, "contract_multiplier", Types.DoubleType.get(), "Contract multiplier for derivatives (default 1.0)")
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create the table with partitioning and properties
|
// Create the table with partitioning and properties
|
||||||
|
|||||||
@@ -108,13 +108,13 @@ public class IngestorWorkQueue {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a data request to ingestors via PUB socket with exchange prefix.
|
* Send a data request to ingestors via PUB socket with exchange prefix.
|
||||||
* The topic prefix is extracted from the ticker (e.g., "BINANCE:BTC/USDT" -> "BINANCE:")
|
* The topic prefix is extracted from the ticker (e.g., "BTC/USDT.BINANCE" -> "BINANCE:")
|
||||||
*/
|
*/
|
||||||
private void sendToIngestors(DataRequestMessage request) {
|
private void sendToIngestors(DataRequestMessage request) {
|
||||||
try {
|
try {
|
||||||
byte[] protobufData = request.toProtobuf();
|
byte[] protobufData = request.toProtobuf();
|
||||||
|
|
||||||
// Extract exchange prefix from ticker (e.g., "BINANCE:BTC/USDT" -> "BINANCE:")
|
// Extract exchange prefix from ticker (e.g., "BTC/USDT.BINANCE" -> "BINANCE:")
|
||||||
String ticker = request.getTicker();
|
String ticker = request.getTicker();
|
||||||
String exchangePrefix = extractExchangePrefix(ticker);
|
String exchangePrefix = extractExchangePrefix(ticker);
|
||||||
|
|
||||||
@@ -143,7 +143,7 @@ public class IngestorWorkQueue {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract exchange prefix from ticker string.
|
* Extract exchange prefix from ticker string.
|
||||||
* E.g., "BINANCE:BTC/USDT" -> "BINANCE:"
|
* E.g., "BTC/USDT.BINANCE" -> "BINANCE:"
|
||||||
*/
|
*/
|
||||||
private String extractExchangePrefix(String ticker) {
|
private String extractExchangePrefix(String ticker) {
|
||||||
int colonIndex = ticker.indexOf(':');
|
int colonIndex = ticker.indexOf(':');
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ public class HistoryNotificationFunction extends ProcessFunction<OHLCBatchWrappe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String getIcebergTableName(String ticker, int periodSeconds) {
|
private String getIcebergTableName(String ticker, int periodSeconds) {
|
||||||
// Extract exchange from ticker (e.g., "BINANCE:BTC/USDT" -> "binance")
|
// Extract exchange from ticker (e.g., "BTC/USDT.BINANCE" -> "binance")
|
||||||
String exchange = ticker.split(":")[0].toLowerCase();
|
String exchange = ticker.split(":")[0].toLowerCase();
|
||||||
|
|
||||||
// Convert period to human-readable format
|
// Convert period to human-readable format
|
||||||
|
|||||||
@@ -62,9 +62,6 @@ public class MarketDeserializer implements DeserializationSchema<MarketWrapper>
|
|||||||
wrapper.setDescription(market.getDescription());
|
wrapper.setDescription(market.getDescription());
|
||||||
wrapper.setBaseAsset(market.getBaseAsset());
|
wrapper.setBaseAsset(market.getBaseAsset());
|
||||||
wrapper.setQuoteAsset(market.getQuoteAsset());
|
wrapper.setQuoteAsset(market.getQuoteAsset());
|
||||||
wrapper.setTickDenom(market.getTickDenom());
|
|
||||||
wrapper.setBaseDenom(market.getBaseDenom());
|
|
||||||
wrapper.setQuoteDenom(market.getQuoteDenom());
|
|
||||||
|
|
||||||
// Convert repeated field to List
|
// Convert repeated field to List
|
||||||
List<Integer> supportedPeriods = new ArrayList<>(market.getSupportedPeriodSecondsList());
|
List<Integer> supportedPeriods = new ArrayList<>(market.getSupportedPeriodSecondsList());
|
||||||
@@ -72,6 +69,18 @@ public class MarketDeserializer implements DeserializationSchema<MarketWrapper>
|
|||||||
|
|
||||||
wrapper.setEarliestTime(market.getEarliestTime());
|
wrapper.setEarliestTime(market.getEarliestTime());
|
||||||
|
|
||||||
|
// Nautilus Instrument fields
|
||||||
|
wrapper.setPricePrecision(market.getPricePrecision());
|
||||||
|
wrapper.setSizePrecision(market.getSizePrecision());
|
||||||
|
wrapper.setTickSize(market.getTickSize());
|
||||||
|
wrapper.setLotSize(market.getLotSize());
|
||||||
|
wrapper.setMinNotional(market.getMinNotional());
|
||||||
|
wrapper.setMarginInit(market.getMarginInit());
|
||||||
|
wrapper.setMarginMaint(market.getMarginMaint());
|
||||||
|
wrapper.setMakerFee(market.getMakerFee());
|
||||||
|
wrapper.setTakerFee(market.getTakerFee());
|
||||||
|
wrapper.setContractMultiplier(market.getContractMultiplier());
|
||||||
|
|
||||||
return wrapper;
|
return wrapper;
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOG.error("Failed to deserialize Market protobuf", e);
|
LOG.error("Failed to deserialize Market protobuf", e);
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import java.util.List;
|
|||||||
* Represents symbol metadata for a trading pair.
|
* Represents symbol metadata for a trading pair.
|
||||||
*/
|
*/
|
||||||
public class MarketWrapper implements Serializable {
|
public class MarketWrapper implements Serializable {
|
||||||
private static final long serialVersionUID = 1L;
|
private static final long serialVersionUID = 2L;
|
||||||
|
|
||||||
private String exchangeId;
|
private String exchangeId;
|
||||||
private String marketId;
|
private String marketId;
|
||||||
@@ -16,119 +16,79 @@ public class MarketWrapper implements Serializable {
|
|||||||
private String description;
|
private String description;
|
||||||
private String baseAsset;
|
private String baseAsset;
|
||||||
private String quoteAsset;
|
private String quoteAsset;
|
||||||
private long tickDenom;
|
|
||||||
private long baseDenom;
|
|
||||||
private long quoteDenom;
|
|
||||||
private List<Integer> supportedPeriodSeconds;
|
private List<Integer> supportedPeriodSeconds;
|
||||||
private long earliestTime;
|
private long earliestTime;
|
||||||
|
|
||||||
|
// Nautilus Instrument fields
|
||||||
|
private int pricePrecision;
|
||||||
|
private int sizePrecision;
|
||||||
|
private double tickSize;
|
||||||
|
private double lotSize;
|
||||||
|
private double minNotional;
|
||||||
|
private double marginInit;
|
||||||
|
private double marginMaint;
|
||||||
|
private double makerFee;
|
||||||
|
private double takerFee;
|
||||||
|
private double contractMultiplier;
|
||||||
|
|
||||||
public MarketWrapper() {
|
public MarketWrapper() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public MarketWrapper(String exchangeId, String marketId, String marketType, String description,
|
|
||||||
String baseAsset, String quoteAsset, long tickDenom, long baseDenom,
|
|
||||||
long quoteDenom, List<Integer> supportedPeriodSeconds, long earliestTime) {
|
|
||||||
this.exchangeId = exchangeId;
|
|
||||||
this.marketId = marketId;
|
|
||||||
this.marketType = marketType;
|
|
||||||
this.description = description;
|
|
||||||
this.baseAsset = baseAsset;
|
|
||||||
this.quoteAsset = quoteAsset;
|
|
||||||
this.tickDenom = tickDenom;
|
|
||||||
this.baseDenom = baseDenom;
|
|
||||||
this.quoteDenom = quoteDenom;
|
|
||||||
this.supportedPeriodSeconds = supportedPeriodSeconds;
|
|
||||||
this.earliestTime = earliestTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Getters and setters
|
// Getters and setters
|
||||||
public String getExchangeId() {
|
|
||||||
return exchangeId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setExchangeId(String exchangeId) {
|
public String getExchangeId() { return exchangeId; }
|
||||||
this.exchangeId = exchangeId;
|
public void setExchangeId(String exchangeId) { this.exchangeId = exchangeId; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getMarketId() {
|
public String getMarketId() { return marketId; }
|
||||||
return marketId;
|
public void setMarketId(String marketId) { this.marketId = marketId; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setMarketId(String marketId) {
|
public String getMarketType() { return marketType; }
|
||||||
this.marketId = marketId;
|
public void setMarketType(String marketType) { this.marketType = marketType; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getMarketType() {
|
public String getDescription() { return description; }
|
||||||
return marketType;
|
public void setDescription(String description) { this.description = description; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setMarketType(String marketType) {
|
public String getBaseAsset() { return baseAsset; }
|
||||||
this.marketType = marketType;
|
public void setBaseAsset(String baseAsset) { this.baseAsset = baseAsset; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
public String getQuoteAsset() { return quoteAsset; }
|
||||||
return description;
|
public void setQuoteAsset(String quoteAsset) { this.quoteAsset = quoteAsset; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setDescription(String description) {
|
public List<Integer> getSupportedPeriodSeconds() { return supportedPeriodSeconds; }
|
||||||
this.description = description;
|
public void setSupportedPeriodSeconds(List<Integer> supportedPeriodSeconds) { this.supportedPeriodSeconds = supportedPeriodSeconds; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getBaseAsset() {
|
public long getEarliestTime() { return earliestTime; }
|
||||||
return baseAsset;
|
public void setEarliestTime(long earliestTime) { this.earliestTime = earliestTime; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setBaseAsset(String baseAsset) {
|
public int getPricePrecision() { return pricePrecision; }
|
||||||
this.baseAsset = baseAsset;
|
public void setPricePrecision(int pricePrecision) { this.pricePrecision = pricePrecision; }
|
||||||
}
|
|
||||||
|
|
||||||
public String getQuoteAsset() {
|
public int getSizePrecision() { return sizePrecision; }
|
||||||
return quoteAsset;
|
public void setSizePrecision(int sizePrecision) { this.sizePrecision = sizePrecision; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuoteAsset(String quoteAsset) {
|
public double getTickSize() { return tickSize; }
|
||||||
this.quoteAsset = quoteAsset;
|
public void setTickSize(double tickSize) { this.tickSize = tickSize; }
|
||||||
}
|
|
||||||
|
|
||||||
public long getTickDenom() {
|
public double getLotSize() { return lotSize; }
|
||||||
return tickDenom;
|
public void setLotSize(double lotSize) { this.lotSize = lotSize; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setTickDenom(long tickDenom) {
|
public double getMinNotional() { return minNotional; }
|
||||||
this.tickDenom = tickDenom;
|
public void setMinNotional(double minNotional) { this.minNotional = minNotional; }
|
||||||
}
|
|
||||||
|
|
||||||
public long getBaseDenom() {
|
public double getMarginInit() { return marginInit; }
|
||||||
return baseDenom;
|
public void setMarginInit(double marginInit) { this.marginInit = marginInit; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setBaseDenom(long baseDenom) {
|
public double getMarginMaint() { return marginMaint; }
|
||||||
this.baseDenom = baseDenom;
|
public void setMarginMaint(double marginMaint) { this.marginMaint = marginMaint; }
|
||||||
}
|
|
||||||
|
|
||||||
public long getQuoteDenom() {
|
public double getMakerFee() { return makerFee; }
|
||||||
return quoteDenom;
|
public void setMakerFee(double makerFee) { this.makerFee = makerFee; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuoteDenom(long quoteDenom) {
|
public double getTakerFee() { return takerFee; }
|
||||||
this.quoteDenom = quoteDenom;
|
public void setTakerFee(double takerFee) { this.takerFee = takerFee; }
|
||||||
}
|
|
||||||
|
|
||||||
public List<Integer> getSupportedPeriodSeconds() {
|
public double getContractMultiplier() { return contractMultiplier; }
|
||||||
return supportedPeriodSeconds;
|
public void setContractMultiplier(double contractMultiplier) { this.contractMultiplier = contractMultiplier; }
|
||||||
}
|
|
||||||
|
|
||||||
public void setSupportedPeriodSeconds(List<Integer> supportedPeriodSeconds) {
|
|
||||||
this.supportedPeriodSeconds = supportedPeriodSeconds;
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getEarliestTime() {
|
|
||||||
return earliestTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEarliestTime(long earliestTime) {
|
|
||||||
this.earliestTime = earliestTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ public class IcebergOHLCSink {
|
|||||||
String requestId = batch.getRequestId();
|
String requestId = batch.getRequestId();
|
||||||
String ticker = batch.getTicker();
|
String ticker = batch.getTicker();
|
||||||
int periodSeconds = batch.getPeriodSeconds();
|
int periodSeconds = batch.getPeriodSeconds();
|
||||||
long ingestedAt = System.currentTimeMillis() * 1000;
|
long ingestedAt = System.currentTimeMillis() * 1_000_000L; // nanoseconds
|
||||||
|
|
||||||
// Emit one RowData for each OHLC row in the batch
|
// Emit one RowData for each OHLC row in the batch
|
||||||
for (OHLCBatchWrapper.OHLCRow row : batch.getRows()) {
|
for (OHLCBatchWrapper.OHLCRow row : batch.getRows()) {
|
||||||
|
|||||||
@@ -87,8 +87,8 @@ public class SymbolMetadataWriter extends RichFlatMapFunction<MarketWrapper, Mar
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void flatMap(MarketWrapper market, Collector<MarketWrapper> out) throws Exception {
|
public void flatMap(MarketWrapper market, Collector<MarketWrapper> out) throws Exception {
|
||||||
// Create unique key for deduplication
|
// Create unique key for deduplication (internal key, not stored)
|
||||||
String symbolKey = market.getExchangeId() + ":" + market.getMarketId();
|
String symbolKey = market.getMarketId() + "." + market.getExchangeId();
|
||||||
|
|
||||||
// Skip if we've already seen this symbol
|
// Skip if we've already seen this symbol
|
||||||
if (seenSymbols.contains(symbolKey)) {
|
if (seenSymbols.contains(symbolKey)) {
|
||||||
@@ -110,16 +110,25 @@ public class SymbolMetadataWriter extends RichFlatMapFunction<MarketWrapper, Mar
|
|||||||
record.setField("description", market.getDescription());
|
record.setField("description", market.getDescription());
|
||||||
record.setField("base_asset", market.getBaseAsset());
|
record.setField("base_asset", market.getBaseAsset());
|
||||||
record.setField("quote_asset", market.getQuoteAsset());
|
record.setField("quote_asset", market.getQuoteAsset());
|
||||||
record.setField("tick_denom", market.getTickDenom());
|
|
||||||
record.setField("base_denom", market.getBaseDenom());
|
|
||||||
record.setField("quote_denom", market.getQuoteDenom());
|
|
||||||
|
|
||||||
// Convert supported_period_seconds to List<Integer>
|
// Convert supported_period_seconds to List<Integer>
|
||||||
List<Integer> supportedPeriods = new ArrayList<>(market.getSupportedPeriodSeconds());
|
List<Integer> supportedPeriods = new ArrayList<>(market.getSupportedPeriodSeconds());
|
||||||
record.setField("supported_period_seconds", supportedPeriods);
|
record.setField("supported_period_seconds", supportedPeriods);
|
||||||
|
|
||||||
record.setField("earliest_time", market.getEarliestTime() != 0 ? market.getEarliestTime() : null);
|
record.setField("earliest_time", market.getEarliestTime() != 0 ? market.getEarliestTime() : null);
|
||||||
record.setField("updated_at", System.currentTimeMillis() * 1000); // Current time in microseconds
|
record.setField("updated_at", System.currentTimeMillis() * 1_000_000L); // Current time in nanoseconds
|
||||||
|
|
||||||
|
// Nautilus Instrument fields (populated from exchange API data)
|
||||||
|
record.setField("price_precision", market.getPricePrecision() != 0 ? market.getPricePrecision() : null);
|
||||||
|
record.setField("size_precision", market.getSizePrecision() != 0 ? market.getSizePrecision() : null);
|
||||||
|
record.setField("tick_size", market.getTickSize() != 0.0 ? market.getTickSize() : null);
|
||||||
|
record.setField("lot_size", market.getLotSize() != 0.0 ? market.getLotSize() : null);
|
||||||
|
record.setField("min_notional", market.getMinNotional() != 0.0 ? market.getMinNotional() : null);
|
||||||
|
record.setField("margin_init", market.getMarginInit() != 0.0 ? market.getMarginInit() : null);
|
||||||
|
record.setField("margin_maint", market.getMarginMaint() != 0.0 ? market.getMarginMaint() : null);
|
||||||
|
record.setField("maker_fee", market.getMakerFee() != 0.0 ? market.getMakerFee() : null);
|
||||||
|
record.setField("taker_fee", market.getTakerFee() != 0.0 ? market.getTakerFee() : null);
|
||||||
|
record.setField("contract_multiplier", market.getContractMultiplier() != 0.0 ? market.getContractMultiplier() : null);
|
||||||
|
|
||||||
// Get or create writer for this exchange
|
// Get or create writer for this exchange
|
||||||
DataWriter<Record> writer = writersByExchange.get(exchangeId);
|
DataWriter<Record> writer = writersByExchange.get(exchangeId);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS trading.ohlc (
|
CREATE TABLE IF NOT EXISTS trading.ohlc (
|
||||||
-- Primary key fields
|
-- Primary key fields
|
||||||
ticker STRING NOT NULL COMMENT 'Market identifier (e.g., BINANCE:BTC/USDT)',
|
ticker STRING NOT NULL COMMENT 'Market identifier (e.g., BTC/USDT.BINANCE)',
|
||||||
period_seconds INT NOT NULL COMMENT 'OHLC period in seconds (60, 300, 900, 3600, 14400, 86400, 604800, etc.)',
|
period_seconds INT NOT NULL COMMENT 'OHLC period in seconds (60, 300, 900, 3600, 14400, 86400, 604800, etc.)',
|
||||||
timestamp BIGINT NOT NULL COMMENT 'Candle timestamp in microseconds since epoch',
|
timestamp BIGINT NOT NULL COMMENT 'Candle timestamp in microseconds since epoch',
|
||||||
|
|
||||||
|
|||||||
@@ -388,8 +388,8 @@ export class DuckDBClient {
|
|||||||
async queryOHLC(
|
async queryOHLC(
|
||||||
ticker: string,
|
ticker: string,
|
||||||
period_seconds: number,
|
period_seconds: number,
|
||||||
start_time: bigint, // microseconds
|
start_time: bigint, // nanoseconds
|
||||||
end_time: bigint // microseconds
|
end_time: bigint // nanoseconds
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
await this.initialize();
|
await this.initialize();
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export interface IcebergMessage {
|
|||||||
role: 'user' | 'assistant' | 'system' | 'workspace';
|
role: 'user' | 'assistant' | 'system' | 'workspace';
|
||||||
content: string;
|
content: string;
|
||||||
metadata: string; // JSON string
|
metadata: string; // JSON string
|
||||||
timestamp: number; // microseconds
|
timestamp: number; // nanoseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +54,7 @@ export interface IcebergCheckpoint {
|
|||||||
checkpoint_id: string;
|
checkpoint_id: string;
|
||||||
checkpoint_data: string; // JSON string
|
checkpoint_data: string; // JSON string
|
||||||
metadata: string; // JSON string
|
metadata: string; // JSON string
|
||||||
timestamp: number; // microseconds
|
timestamp: number; // nanoseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,8 +213,8 @@ export class IcebergClient {
|
|||||||
async queryOHLC(
|
async queryOHLC(
|
||||||
ticker: string,
|
ticker: string,
|
||||||
period_seconds: number,
|
period_seconds: number,
|
||||||
start_time: bigint, // microseconds
|
start_time: bigint, // nanoseconds
|
||||||
end_time: bigint // microseconds
|
end_time: bigint // nanoseconds
|
||||||
): Promise<any[]> {
|
): Promise<any[]> {
|
||||||
return this.duckdb.queryOHLC(ticker, period_seconds, start_time, end_time);
|
return this.duckdb.queryOHLC(ticker, period_seconds, start_time, end_time);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ export class ZMQRelayClient {
|
|||||||
*
|
*
|
||||||
* IMPORTANT: Call connect() before using this method.
|
* IMPORTANT: Call connect() before using this method.
|
||||||
*
|
*
|
||||||
* @param ticker Market identifier (e.g., "BINANCE:BTC/USDT")
|
* @param ticker Market identifier (e.g., "BTC/USDT.BINANCE")
|
||||||
* @param period_seconds OHLC period in seconds
|
* @param period_seconds OHLC period in seconds
|
||||||
* @param start_time Start timestamp in MICROSECONDS
|
* @param start_time Start timestamp in MICROSECONDS
|
||||||
* @param end_time End timestamp in MICROSECONDS
|
* @param end_time End timestamp in MICROSECONDS
|
||||||
|
|||||||
@@ -588,7 +588,7 @@ export class AgentHarness {
|
|||||||
const labels: Record<string, string> = {
|
const labels: Record<string, string> = {
|
||||||
research: 'Researching...',
|
research: 'Researching...',
|
||||||
get_chart_data: 'Fetching chart data...',
|
get_chart_data: 'Fetching chart data...',
|
||||||
symbol_lookup: 'Looking up symbol...',
|
symbol_lookup: 'Searching symbol...',
|
||||||
category_list: 'Seeing what we have...',
|
category_list: 'Seeing what we have...',
|
||||||
category_edit: 'Coding...',
|
category_edit: 'Coding...',
|
||||||
category_write: 'Coding...',
|
category_write: 'Coding...',
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class API:
|
|||||||
|
|
||||||
# Fetch data
|
# Fetch data
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21"
|
end_time="2021-12-21"
|
||||||
@@ -107,8 +107,8 @@ class DataAPI(ABC):
|
|||||||
Fetch historical OHLC candlestick data for a market.
|
Fetch historical OHLC candlestick data for a market.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier in format "EXCHANGE:SYMBOL"
|
ticker: Market identifier in format "MARKET.EXCHANGE"
|
||||||
Examples: "BINANCE:BTC/USDT", "COINBASE:ETH/USD"
|
Examples: "BTC/USDT.BINANCE", "ETH/USD.COINBASE"
|
||||||
period_seconds: Candle period in seconds
|
period_seconds: Candle period in seconds
|
||||||
Common values:
|
Common values:
|
||||||
- 60 (1 minute)
|
- 60 (1 minute)
|
||||||
@@ -135,7 +135,7 @@ class DataAPI(ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
DataFrame with candlestick data sorted by timestamp (ascending).
|
DataFrame with candlestick data sorted by timestamp (ascending).
|
||||||
Standard columns (always included):
|
Standard columns (always included):
|
||||||
- timestamp: Period start time in microseconds
|
- timestamp: Period start time in nanoseconds
|
||||||
- open: Opening price (decimal float)
|
- open: Opening price (decimal float)
|
||||||
- high: Highest price (decimal float)
|
- high: Highest price (decimal float)
|
||||||
- low: Lowest price (decimal float)
|
- low: Lowest price (decimal float)
|
||||||
@@ -151,7 +151,7 @@ class DataAPI(ABC):
|
|||||||
Examples:
|
Examples:
|
||||||
# Basic OHLC with Unix timestamp
|
# Basic OHLC with Unix timestamp
|
||||||
df = await api.historical_ohlc(
|
df = await api.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time=1640000000,
|
start_time=1640000000,
|
||||||
end_time=1640086400
|
end_time=1640086400
|
||||||
@@ -159,7 +159,7 @@ class DataAPI(ABC):
|
|||||||
|
|
||||||
# Using date strings with volume
|
# Using date strings with volume
|
||||||
df = await api.historical_ohlc(
|
df = await api.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21",
|
end_time="2021-12-21",
|
||||||
@@ -169,7 +169,7 @@ class DataAPI(ABC):
|
|||||||
# Using datetime objects
|
# Using datetime objects
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
df = await api.historical_ohlc(
|
df = await api.historical_ohlc(
|
||||||
ticker="COINBASE:ETH/USD",
|
ticker="ETH/USD.COINBASE",
|
||||||
period_seconds=300,
|
period_seconds=300,
|
||||||
start_time=datetime(2021, 12, 20, 9, 30),
|
start_time=datetime(2021, 12, 20, 9, 30),
|
||||||
end_time=datetime(2021, 12, 20, 16, 30),
|
end_time=datetime(2021, 12, 20, 16, 30),
|
||||||
@@ -193,8 +193,8 @@ class DataAPI(ABC):
|
|||||||
specify exact timestamps. Useful for real-time analysis and indicators.
|
specify exact timestamps. Useful for real-time analysis and indicators.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier in format "EXCHANGE:SYMBOL"
|
ticker: Market identifier in format "MARKET.EXCHANGE"
|
||||||
Examples: "BINANCE:BTC/USDT", "COINBASE:ETH/USD"
|
Examples: "BTC/USDT.BINANCE", "ETH/USD.COINBASE"
|
||||||
period_seconds: OHLC candle period in seconds
|
period_seconds: OHLC candle period in seconds
|
||||||
Common values: 60 (1m), 300 (5m), 900 (15m), 3600 (1h),
|
Common values: 60 (1m), 300 (5m), 900 (15m), 3600 (1h),
|
||||||
86400 (1d), 604800 (1w)
|
86400 (1d), 604800 (1w)
|
||||||
@@ -213,14 +213,14 @@ class DataAPI(ABC):
|
|||||||
Examples:
|
Examples:
|
||||||
# Get the last candle
|
# Get the last candle
|
||||||
df = await api.latest_ohlc(
|
df = await api.latest_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600
|
period_seconds=3600
|
||||||
)
|
)
|
||||||
# Returns: timestamp, open, high, low, close
|
# Returns: timestamp, open, high, low, close
|
||||||
|
|
||||||
# Get the last 50 5-minute candles with volume
|
# Get the last 50 5-minute candles with volume
|
||||||
df = await api.latest_ohlc(
|
df = await api.latest_ohlc(
|
||||||
ticker="COINBASE:ETH/USD",
|
ticker="ETH/USD.COINBASE",
|
||||||
period_seconds=300,
|
period_seconds=300,
|
||||||
length=50,
|
length=50,
|
||||||
extra_columns=["volume", "buy_vol", "sell_vol"]
|
extra_columns=["volume", "buy_vol", "sell_vol"]
|
||||||
@@ -228,7 +228,7 @@ class DataAPI(ABC):
|
|||||||
|
|
||||||
# Get recent candles with all timing data
|
# Get recent candles with all timing data
|
||||||
df = await api.latest_ohlc(
|
df = await api.latest_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=60,
|
period_seconds=60,
|
||||||
length=100,
|
length=100,
|
||||||
extra_columns=["open_time", "high_time", "low_time", "close_time"]
|
extra_columns=["open_time", "high_time", "low_time", "close_time"]
|
||||||
@@ -451,7 +451,7 @@ def get_api() -> API:
|
|||||||
|
|
||||||
# Fetch data
|
# Fetch data
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21"
|
end_time="2021-12-21"
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ import asyncio
|
|||||||
api = get_api()
|
api = get_api()
|
||||||
|
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2024-01-01",
|
start_time="2024-01-01",
|
||||||
end_time="2024-01-08",
|
end_time="2024-01-08",
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ api = get_api()
|
|||||||
|
|
||||||
# Method 1: Using Unix timestamps (seconds)
|
# Method 1: Using Unix timestamps (seconds)
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600, # 1 hour candles
|
period_seconds=3600, # 1 hour candles
|
||||||
start_time=1640000000, # Unix timestamp in seconds
|
start_time=1640000000, # Unix timestamp in seconds
|
||||||
end_time=1640086400,
|
end_time=1640086400,
|
||||||
@@ -38,7 +38,7 @@ df = asyncio.run(api.data.historical_ohlc(
|
|||||||
|
|
||||||
# Method 2: Using date strings
|
# Method 2: Using date strings
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20", # Simple date string
|
start_time="2021-12-20", # Simple date string
|
||||||
end_time="2021-12-21",
|
end_time="2021-12-21",
|
||||||
@@ -47,7 +47,7 @@ df = asyncio.run(api.data.historical_ohlc(
|
|||||||
|
|
||||||
# Method 3: Using date strings with time
|
# Method 3: Using date strings with time
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20 00:00:00",
|
start_time="2021-12-20 00:00:00",
|
||||||
end_time="2021-12-20 23:59:59",
|
end_time="2021-12-20 23:59:59",
|
||||||
@@ -56,7 +56,7 @@ df = asyncio.run(api.data.historical_ohlc(
|
|||||||
|
|
||||||
# Method 4: Using datetime objects
|
# Method 4: Using datetime objects
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time=datetime(2021, 12, 20),
|
start_time=datetime(2021, 12, 20),
|
||||||
end_time=datetime(2021, 12, 21),
|
end_time=datetime(2021, 12, 21),
|
||||||
@@ -92,7 +92,7 @@ api = get_api()
|
|||||||
|
|
||||||
# Fetch data
|
# Fetch data
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21",
|
end_time="2021-12-21",
|
||||||
@@ -123,7 +123,7 @@ api = get_api()
|
|||||||
|
|
||||||
# Fetch data
|
# Fetch data
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21"
|
end_time="2021-12-21"
|
||||||
@@ -191,7 +191,7 @@ api = get_api()
|
|||||||
|
|
||||||
# Fetch historical data using date strings (easiest for research)
|
# Fetch historical data using date strings (easiest for research)
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600, # 1 hour
|
period_seconds=3600, # 1 hour
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21",
|
end_time="2021-12-21",
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ export class SymbolRoutes {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Resolve symbol (use wildcard to capture ticker with slashes like BINANCE:BTC/USDT)
|
// Resolve symbol (use wildcard to capture ticker with slashes like BTC/USDT.BINANCE)
|
||||||
app.get('/symbols/*', async (request, reply) => {
|
app.get('/symbols/*', async (request, reply) => {
|
||||||
const symbolIndexService = this.getSymbolIndexService();
|
const symbolIndexService = this.getSymbolIndexService();
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import type {
|
|||||||
TradingViewBar,
|
TradingViewBar,
|
||||||
} from '../types/ohlc.js';
|
} from '../types/ohlc.js';
|
||||||
import {
|
import {
|
||||||
secondsToMicros,
|
secondsToNanos,
|
||||||
backendToTradingView,
|
backendToTradingView,
|
||||||
DEFAULT_SUPPORTED_RESOLUTIONS,
|
DEFAULT_SUPPORTED_RESOLUTIONS,
|
||||||
} from '../types/ohlc.js';
|
} from '../types/ohlc.js';
|
||||||
@@ -79,19 +79,19 @@ export class OHLCService {
|
|||||||
countback,
|
countback,
|
||||||
}, 'Fetching OHLC data');
|
}, 'Fetching OHLC data');
|
||||||
|
|
||||||
// Convert times to microseconds, then align to period boundaries using
|
// Convert times to nanoseconds, then align to period boundaries using
|
||||||
// [ceil(start), ceil(end)) semantics:
|
// [ceil(start), ceil(end)) semantics:
|
||||||
// - start: ceil to next period boundary — excludes any in-progress candle whose
|
// - start: ceil to next period boundary — excludes any in-progress candle whose
|
||||||
// official timestamp is before from_time.
|
// official timestamp is before from_time.
|
||||||
// - end: ceil to next period boundary, used as EXCLUSIVE upper bound — includes
|
// - end: ceil to next period boundary, used as EXCLUSIVE upper bound — includes
|
||||||
// the last candle whose timestamp < to_time, excludes one sitting exactly on
|
// the last candle whose timestamp < to_time, excludes one sitting exactly on
|
||||||
// to_time (which would be the next candle, not yet started).
|
// to_time (which would be the next candle, not yet started).
|
||||||
const periodMicros = BigInt(period_seconds) * 1_000_000n;
|
const periodNanos = BigInt(period_seconds) * 1_000_000_000n;
|
||||||
const raw_start = secondsToMicros(from_time);
|
const raw_start = secondsToNanos(from_time);
|
||||||
const raw_end = secondsToMicros(to_time);
|
const raw_end = secondsToNanos(to_time);
|
||||||
// bigint ceiling: ceil(a/b)*b = ((a + b - 1) / b) * b
|
// bigint ceiling: ceil(a/b)*b = ((a + b - 1) / b) * b
|
||||||
const start_time = ((raw_start + periodMicros - 1n) / periodMicros) * periodMicros;
|
const start_time = ((raw_start + periodNanos - 1n) / periodNanos) * periodNanos;
|
||||||
const end_time = ((raw_end + periodMicros - 1n) / periodMicros) * periodMicros; // exclusive
|
const end_time = ((raw_end + periodNanos - 1n) / periodNanos) * periodNanos; // exclusive
|
||||||
|
|
||||||
// Step 1: Check Iceberg for existing data
|
// Step 1: Check Iceberg for existing data
|
||||||
let data = await this.icebergClient.queryOHLC(ticker, period_seconds, start_time, end_time);
|
let data = await this.icebergClient.queryOHLC(ticker, period_seconds, start_time, end_time);
|
||||||
@@ -220,11 +220,11 @@ export class OHLCService {
|
|||||||
// For now, return default symbol if query matches
|
// For now, return default symbol if query matches
|
||||||
if (query.toLowerCase().includes('btc') || query.toLowerCase().includes('binance')) {
|
if (query.toLowerCase().includes('btc') || query.toLowerCase().includes('binance')) {
|
||||||
return [{
|
return [{
|
||||||
symbol: 'BINANCE:BTC/USDT',
|
symbol: 'BTC/USDT',
|
||||||
full_name: 'BINANCE:BTC/USDT',
|
full_name: 'BTC/USDT (BINANCE)',
|
||||||
description: 'Bitcoin / Tether USD',
|
description: 'Bitcoin / Tether USD',
|
||||||
exchange: 'BINANCE',
|
exchange: 'BINANCE',
|
||||||
ticker: 'BINANCE:BTC/USDT',
|
ticker: 'BTC/USDT.BINANCE',
|
||||||
type: 'crypto',
|
type: 'crypto',
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -241,12 +241,12 @@ export class OHLCService {
|
|||||||
this.logger.debug({ symbol }, 'Resolving symbol');
|
this.logger.debug({ symbol }, 'Resolving symbol');
|
||||||
|
|
||||||
// TODO: Implement central symbol registry
|
// TODO: Implement central symbol registry
|
||||||
// For now, return default symbol info for BINANCE:BTC/USDT
|
// For now, return default symbol info for BTC/USDT.BINANCE
|
||||||
if (symbol === 'BINANCE:BTC/USDT' || symbol === 'BTC/USDT') {
|
if (symbol === 'BTC/USDT.BINANCE' || symbol === 'BTC/USDT') {
|
||||||
return {
|
return {
|
||||||
symbol: 'BINANCE:BTC/USDT',
|
symbol: 'BTC/USDT',
|
||||||
name: 'BINANCE:BTC/USDT',
|
name: 'BTC/USDT',
|
||||||
ticker: 'BINANCE:BTC/USDT',
|
ticker: 'BTC/USDT.BINANCE',
|
||||||
description: 'Bitcoin / Tether USD',
|
description: 'Bitcoin / Tether USD',
|
||||||
type: 'crypto',
|
type: 'crypto',
|
||||||
session: '24x7',
|
session: '24x7',
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export interface SymbolIndexServiceConfig {
|
|||||||
export class SymbolIndexService {
|
export class SymbolIndexService {
|
||||||
private icebergClient: IcebergClient;
|
private icebergClient: IcebergClient;
|
||||||
private logger: FastifyBaseLogger;
|
private logger: FastifyBaseLogger;
|
||||||
private symbols: Map<string, SymbolMetadata> = new Map(); // key: "EXCHANGE:MARKET_ID"
|
private symbols: Map<string, SymbolMetadata> = new Map(); // key: "MARKET_ID.EXCHANGE" (Nautilus format)
|
||||||
private initialized: boolean = false;
|
private initialized: boolean = false;
|
||||||
|
|
||||||
constructor(config: SymbolIndexServiceConfig) {
|
constructor(config: SymbolIndexServiceConfig) {
|
||||||
@@ -52,7 +52,7 @@ export class SymbolIndexService {
|
|||||||
const uniqueKeys = new Set<string>();
|
const uniqueKeys = new Set<string>();
|
||||||
|
|
||||||
for (const symbol of symbols) {
|
for (const symbol of symbols) {
|
||||||
const key = `${symbol.exchange_id}:${symbol.market_id}`;
|
const key = `${symbol.market_id}.${symbol.exchange_id}`;
|
||||||
uniqueKeys.add(key);
|
uniqueKeys.add(key);
|
||||||
this.symbols.set(key, symbol);
|
this.symbols.set(key, symbol);
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ export class SymbolIndexService {
|
|||||||
* Update or add a symbol to the index
|
* Update or add a symbol to the index
|
||||||
*/
|
*/
|
||||||
updateSymbol(symbol: SymbolMetadata): void {
|
updateSymbol(symbol: SymbolMetadata): void {
|
||||||
const key = `${symbol.exchange_id}:${symbol.market_id}`;
|
const key = `${symbol.market_id}.${symbol.exchange_id}`;
|
||||||
this.symbols.set(key, symbol);
|
this.symbols.set(key, symbol);
|
||||||
this.logger.debug({ key }, 'Updated symbol in index');
|
this.logger.debug({ key }, 'Updated symbol in index');
|
||||||
}
|
}
|
||||||
@@ -149,11 +149,11 @@ export class SymbolIndexService {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ticker format: "EXCHANGE:MARKET_ID" or just "MARKET_ID"
|
// ticker format: "MARKET_ID.EXCHANGE" (Nautilus) or just "MARKET_ID"
|
||||||
let key = ticker;
|
let key = ticker;
|
||||||
|
|
||||||
// If no exchange prefix, search for first match
|
// If no dot separator, search for first match by market_id
|
||||||
if (!ticker.includes(':')) {
|
if (!ticker.includes('.')) {
|
||||||
for (const [k, metadata] of this.symbols) {
|
for (const [k, metadata] of this.symbols) {
|
||||||
if (metadata.market_id === ticker) {
|
if (metadata.market_id === ticker) {
|
||||||
key = k;
|
key = k;
|
||||||
@@ -176,7 +176,7 @@ export class SymbolIndexService {
|
|||||||
*/
|
*/
|
||||||
private metadataToSearchResult(metadata: SymbolMetadata): SearchResult {
|
private metadataToSearchResult(metadata: SymbolMetadata): SearchResult {
|
||||||
const symbol = metadata.market_id; // Clean format: "BTC/USDT"
|
const symbol = metadata.market_id; // Clean format: "BTC/USDT"
|
||||||
const ticker = `${metadata.exchange_id}:${metadata.market_id}`; // "BINANCE:BTC/USDT"
|
const ticker = `${metadata.market_id}.${metadata.exchange_id}`; // "BTC/USDT.BINANCE"
|
||||||
const fullName = `${metadata.market_id} (${metadata.exchange_id})`;
|
const fullName = `${metadata.market_id} (${metadata.exchange_id})`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -194,15 +194,12 @@ export class SymbolIndexService {
|
|||||||
*/
|
*/
|
||||||
private metadataToSymbolInfo(metadata: SymbolMetadata): SymbolInfo {
|
private metadataToSymbolInfo(metadata: SymbolMetadata): SymbolInfo {
|
||||||
const symbol = metadata.market_id;
|
const symbol = metadata.market_id;
|
||||||
const ticker = `${metadata.exchange_id}:${metadata.market_id}`;
|
const ticker = `${metadata.market_id}.${metadata.exchange_id}`; // "BTC/USDT.BINANCE"
|
||||||
|
|
||||||
// Convert supported_period_seconds to resolution strings
|
|
||||||
const supportedResolutions = this.periodSecondsToResolutions(metadata.supported_period_seconds || []);
|
const supportedResolutions = this.periodSecondsToResolutions(metadata.supported_period_seconds || []);
|
||||||
|
|
||||||
// Calculate pricescale from tick_denom
|
// pricescale = 10^price_precision (e.g., price_precision=2 → pricescale=100)
|
||||||
// tick_denom is 10^n where n is the number of decimal places
|
const pricescale = metadata.price_precision != null ? Math.pow(10, metadata.price_precision) : 100;
|
||||||
// pricescale is the same value
|
|
||||||
const pricescale = metadata.tick_denom ? Number(metadata.tick_denom) : 100;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
symbol,
|
symbol,
|
||||||
@@ -222,9 +219,12 @@ export class SymbolIndexService {
|
|||||||
base_currency: metadata.base_asset,
|
base_currency: metadata.base_asset,
|
||||||
quote_currency: metadata.quote_asset,
|
quote_currency: metadata.quote_asset,
|
||||||
data_status: 'streaming',
|
data_status: 'streaming',
|
||||||
tick_denominator: metadata.tick_denom ? Number(metadata.tick_denom) : undefined,
|
price_precision: metadata.price_precision,
|
||||||
base_denominator: metadata.base_denom ? Number(metadata.base_denom) : undefined,
|
size_precision: metadata.size_precision,
|
||||||
quote_denominator: metadata.quote_denom ? Number(metadata.quote_denom) : undefined,
|
tick_size: metadata.tick_size,
|
||||||
|
lot_size: metadata.lot_size,
|
||||||
|
maker_fee: metadata.maker_fee,
|
||||||
|
taker_fee: metadata.taker_fee,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ async function getChartState(workspaceManager: WorkspaceManager, logger: Fastify
|
|||||||
if (!chartState) {
|
if (!chartState) {
|
||||||
// Return default chart state
|
// Return default chart state
|
||||||
return {
|
return {
|
||||||
symbol: 'BINANCE:BTC/USDT',
|
symbol: 'BTC/USDT.BINANCE',
|
||||||
start_time: null,
|
start_time: null,
|
||||||
end_time: null,
|
end_time: null,
|
||||||
period: 900,
|
period: 900,
|
||||||
@@ -177,7 +177,7 @@ async function getChartState(workspaceManager: WorkspaceManager, logger: Fastify
|
|||||||
logger.error({ error }, 'Failed to get chart state from workspace');
|
logger.error({ error }, 'Failed to get chart state from workspace');
|
||||||
// Return default chart state
|
// Return default chart state
|
||||||
return {
|
return {
|
||||||
symbol: 'BINANCE:BTC/USDT',
|
symbol: 'BTC/USDT.BINANCE',
|
||||||
start_time: null,
|
start_time: null,
|
||||||
end_time: null,
|
end_time: null,
|
||||||
period: 900,
|
period: 900,
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
*
|
*
|
||||||
* Handles conversion between:
|
* Handles conversion between:
|
||||||
* - TradingView datafeed format (seconds, OHLCV structure)
|
* - TradingView datafeed format (seconds, OHLCV structure)
|
||||||
* - Backend/Iceberg format (microseconds, ticker prefix)
|
* - Backend/Iceberg format (nanoseconds, Nautilus ticker)
|
||||||
* - ZMQ protocol format (protobuf messages)
|
* - ZMQ protocol format (protobuf messages)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@@ -31,8 +31,8 @@ export interface TradingViewBar {
|
|||||||
* Backend OHLC format (from Iceberg)
|
* Backend OHLC format (from Iceberg)
|
||||||
*/
|
*/
|
||||||
export interface BackendOHLC {
|
export interface BackendOHLC {
|
||||||
timestamp: bigint; // Unix timestamp in MICROSECONDS — kept as bigint to preserve precision
|
timestamp: bigint; // Unix timestamp in NANOSECONDS — kept as bigint to preserve precision
|
||||||
ticker: string;
|
ticker: string; // Nautilus format: "BTC/USDT.BINANCE"
|
||||||
period_seconds: number;
|
period_seconds: number;
|
||||||
open: number | null; // null for gap bars (no trades that period)
|
open: number | null; // null for gap bars (no trades that period)
|
||||||
high: number | null;
|
high: number | null;
|
||||||
@@ -59,7 +59,7 @@ export interface DatafeedConfig {
|
|||||||
*/
|
*/
|
||||||
export interface SymbolInfo {
|
export interface SymbolInfo {
|
||||||
symbol: string; // Clean format (e.g., "BTC/USDT")
|
symbol: string; // Clean format (e.g., "BTC/USDT")
|
||||||
ticker: string; // With exchange prefix (e.g., "BINANCE:BTC/USDT")
|
ticker: string; // Nautilus format (e.g., "BTC/USDT.BINANCE")
|
||||||
name: string; // Display name
|
name: string; // Display name
|
||||||
description: string; // Human-readable description
|
description: string; // Human-readable description
|
||||||
type: string; // "crypto", "spot", "futures", etc.
|
type: string; // "crypto", "spot", "futures", etc.
|
||||||
@@ -70,14 +70,18 @@ export interface SymbolInfo {
|
|||||||
has_intraday: boolean;
|
has_intraday: boolean;
|
||||||
has_daily: boolean;
|
has_daily: boolean;
|
||||||
has_weekly_and_monthly: boolean;
|
has_weekly_and_monthly: boolean;
|
||||||
pricescale: number; // Price scale factor
|
pricescale: number; // 10^price_precision
|
||||||
minmov: number; // Minimum price movement
|
minmov: number; // Minimum price movement
|
||||||
base_currency?: string; // Base asset (e.g., "BTC")
|
base_currency?: string; // Base asset (e.g., "BTC")
|
||||||
quote_currency?: string; // Quote asset (e.g., "USDT")
|
quote_currency?: string; // Quote asset (e.g., "USDT")
|
||||||
data_status?: string; // "streaming", "delayed", etc.
|
data_status?: string; // "streaming", "delayed", etc.
|
||||||
tick_denominator?: number; // Denominator for price scaling (e.g., 1e6)
|
// Nautilus Instrument fields
|
||||||
base_denominator?: number; // Denominator for base asset
|
price_precision?: number;
|
||||||
quote_denominator?: number; // Denominator for quote asset
|
size_precision?: number;
|
||||||
|
tick_size?: number;
|
||||||
|
lot_size?: number;
|
||||||
|
maker_fee?: number;
|
||||||
|
taker_fee?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,7 +99,7 @@ export interface HistoryResult {
|
|||||||
*/
|
*/
|
||||||
export interface SearchResult {
|
export interface SearchResult {
|
||||||
symbol: string; // Clean format (e.g., "BTC/USDT")
|
symbol: string; // Clean format (e.g., "BTC/USDT")
|
||||||
ticker: string; // With exchange prefix for routing (e.g., "BINANCE:BTC/USDT")
|
ticker: string; // Nautilus format (e.g., "BTC/USDT.BINANCE")
|
||||||
full_name: string; // Full display name (e.g., "BTC/USDT (BINANCE)")
|
full_name: string; // Full display name (e.g., "BTC/USDT (BINANCE)")
|
||||||
description: string; // Human-readable description
|
description: string; // Human-readable description
|
||||||
exchange: string; // Exchange identifier
|
exchange: string; // Exchange identifier
|
||||||
@@ -122,9 +126,9 @@ export enum NotificationStatus {
|
|||||||
|
|
||||||
export interface SubmitHistoricalRequest {
|
export interface SubmitHistoricalRequest {
|
||||||
request_id: string;
|
request_id: string;
|
||||||
ticker: string;
|
ticker: string; // Nautilus format: "BTC/USDT.BINANCE"
|
||||||
start_time: bigint; // microseconds
|
start_time: bigint; // nanoseconds
|
||||||
end_time: bigint; // microseconds
|
end_time: bigint; // nanoseconds
|
||||||
period_seconds: number;
|
period_seconds: number;
|
||||||
limit?: number;
|
limit?: number;
|
||||||
client_id?: string;
|
client_id?: string;
|
||||||
@@ -139,34 +143,33 @@ export interface SubmitResponse {
|
|||||||
|
|
||||||
export interface HistoryReadyNotification {
|
export interface HistoryReadyNotification {
|
||||||
request_id: string;
|
request_id: string;
|
||||||
ticker: string;
|
ticker: string; // Nautilus format: "BTC/USDT.BINANCE"
|
||||||
period_seconds: number;
|
period_seconds: number;
|
||||||
start_time: bigint; // microseconds
|
start_time: bigint; // nanoseconds
|
||||||
end_time: bigint; // microseconds
|
end_time: bigint; // nanoseconds
|
||||||
status: NotificationStatus;
|
status: NotificationStatus;
|
||||||
error_message?: string;
|
error_message?: string;
|
||||||
iceberg_namespace: string;
|
iceberg_namespace: string;
|
||||||
iceberg_table: string;
|
iceberg_table: string;
|
||||||
row_count: number;
|
row_count: number;
|
||||||
completed_at: bigint; // microseconds
|
completed_at: bigint; // nanoseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conversion utilities
|
* Conversion utilities
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export function secondsToMicros(seconds: number): bigint {
|
export function secondsToNanos(seconds: number): bigint {
|
||||||
return BigInt(Math.floor(seconds)) * 1000000n;
|
return BigInt(Math.floor(seconds)) * 1_000_000_000n;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function microsToSeconds(micros: bigint | number): number {
|
export function nanosToSeconds(nanos: bigint | number): number {
|
||||||
// Integer division: convert microseconds to seconds (truncates to integer)
|
return Number(BigInt(nanos) / 1_000_000_000n);
|
||||||
return Number(BigInt(micros) / 1000000n);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function backendToTradingView(backend: BackendOHLC): TradingViewBar {
|
export function backendToTradingView(backend: BackendOHLC): TradingViewBar {
|
||||||
return {
|
return {
|
||||||
time: microsToSeconds(backend.timestamp),
|
time: nanosToSeconds(backend.timestamp),
|
||||||
open: backend.open,
|
open: backend.open,
|
||||||
high: backend.high,
|
high: backend.high,
|
||||||
low: backend.low,
|
low: backend.low,
|
||||||
@@ -220,10 +223,18 @@ export interface SymbolMetadata {
|
|||||||
description?: string;
|
description?: string;
|
||||||
base_asset?: string;
|
base_asset?: string;
|
||||||
quote_asset?: string;
|
quote_asset?: string;
|
||||||
tick_denom?: bigint;
|
|
||||||
base_denom?: bigint;
|
|
||||||
quote_denom?: bigint;
|
|
||||||
supported_period_seconds?: number[];
|
supported_period_seconds?: number[];
|
||||||
earliest_time?: bigint;
|
earliest_time?: bigint; // nanoseconds
|
||||||
updated_at: bigint;
|
updated_at: bigint; // nanoseconds
|
||||||
|
// Nautilus Instrument fields
|
||||||
|
price_precision?: number;
|
||||||
|
size_precision?: number;
|
||||||
|
tick_size?: number;
|
||||||
|
lot_size?: number;
|
||||||
|
min_notional?: number;
|
||||||
|
margin_init?: number;
|
||||||
|
margin_maint?: number;
|
||||||
|
maker_fee?: number;
|
||||||
|
taker_fee?: number;
|
||||||
|
contract_multiplier?: number;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ export const DEFAULT_STORES: StoreConfig[] = [
|
|||||||
name: 'chartState',
|
name: 'chartState',
|
||||||
persistent: false,
|
persistent: false,
|
||||||
initialState: () => ({
|
initialState: () => ({
|
||||||
symbol: 'BINANCE:BTC/USDT',
|
symbol: 'BTC/USDT.BINANCE',
|
||||||
start_time: null,
|
start_time: null,
|
||||||
end_time: null,
|
end_time: null,
|
||||||
period: '15',
|
period: '15',
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ Historical OHLC (Open, High, Low, Close, Volume) candle data for all periods in
|
|||||||
```sql
|
```sql
|
||||||
-- Query 1-hour candles for specific ticker and time range
|
-- Query 1-hour candles for specific ticker and time range
|
||||||
SELECT * FROM trading.ohlc
|
SELECT * FROM trading.ohlc
|
||||||
WHERE ticker = 'BINANCE:BTC/USDT'
|
WHERE ticker = 'BTC/USDT.BINANCE'
|
||||||
AND period_seconds = 3600
|
AND period_seconds = 3600
|
||||||
AND timestamp BETWEEN 1735689600000000 AND 1736294399000000
|
AND timestamp BETWEEN 1735689600000000 AND 1736294399000000
|
||||||
ORDER BY timestamp;
|
ORDER BY timestamp;
|
||||||
|
|
||||||
-- Query most recent 1-minute candles
|
-- Query most recent 1-minute candles
|
||||||
SELECT * FROM trading.ohlc
|
SELECT * FROM trading.ohlc
|
||||||
WHERE ticker = 'BINANCE:BTC/USDT'
|
WHERE ticker = 'BTC/USDT.BINANCE'
|
||||||
AND period_seconds = 60
|
AND period_seconds = 60
|
||||||
AND timestamp > (UNIX_MICROS(CURRENT_TIMESTAMP()) - 3600000000)
|
AND timestamp > (UNIX_MICROS(CURRENT_TIMESTAMP()) - 3600000000)
|
||||||
ORDER BY timestamp DESC
|
ORDER BY timestamp DESC
|
||||||
@@ -53,7 +53,7 @@ LIMIT 60;
|
|||||||
-- Query all periods for a ticker
|
-- Query all periods for a ticker
|
||||||
SELECT period_seconds, COUNT(*) as candle_count
|
SELECT period_seconds, COUNT(*) as candle_count
|
||||||
FROM trading.ohlc
|
FROM trading.ohlc
|
||||||
WHERE ticker = 'BINANCE:BTC/USDT'
|
WHERE ticker = 'BTC/USDT.BINANCE'
|
||||||
GROUP BY period_seconds;
|
GROUP BY period_seconds;
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ table = catalog.load_table("trading.ohlc")
|
|||||||
# Query with filters
|
# Query with filters
|
||||||
df = table.scan(
|
df = table.scan(
|
||||||
row_filter=(
|
row_filter=(
|
||||||
(col("ticker") == "BINANCE:BTC/USDT") &
|
(col("ticker") == "BTC/USDT.BINANCE") &
|
||||||
(col("period_seconds") == 3600) &
|
(col("period_seconds") == 3600) &
|
||||||
(col("timestamp") >= 1735689600000000)
|
(col("timestamp") >= 1735689600000000)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS trading.ohlc (
|
CREATE TABLE IF NOT EXISTS trading.ohlc (
|
||||||
-- Natural key fields (uniqueness enforced by Flink upsert logic)
|
-- Natural key fields (uniqueness enforced by Flink upsert logic)
|
||||||
ticker STRING NOT NULL COMMENT 'Market identifier (e.g., BINANCE:BTC/USDT)',
|
ticker STRING NOT NULL COMMENT 'Market identifier (e.g., BTC/USDT.BINANCE)',
|
||||||
period_seconds INT NOT NULL COMMENT 'OHLC period in seconds (60, 300, 900, 3600, 14400, 86400, 604800, etc.)',
|
period_seconds INT NOT NULL COMMENT 'OHLC period in seconds (60, 300, 900, 3600, 14400, 86400, 604800, etc.)',
|
||||||
timestamp BIGINT NOT NULL COMMENT 'Candle timestamp in microseconds since epoch',
|
timestamp BIGINT NOT NULL COMMENT 'Candle timestamp in microseconds since epoch',
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ docker run -v /path/to/config:/config ccxt-ingestor
|
|||||||
Tickers must be in the format: `EXCHANGE:SYMBOL`
|
Tickers must be in the format: `EXCHANGE:SYMBOL`
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
- `BINANCE:BTC/USDT`
|
- `BTC/USDT.BINANCE`
|
||||||
- `COINBASE:ETH/USD`
|
- `COINBASE:ETH/USD`
|
||||||
- `KRAKEN:XRP/EUR`
|
- `KRAKEN:XRP/EUR`
|
||||||
|
|
||||||
|
|||||||
@@ -12,17 +12,17 @@ export class CCXTFetcher {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse ticker string to exchange and symbol
|
* Parse ticker string to exchange and symbol
|
||||||
* Expected format: "EXCHANGE:SYMBOL" (e.g., "BINANCE:BTC/USDT")
|
* Expected format: "SYMBOL.EXCHANGE" (e.g., "BTC/USDT.BINANCE")
|
||||||
*/
|
*/
|
||||||
parseTicker(ticker) {
|
parseTicker(ticker) {
|
||||||
const parts = ticker.split(':');
|
const lastDot = ticker.lastIndexOf('.');
|
||||||
if (parts.length !== 2) {
|
if (lastDot === -1) {
|
||||||
throw new Error(`Invalid ticker format: ${ticker}. Expected "EXCHANGE:SYMBOL"`);
|
throw new Error(`Invalid ticker format: ${ticker}. Expected "SYMBOL.EXCHANGE"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exchange: parts[0].toLowerCase(),
|
exchange: ticker.slice(lastDot + 1).toLowerCase(),
|
||||||
symbol: parts[1]
|
symbol: ticker.slice(0, lastDot)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,9 +101,9 @@ export class CCXTFetcher {
|
|||||||
const { exchange: exchangeName, symbol } = this.parseTicker(ticker);
|
const { exchange: exchangeName, symbol } = this.parseTicker(ticker);
|
||||||
const exchange = this.getExchange(exchangeName);
|
const exchange = this.getExchange(exchangeName);
|
||||||
|
|
||||||
// Convert microseconds to milliseconds
|
// Convert nanoseconds to milliseconds
|
||||||
const startMs = Math.floor(parseInt(startTime) / 1000);
|
const startMs = Math.floor(parseInt(startTime) / 1_000_000);
|
||||||
const endMs = Math.floor(parseInt(endTime) / 1000);
|
const endMs = Math.floor(parseInt(endTime) / 1_000_000);
|
||||||
|
|
||||||
// Map period seconds to CCXT timeframe
|
// Map period seconds to CCXT timeframe
|
||||||
const timeframe = this.secondsToTimeframe(periodSeconds);
|
const timeframe = this.secondsToTimeframe(periodSeconds);
|
||||||
@@ -208,7 +208,7 @@ export class CCXTFetcher {
|
|||||||
/**
|
/**
|
||||||
* Fetch recent trades for realtime tick data
|
* Fetch recent trades for realtime tick data
|
||||||
* @param {string} ticker - Ticker in format "EXCHANGE:SYMBOL"
|
* @param {string} ticker - Ticker in format "EXCHANGE:SYMBOL"
|
||||||
* @param {string} since - Optional timestamp in microseconds to fetch from
|
* @param {string} since - Optional timestamp in nanoseconds to fetch from
|
||||||
* @returns {Promise<Array>} Array of trade ticks
|
* @returns {Promise<Array>} Array of trade ticks
|
||||||
*/
|
*/
|
||||||
async fetchRecentTrades(ticker, since = null) {
|
async fetchRecentTrades(ticker, since = null) {
|
||||||
@@ -216,8 +216,8 @@ export class CCXTFetcher {
|
|||||||
const exchange = this.getExchange(exchangeName);
|
const exchange = this.getExchange(exchangeName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Convert microseconds to milliseconds if provided
|
// Convert nanoseconds to milliseconds if provided
|
||||||
const sinceMs = since ? Math.floor(parseInt(since) / 1000) : undefined;
|
const sinceMs = since ? Math.floor(parseInt(since) / 1_000_000) : undefined;
|
||||||
|
|
||||||
const trades = await exchange.fetchTrades(symbol, sinceMs, 1000);
|
const trades = await exchange.fetchTrades(symbol, sinceMs, 1000);
|
||||||
|
|
||||||
@@ -243,25 +243,24 @@ export class CCXTFetcher {
|
|||||||
/**
|
/**
|
||||||
* Convert CCXT OHLCV array to our OHLC format
|
* Convert CCXT OHLCV array to our OHLC format
|
||||||
* CCXT format: [timestamp, open, high, low, close, volume]
|
* CCXT format: [timestamp, open, high, low, close, volume]
|
||||||
* Uses denominators from market metadata for proper integer representation
|
* Uses precision fields from market metadata for proper integer representation
|
||||||
*/
|
*/
|
||||||
convertToOHLC(candle, ticker, periodSeconds, metadata) {
|
convertToOHLC(candle, ticker, periodSeconds, metadata) {
|
||||||
const [timestamp, open, high, low, close, volume] = candle;
|
const [timestamp, open, high, low, close, volume] = candle;
|
||||||
|
|
||||||
// Use denominators from metadata
|
const priceMult = Math.pow(10, metadata.pricePrecision ?? 2);
|
||||||
const tickDenom = metadata.tickDenom || 100;
|
const sizeMult = Math.pow(10, metadata.sizePrecision ?? 8);
|
||||||
const baseDenom = metadata.baseDenom || 100000000;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ticker,
|
ticker,
|
||||||
timestamp: (timestamp * 1000).toString(), // Convert ms to microseconds
|
timestamp: (timestamp * 1_000_000).toString(), // Convert ms to nanoseconds
|
||||||
open: Math.round(open * tickDenom).toString(),
|
open: Math.round(open * priceMult).toString(),
|
||||||
high: Math.round(high * tickDenom).toString(),
|
high: Math.round(high * priceMult).toString(),
|
||||||
low: Math.round(low * tickDenom).toString(),
|
low: Math.round(low * priceMult).toString(),
|
||||||
close: Math.round(close * tickDenom).toString(),
|
close: Math.round(close * priceMult).toString(),
|
||||||
volume: Math.round(volume * baseDenom).toString(),
|
volume: Math.round(volume * sizeMult).toString(),
|
||||||
open_time: (timestamp * 1000).toString(),
|
open_time: (timestamp * 1_000_000).toString(),
|
||||||
close_time: ((timestamp + periodSeconds * 1000) * 1000).toString()
|
close_time: ((timestamp + periodSeconds * 1000) * 1_000_000).toString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -272,35 +271,33 @@ export class CCXTFetcher {
|
|||||||
createGapBar(timestampMs, ticker, periodSeconds, metadata) {
|
createGapBar(timestampMs, ticker, periodSeconds, metadata) {
|
||||||
return {
|
return {
|
||||||
ticker,
|
ticker,
|
||||||
timestamp: (timestampMs * 1000).toString(), // Convert ms to microseconds
|
timestamp: (timestampMs * 1_000_000).toString(), // Convert ms to nanoseconds
|
||||||
open: null,
|
open: null,
|
||||||
high: null,
|
high: null,
|
||||||
low: null,
|
low: null,
|
||||||
close: null,
|
close: null,
|
||||||
volume: null,
|
volume: null,
|
||||||
open_time: (timestampMs * 1000).toString(),
|
open_time: (timestampMs * 1_000_000).toString(),
|
||||||
close_time: ((timestampMs + periodSeconds * 1000) * 1000).toString()
|
close_time: ((timestampMs + periodSeconds * 1000) * 1_000_000).toString()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert CCXT trade to our Tick format
|
* Convert CCXT trade to our Tick format
|
||||||
* Uses denominators from market metadata for proper integer representation
|
* Uses precision fields from market metadata for proper integer representation
|
||||||
*/
|
*/
|
||||||
convertToTick(trade, ticker, metadata) {
|
convertToTick(trade, ticker, metadata) {
|
||||||
// Use denominators from metadata
|
const priceMult = Math.pow(10, metadata.pricePrecision ?? 2);
|
||||||
const tickDenom = metadata.tickDenom || 100;
|
const sizeMult = Math.pow(10, metadata.sizePrecision ?? 8);
|
||||||
const baseDenom = metadata.baseDenom || 100000000;
|
|
||||||
const quoteDenom = metadata.quoteDenom || tickDenom;
|
|
||||||
|
|
||||||
const price = Math.round(trade.price * tickDenom);
|
const price = Math.round(trade.price * priceMult);
|
||||||
const amount = Math.round(trade.amount * baseDenom);
|
const amount = Math.round(trade.amount * sizeMult);
|
||||||
const quoteAmount = Math.round((trade.price * trade.amount) * quoteDenom);
|
const quoteAmount = Math.round((trade.price * trade.amount) * priceMult);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
trade_id: trade.id || `${trade.timestamp}`,
|
trade_id: trade.id || `${trade.timestamp}`,
|
||||||
ticker,
|
ticker,
|
||||||
timestamp: (trade.timestamp * 1000).toString(), // Convert ms to microseconds
|
timestamp: (trade.timestamp * 1_000_000).toString(), // Convert ms to nanoseconds
|
||||||
price: price.toString(),
|
price: price.toString(),
|
||||||
amount: amount.toString(),
|
amount: amount.toString(),
|
||||||
quote_amount: quoteAmount.toString(),
|
quote_amount: quoteAmount.toString(),
|
||||||
|
|||||||
@@ -141,59 +141,50 @@ export class SymbolMetadataGenerator {
|
|||||||
const precision = market.precision || {};
|
const precision = market.precision || {};
|
||||||
const limits = market.limits || {};
|
const limits = market.limits || {};
|
||||||
|
|
||||||
// Get tick_denom from price precision
|
// Derive Nautilus Instrument fields from CCXT market data
|
||||||
// This tells us the denominator for price values.
|
|
||||||
// For example, if BTC/USDT trades with 2 decimals (0.01 precision), tick_denom = 100
|
|
||||||
//
|
//
|
||||||
// CCXT precision.price can be:
|
// CCXT precision.price can be:
|
||||||
// - Integer (decimal places): 2 means 0.01 tick size -> denominator 100
|
// - Integer (decimal places): 2 means 0.01 tick size -> price_precision=2, tick_size=0.01
|
||||||
// - Float (tick size): 0.01 -> invert to get denominator 100
|
// - Float (tick size): 0.01 -> tick_size=0.01, price_precision=2
|
||||||
let tick_denom;
|
let price_precision;
|
||||||
|
let tick_size;
|
||||||
if (precision.price !== undefined) {
|
if (precision.price !== undefined) {
|
||||||
if (Number.isInteger(precision.price)) {
|
if (Number.isInteger(precision.price)) {
|
||||||
// Integer: number of decimal places
|
price_precision = precision.price;
|
||||||
// e.g., precision.price = 2 means 2 decimal places = 0.01 tick = 100 denom
|
tick_size = Math.pow(10, -precision.price);
|
||||||
tick_denom = Math.pow(10, precision.price);
|
|
||||||
} else {
|
} else {
|
||||||
// Float: actual tick size, need to invert and round
|
tick_size = precision.price;
|
||||||
// e.g., precision.price = 0.01 -> 1/0.01 = 100
|
price_precision = Math.round(-Math.log10(precision.price));
|
||||||
tick_denom = Math.round(1 / precision.price);
|
|
||||||
}
|
}
|
||||||
} else if (limits.price?.min !== undefined) {
|
} else if (limits.price?.min !== undefined) {
|
||||||
// Fallback: use minimum price as tick size
|
tick_size = limits.price.min;
|
||||||
tick_denom = Math.round(1 / limits.price.min);
|
price_precision = Math.round(-Math.log10(tick_size));
|
||||||
} else {
|
} else {
|
||||||
// Default to 2 decimals (pennies)
|
price_precision = 2;
|
||||||
tick_denom = 100;
|
tick_size = 0.01;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get base_denom from amount precision (for volumes)
|
let size_precision;
|
||||||
let base_denom;
|
let lot_size;
|
||||||
if (precision.amount !== undefined) {
|
if (precision.amount !== undefined) {
|
||||||
if (Number.isInteger(precision.amount)) {
|
if (Number.isInteger(precision.amount)) {
|
||||||
base_denom = Math.pow(10, precision.amount);
|
size_precision = precision.amount;
|
||||||
|
lot_size = Math.pow(10, -precision.amount);
|
||||||
} else {
|
} else {
|
||||||
base_denom = Math.round(1 / precision.amount);
|
lot_size = precision.amount;
|
||||||
|
size_precision = Math.round(-Math.log10(precision.amount));
|
||||||
}
|
}
|
||||||
} else if (limits.amount?.min !== undefined) {
|
} else if (limits.amount?.min !== undefined) {
|
||||||
base_denom = Math.round(1 / limits.amount.min);
|
lot_size = limits.amount.min;
|
||||||
|
size_precision = Math.round(-Math.log10(lot_size));
|
||||||
} else {
|
} else {
|
||||||
// Default to 8 decimals (standard for crypto)
|
size_precision = 8;
|
||||||
base_denom = 100000000;
|
lot_size = 0.00000001;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get quote_denom from cost precision (price * amount)
|
const min_notional = limits.cost?.min || 0;
|
||||||
let quote_denom;
|
const maker_fee = market.maker !== undefined ? market.maker : 0.001;
|
||||||
if (precision.cost !== undefined) {
|
const taker_fee = market.taker !== undefined ? market.taker : 0.001;
|
||||||
if (Number.isInteger(precision.cost)) {
|
|
||||||
quote_denom = Math.pow(10, precision.cost);
|
|
||||||
} else {
|
|
||||||
quote_denom = Math.round(1 / precision.cost);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Default: typically tick_denom for most exchanges
|
|
||||||
quote_denom = tick_denom;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standard supported periods (in seconds)
|
// Standard supported periods (in seconds)
|
||||||
// Most exchanges support these timeframes
|
// Most exchanges support these timeframes
|
||||||
@@ -218,9 +209,14 @@ export class SymbolMetadataGenerator {
|
|||||||
description,
|
description,
|
||||||
baseAsset: base,
|
baseAsset: base,
|
||||||
quoteAsset: quote,
|
quoteAsset: quote,
|
||||||
tickDenom: tick_denom,
|
pricePrecision: price_precision,
|
||||||
baseDenom: base_denom,
|
sizePrecision: size_precision,
|
||||||
quoteDenom: quote_denom,
|
tickSize: tick_size,
|
||||||
|
lotSize: lot_size,
|
||||||
|
minNotional: min_notional,
|
||||||
|
makerFee: maker_fee,
|
||||||
|
takerFee: taker_fee,
|
||||||
|
contractMultiplier: 1.0,
|
||||||
supportedPeriodSeconds: supported_period_seconds,
|
supportedPeriodSeconds: supported_period_seconds,
|
||||||
// earliestTime can be added later if we track it
|
// earliestTime can be added later if we track it
|
||||||
};
|
};
|
||||||
@@ -238,7 +234,7 @@ export class SymbolMetadataGenerator {
|
|||||||
let duplicateCount = 0;
|
let duplicateCount = 0;
|
||||||
|
|
||||||
for (const metadata of metadataList) {
|
for (const metadata of metadataList) {
|
||||||
const key = `${metadata.exchangeId}:${metadata.marketId}`;
|
const key = `${metadata.marketId}.${metadata.exchangeId}`;
|
||||||
|
|
||||||
// Debug first few to understand duplication
|
// Debug first few to understand duplication
|
||||||
if (uniqueMetadata.length < 3 || (uniqueMetadata.length === 0 && duplicateCount < 3)) {
|
if (uniqueMetadata.length < 3 || (uniqueMetadata.length === 0 && duplicateCount < 3)) {
|
||||||
@@ -269,7 +265,7 @@ export class SymbolMetadataGenerator {
|
|||||||
|
|
||||||
// Convert each metadata to protobuf Market message
|
// Convert each metadata to protobuf Market message
|
||||||
const messages = uniqueMetadata.map(metadata => {
|
const messages = uniqueMetadata.map(metadata => {
|
||||||
const key = `${metadata.exchangeId}:${metadata.marketId}`;
|
const key = `${metadata.marketId}.${metadata.exchangeId}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ export class ZmqClient {
|
|||||||
const workEndpoint = `tcp://${flink_hostname}:${ingestor_work_port}`;
|
const workEndpoint = `tcp://${flink_hostname}:${ingestor_work_port}`;
|
||||||
await this.workSocket.connect(workEndpoint);
|
await this.workSocket.connect(workEndpoint);
|
||||||
|
|
||||||
// Subscribe to each supported exchange prefix
|
// Subscribe to each supported exchange suffix (Nautilus format: "BTC/USDT.BINANCE")
|
||||||
for (const exchange of this.supportedExchanges) {
|
for (const exchange of this.supportedExchanges) {
|
||||||
const prefix = `${exchange}:`;
|
const prefix = `${exchange}.`;
|
||||||
this.workSocket.subscribe(prefix);
|
this.workSocket.subscribe(prefix);
|
||||||
this.logger.info(`Subscribed to exchange prefix: ${prefix}`);
|
this.logger.info(`Subscribed to exchange prefix: ${prefix}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ message DataRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
message HistoricalParams {
|
message HistoricalParams {
|
||||||
// Start time (microseconds since epoch)
|
// Start time (nanoseconds since epoch)
|
||||||
uint64 start_time = 1;
|
uint64 start_time = 1;
|
||||||
|
|
||||||
// End time (microseconds since epoch)
|
// End time (nanoseconds since epoch)
|
||||||
uint64 end_time = 2;
|
uint64 end_time = 2;
|
||||||
|
|
||||||
// OHLC period in seconds (e.g., 60 = 1m, 300 = 5m, 3600 = 1h, 86400 = 1d)
|
// OHLC period in seconds (e.g., 60 = 1m, 300 = 5m, 3600 = 1h, 86400 = 1d)
|
||||||
@@ -115,13 +115,13 @@ message SubmitHistoricalRequest {
|
|||||||
// Client-generated request ID for tracking
|
// Client-generated request ID for tracking
|
||||||
string request_id = 1;
|
string request_id = 1;
|
||||||
|
|
||||||
// Market identifier (e.g., "BINANCE:BTC/USDT")
|
// Market identifier in Nautilus format (e.g., "BTC/USDT.BINANCE")
|
||||||
string ticker = 2;
|
string ticker = 2;
|
||||||
|
|
||||||
// Start time (microseconds since epoch)
|
// Start time (nanoseconds since epoch)
|
||||||
uint64 start_time = 3;
|
uint64 start_time = 3;
|
||||||
|
|
||||||
// End time (microseconds since epoch)
|
// End time (nanoseconds since epoch)
|
||||||
uint64 end_time = 4;
|
uint64 end_time = 4;
|
||||||
|
|
||||||
// OHLC period in seconds (e.g., 60 = 1m, 300 = 5m, 3600 = 1h)
|
// OHLC period in seconds (e.g., 60 = 1m, 300 = 5m, 3600 = 1h)
|
||||||
@@ -170,10 +170,10 @@ message HistoryReadyNotification {
|
|||||||
// OHLC period in seconds
|
// OHLC period in seconds
|
||||||
uint32 period_seconds = 3;
|
uint32 period_seconds = 3;
|
||||||
|
|
||||||
// Start time (microseconds since epoch)
|
// Start time (nanoseconds since epoch)
|
||||||
uint64 start_time = 4;
|
uint64 start_time = 4;
|
||||||
|
|
||||||
// End time (microseconds since epoch)
|
// End time (nanoseconds since epoch)
|
||||||
uint64 end_time = 5;
|
uint64 end_time = 5;
|
||||||
|
|
||||||
// Status of the data fetch
|
// Status of the data fetch
|
||||||
@@ -189,7 +189,7 @@ message HistoryReadyNotification {
|
|||||||
// Number of records written
|
// Number of records written
|
||||||
uint32 row_count = 12;
|
uint32 row_count = 12;
|
||||||
|
|
||||||
// Timestamp when data was written (microseconds since epoch)
|
// Timestamp when data was written (nanoseconds since epoch)
|
||||||
uint64 completed_at = 13;
|
uint64 completed_at = 13;
|
||||||
|
|
||||||
enum NotificationStatus {
|
enum NotificationStatus {
|
||||||
@@ -208,10 +208,10 @@ message OHLCRequest {
|
|||||||
// Market identifier
|
// Market identifier
|
||||||
string ticker = 2;
|
string ticker = 2;
|
||||||
|
|
||||||
// Start time (microseconds since epoch)
|
// Start time (nanoseconds since epoch)
|
||||||
uint64 start_time = 3;
|
uint64 start_time = 3;
|
||||||
|
|
||||||
// End time (microseconds since epoch)
|
// End time (nanoseconds since epoch)
|
||||||
uint64 end_time = 4;
|
uint64 end_time = 4;
|
||||||
|
|
||||||
// OHLC period in seconds (e.g., 60 = 1m, 300 = 5m, 3600 = 1h)
|
// OHLC period in seconds (e.g., 60 = 1m, 300 = 5m, 3600 = 1h)
|
||||||
@@ -290,7 +290,7 @@ message CEPTriggerEvent {
|
|||||||
// Trigger ID that fired
|
// Trigger ID that fired
|
||||||
string trigger_id = 1;
|
string trigger_id = 1;
|
||||||
|
|
||||||
// Timestamp when trigger fired (microseconds since epoch)
|
// Timestamp when trigger fired (nanoseconds since epoch)
|
||||||
uint64 timestamp = 2;
|
uint64 timestamp = 2;
|
||||||
|
|
||||||
// Schema information for the result rows
|
// Schema information for the result rows
|
||||||
|
|||||||
@@ -4,19 +4,27 @@ option java_multiple_files = true;
|
|||||||
option java_package = "com.dexorder.proto";
|
option java_package = "com.dexorder.proto";
|
||||||
|
|
||||||
message Market {
|
message Market {
|
||||||
// The prices and volumes must be adjusted by the rational denominator provided
|
string exchange_id = 2; // e.g., BINANCE
|
||||||
// by the market metadata
|
string market_id = 3; // e.g., BTC/USDT
|
||||||
string exchange_id = 2; // e.g., BINANCE
|
string market_type = 4; // e.g., Spot, CryptoPerpetual
|
||||||
string market_id = 3; // e.g., BTC/USDT
|
string description = 5; // e.g., Bitcoin/Tether on Binance
|
||||||
string market_type = 4; // e.g., Spot
|
repeated string column_names = 6; // e.g., ['open', 'high', 'low', 'close', 'volume']
|
||||||
string description = 5; // e.g., Bitcoin/Tether on Binance
|
string base_asset = 9; // e.g., BTC
|
||||||
repeated string column_names = 6; // e.g., ['open', 'high', 'low', 'close', 'volume', 'taker_vol', 'maker_vol']
|
string quote_asset = 10; // e.g., USDT
|
||||||
string base_asset = 9;
|
uint64 earliest_time = 11; // nanoseconds since epoch
|
||||||
string quote_asset = 10;
|
|
||||||
uint64 earliest_time = 11;
|
|
||||||
uint64 tick_denom = 12; // denominator applied to all OHLC price data
|
|
||||||
uint64 base_denom = 13; // denominator applied to base asset units
|
|
||||||
uint64 quote_denom = 14; // denominator applied to quote asset units
|
|
||||||
repeated uint32 supported_period_seconds = 15;
|
repeated uint32 supported_period_seconds = 15;
|
||||||
|
|
||||||
|
// Nautilus Instrument fields — used to construct Instrument objects in the sandbox bridge
|
||||||
|
uint32 price_precision = 16; // decimal places for prices (e.g., 2 for $0.01 resolution)
|
||||||
|
uint32 size_precision = 17; // decimal places for quantities
|
||||||
|
double tick_size = 18; // minimum price increment (e.g., 0.01)
|
||||||
|
double lot_size = 19; // minimum order size
|
||||||
|
double min_notional = 20; // minimum order value in quote currency
|
||||||
|
double margin_init = 21; // initial margin requirement (futures/perps only)
|
||||||
|
double margin_maint = 22; // maintenance margin (futures/perps only)
|
||||||
|
double maker_fee = 23; // maker fee rate (e.g., 0.001 = 0.1%)
|
||||||
|
double taker_fee = 24; // taker fee rate
|
||||||
|
double contract_multiplier = 25; // contract multiplier for derivatives (default 1.0)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ option java_package = "com.dexorder.proto";
|
|||||||
|
|
||||||
// Single OHLC row
|
// Single OHLC row
|
||||||
message OHLC {
|
message OHLC {
|
||||||
// Timestamp in microseconds since epoch
|
// Timestamp in nanoseconds since epoch
|
||||||
uint64 timestamp = 1;
|
uint64 timestamp = 1;
|
||||||
|
|
||||||
// The prices and volumes must be adjusted by the rational denominator provided
|
// Prices are stored as doubles (Nautilus-aligned, no denominator needed).
|
||||||
// by the market metadata. Optional to support null bars for periods with no trades.
|
// Optional to support null bars for periods with no trades.
|
||||||
optional int64 open = 2;
|
optional int64 open = 2;
|
||||||
optional int64 high = 3;
|
optional int64 high = 3;
|
||||||
optional int64 low = 4;
|
optional int64 low = 4;
|
||||||
@@ -22,7 +22,7 @@ message OHLC {
|
|||||||
optional int64 low_time = 11;
|
optional int64 low_time = 11;
|
||||||
optional int64 close_time = 12;
|
optional int64 close_time = 12;
|
||||||
optional int64 open_interest = 13;
|
optional int64 open_interest = 13;
|
||||||
string ticker = 14;
|
string ticker = 14; // Nautilus format: "BTC/USDT.BINANCE"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Batch of OHLC rows with metadata for historical request tracking
|
// Batch of OHLC rows with metadata for historical request tracking
|
||||||
@@ -49,7 +49,7 @@ message OHLCBatchMetadata {
|
|||||||
// OHLC period in seconds
|
// OHLC period in seconds
|
||||||
uint32 period_seconds = 4;
|
uint32 period_seconds = 4;
|
||||||
|
|
||||||
// Time range requested (microseconds since epoch)
|
// Time range requested (nanoseconds since epoch)
|
||||||
uint64 start_time = 5;
|
uint64 start_time = 5;
|
||||||
uint64 end_time = 6;
|
uint64 end_time = 6;
|
||||||
|
|
||||||
|
|||||||
@@ -7,19 +7,19 @@ message Tick {
|
|||||||
// Unique identifier for the trade
|
// Unique identifier for the trade
|
||||||
string trade_id = 1;
|
string trade_id = 1;
|
||||||
|
|
||||||
// Market identifier (matches Market.market_id)
|
// Market identifier in Nautilus format: "BTC/USDT.BINANCE"
|
||||||
string ticker = 2;
|
string ticker = 2;
|
||||||
|
|
||||||
// Timestamp in microseconds since epoch
|
// Timestamp in nanoseconds since epoch
|
||||||
uint64 timestamp = 3;
|
uint64 timestamp = 3;
|
||||||
|
|
||||||
// Price (must be adjusted by tick_denom from Market metadata)
|
// Price as a scaled integer (divide by 10^price_precision from Market metadata)
|
||||||
int64 price = 4;
|
int64 price = 4;
|
||||||
|
|
||||||
// Base asset amount (must be adjusted by base_denom from Market metadata)
|
// Base asset amount as a scaled integer (divide by 10^size_precision from Market metadata)
|
||||||
int64 amount = 5;
|
int64 amount = 5;
|
||||||
|
|
||||||
// Quote asset amount (must be adjusted by quote_denom from Market metadata)
|
// Quote asset amount as a scaled integer
|
||||||
int64 quote_amount = 6;
|
int64 quote_amount = 6;
|
||||||
|
|
||||||
// Side: true = taker buy (market buy), false = taker sell (market sell)
|
// Side: true = taker buy (market buy), false = taker sell (market sell)
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ The relay acts as a well-known bind point for all components:
|
|||||||
```
|
```
|
||||||
1. Client subscribes to ticker
|
1. Client subscribes to ticker
|
||||||
Socket: SUB → XPUB (5558)
|
Socket: SUB → XPUB (5558)
|
||||||
Topic: "BINANCE:BTC/USDT|tick"
|
Topic: "BTC/USDT.BINANCE|tick"
|
||||||
|
|
||||||
2. Relay forwards subscription
|
2. Relay forwards subscription
|
||||||
Socket: XSUB → Flink PUB (5557)
|
Socket: XSUB → Flink PUB (5557)
|
||||||
|
|||||||
@@ -205,13 +205,13 @@ impl Relay {
|
|||||||
info!("Handling request submission: request_id={}, ticker={}, client_id={:?}",
|
info!("Handling request submission: request_id={}, ticker={}, client_id={:?}",
|
||||||
request_id, ticker, client_id);
|
request_id, ticker, client_id);
|
||||||
|
|
||||||
// Extract exchange prefix from ticker
|
// Extract exchange suffix from ticker (Nautilus format: "BTC/USDT.BINANCE")
|
||||||
let exchange_prefix = ticker.split(':').next()
|
let exchange_prefix = ticker.rsplitn(2, '.').next()
|
||||||
.map(|s| format!("{}:", s))
|
.map(|s| format!("{}.", s))
|
||||||
.unwrap_or_else(|| String::from(""));
|
.unwrap_or_else(|| String::from(""));
|
||||||
|
|
||||||
if exchange_prefix.is_empty() {
|
if exchange_prefix.is_empty() {
|
||||||
warn!("Ticker '{}' missing exchange prefix", ticker);
|
warn!("Ticker '{}' missing exchange suffix", ticker);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build DataRequest protobuf for ingestors
|
// Build DataRequest protobuf for ingestors
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async def main():
|
|||||||
try:
|
try:
|
||||||
# Fetch OHLC data (automatically checks cache and requests missing data)
|
# Fetch OHLC data (automatically checks cache and requests missing data)
|
||||||
df = await client.fetch_ohlc(
|
df = await client.fetch_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600, # 1-hour candles
|
period_seconds=3600, # 1-hour candles
|
||||||
start_time=1735689600000000, # microseconds
|
start_time=1735689600000000, # microseconds
|
||||||
end_time=1736294399000000
|
end_time=1736294399000000
|
||||||
@@ -112,7 +112,7 @@ Initialize the client with connection parameters.
|
|||||||
Fetch OHLC data with smart caching.
|
Fetch OHLC data with smart caching.
|
||||||
|
|
||||||
**Parameters:**
|
**Parameters:**
|
||||||
- `ticker` (str): Market identifier (e.g., "BINANCE:BTC/USDT")
|
- `ticker` (str): Market identifier (e.g., "BTC/USDT.BINANCE")
|
||||||
- `period_seconds` (int): OHLC period in seconds (60, 300, 3600, 86400, etc.)
|
- `period_seconds` (int): OHLC period in seconds (60, 300, 3600, 86400, etc.)
|
||||||
- `start_time` (int): Start timestamp in microseconds
|
- `start_time` (int): Start timestamp in microseconds
|
||||||
- `end_time` (int): End timestamp in microseconds
|
- `end_time` (int): End timestamp in microseconds
|
||||||
@@ -179,7 +179,7 @@ await client.connect()
|
|||||||
|
|
||||||
# Now safe to make requests
|
# Now safe to make requests
|
||||||
result = await client.request_historical_ohlc(
|
result = await client.request_historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time=1735689600000000,
|
start_time=1735689600000000,
|
||||||
end_time=1736294399000000
|
end_time=1736294399000000
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ api = get_api()
|
|||||||
|
|
||||||
# Method 1: Using Unix timestamps (seconds)
|
# Method 1: Using Unix timestamps (seconds)
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600, # 1 hour candles
|
period_seconds=3600, # 1 hour candles
|
||||||
start_time=1640000000, # Unix timestamp in seconds
|
start_time=1640000000, # Unix timestamp in seconds
|
||||||
end_time=1640086400,
|
end_time=1640086400,
|
||||||
@@ -38,7 +38,7 @@ df = asyncio.run(api.data.historical_ohlc(
|
|||||||
|
|
||||||
# Method 2: Using date strings
|
# Method 2: Using date strings
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20", # Simple date string
|
start_time="2021-12-20", # Simple date string
|
||||||
end_time="2021-12-21",
|
end_time="2021-12-21",
|
||||||
@@ -47,7 +47,7 @@ df = asyncio.run(api.data.historical_ohlc(
|
|||||||
|
|
||||||
# Method 3: Using date strings with time
|
# Method 3: Using date strings with time
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20 00:00:00",
|
start_time="2021-12-20 00:00:00",
|
||||||
end_time="2021-12-20 23:59:59",
|
end_time="2021-12-20 23:59:59",
|
||||||
@@ -56,7 +56,7 @@ df = asyncio.run(api.data.historical_ohlc(
|
|||||||
|
|
||||||
# Method 4: Using datetime objects
|
# Method 4: Using datetime objects
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time=datetime(2021, 12, 20),
|
start_time=datetime(2021, 12, 20),
|
||||||
end_time=datetime(2021, 12, 21),
|
end_time=datetime(2021, 12, 21),
|
||||||
@@ -92,7 +92,7 @@ api = get_api()
|
|||||||
|
|
||||||
# Fetch data
|
# Fetch data
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21",
|
end_time="2021-12-21",
|
||||||
@@ -121,7 +121,7 @@ api = get_api()
|
|||||||
|
|
||||||
# Fetch data
|
# Fetch data
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21"
|
end_time="2021-12-21"
|
||||||
@@ -161,7 +161,7 @@ api = get_api()
|
|||||||
|
|
||||||
# Fetch historical data using date strings (easiest for research)
|
# Fetch historical data using date strings (easiest for research)
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600, # 1 hour
|
period_seconds=3600, # 1 hour
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21",
|
end_time="2021-12-21",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def get_api() -> API:
|
|||||||
|
|
||||||
# Fetch data
|
# Fetch data
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21"
|
end_time="2021-12-21"
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class API:
|
|||||||
|
|
||||||
# Fetch data
|
# Fetch data
|
||||||
df = asyncio.run(api.data.historical_ohlc(
|
df = asyncio.run(api.data.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21"
|
end_time="2021-12-21"
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ class DataAPI(ABC):
|
|||||||
Fetch historical OHLC candlestick data for a market.
|
Fetch historical OHLC candlestick data for a market.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier in format "EXCHANGE:SYMBOL"
|
ticker: Market identifier in format "MARKET.EXCHANGE"
|
||||||
Examples: "BINANCE:BTC/USDT", "COINBASE:ETH/USD"
|
Examples: "BTC/USDT.BINANCE", "ETH/USD.COINBASE"
|
||||||
period_seconds: Candle period in seconds
|
period_seconds: Candle period in seconds
|
||||||
Common values:
|
Common values:
|
||||||
- 60 (1 minute)
|
- 60 (1 minute)
|
||||||
@@ -55,7 +55,7 @@ class DataAPI(ABC):
|
|||||||
Returns:
|
Returns:
|
||||||
DataFrame with candlestick data sorted by timestamp (ascending).
|
DataFrame with candlestick data sorted by timestamp (ascending).
|
||||||
Standard columns (always included):
|
Standard columns (always included):
|
||||||
- timestamp: Period start time in microseconds
|
- timestamp: Period start time in nanoseconds
|
||||||
- open: Opening price (decimal float)
|
- open: Opening price (decimal float)
|
||||||
- high: Highest price (decimal float)
|
- high: Highest price (decimal float)
|
||||||
- low: Lowest price (decimal float)
|
- low: Lowest price (decimal float)
|
||||||
@@ -71,7 +71,7 @@ class DataAPI(ABC):
|
|||||||
Examples:
|
Examples:
|
||||||
# Basic OHLC with Unix timestamp
|
# Basic OHLC with Unix timestamp
|
||||||
df = await api.historical_ohlc(
|
df = await api.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time=1640000000,
|
start_time=1640000000,
|
||||||
end_time=1640086400
|
end_time=1640086400
|
||||||
@@ -79,7 +79,7 @@ class DataAPI(ABC):
|
|||||||
|
|
||||||
# Using date strings with volume
|
# Using date strings with volume
|
||||||
df = await api.historical_ohlc(
|
df = await api.historical_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600,
|
period_seconds=3600,
|
||||||
start_time="2021-12-20",
|
start_time="2021-12-20",
|
||||||
end_time="2021-12-21",
|
end_time="2021-12-21",
|
||||||
@@ -89,7 +89,7 @@ class DataAPI(ABC):
|
|||||||
# Using datetime objects
|
# Using datetime objects
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
df = await api.historical_ohlc(
|
df = await api.historical_ohlc(
|
||||||
ticker="COINBASE:ETH/USD",
|
ticker="ETH/USD.COINBASE",
|
||||||
period_seconds=300,
|
period_seconds=300,
|
||||||
start_time=datetime(2021, 12, 20, 9, 30),
|
start_time=datetime(2021, 12, 20, 9, 30),
|
||||||
end_time=datetime(2021, 12, 20, 16, 30),
|
end_time=datetime(2021, 12, 20, 16, 30),
|
||||||
@@ -113,8 +113,8 @@ class DataAPI(ABC):
|
|||||||
specify exact timestamps. Useful for real-time analysis and indicators.
|
specify exact timestamps. Useful for real-time analysis and indicators.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier in format "EXCHANGE:SYMBOL"
|
ticker: Market identifier in format "MARKET.EXCHANGE"
|
||||||
Examples: "BINANCE:BTC/USDT", "COINBASE:ETH/USD"
|
Examples: "BTC/USDT.BINANCE", "ETH/USD.COINBASE"
|
||||||
period_seconds: OHLC candle period in seconds
|
period_seconds: OHLC candle period in seconds
|
||||||
Common values: 60 (1m), 300 (5m), 900 (15m), 3600 (1h),
|
Common values: 60 (1m), 300 (5m), 900 (15m), 3600 (1h),
|
||||||
86400 (1d), 604800 (1w)
|
86400 (1d), 604800 (1w)
|
||||||
@@ -133,14 +133,14 @@ class DataAPI(ABC):
|
|||||||
Examples:
|
Examples:
|
||||||
# Get the last candle
|
# Get the last candle
|
||||||
df = await api.latest_ohlc(
|
df = await api.latest_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=3600
|
period_seconds=3600
|
||||||
)
|
)
|
||||||
# Returns: timestamp, open, high, low, close
|
# Returns: timestamp, open, high, low, close
|
||||||
|
|
||||||
# Get the last 50 5-minute candles with volume
|
# Get the last 50 5-minute candles with volume
|
||||||
df = await api.latest_ohlc(
|
df = await api.latest_ohlc(
|
||||||
ticker="COINBASE:ETH/USD",
|
ticker="ETH/USD.COINBASE",
|
||||||
period_seconds=300,
|
period_seconds=300,
|
||||||
length=50,
|
length=50,
|
||||||
extra_columns=["volume", "buy_vol", "sell_vol"]
|
extra_columns=["volume", "buy_vol", "sell_vol"]
|
||||||
@@ -148,7 +148,7 @@ class DataAPI(ABC):
|
|||||||
|
|
||||||
# Get recent candles with all timing data
|
# Get recent candles with all timing data
|
||||||
df = await api.latest_ohlc(
|
df = await api.latest_ohlc(
|
||||||
ticker="BINANCE:BTC/USDT",
|
ticker="BTC/USDT.BINANCE",
|
||||||
period_seconds=60,
|
period_seconds=60,
|
||||||
length=100,
|
length=100,
|
||||||
extra_columns=["open_time", "high_time", "low_time", "close_time"]
|
extra_columns=["open_time", "high_time", "low_time", "close_time"]
|
||||||
|
|||||||
@@ -110,10 +110,10 @@ class HistoryClient:
|
|||||||
IMPORTANT: Call connect() before using this method.
|
IMPORTANT: Call connect() before using this method.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier (e.g., "BINANCE:BTC/USDT")
|
ticker: Market identifier (e.g., "BTC/USDT.BINANCE")
|
||||||
period_seconds: OHLC period in seconds
|
period_seconds: OHLC period in seconds
|
||||||
start_time: Start timestamp in microseconds
|
start_time: Start timestamp in nanoseconds
|
||||||
end_time: End timestamp in microseconds
|
end_time: End timestamp in nanoseconds
|
||||||
timeout: Request timeout in seconds (default: 30)
|
timeout: Request timeout in seconds (default: 30)
|
||||||
limit: Optional limit on number of candles
|
limit: Optional limit on number of candles
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
IcebergClient - Query OHLC data from Iceberg warehouse (Iceberg 1.10.1)
|
IcebergClient - Query OHLC data from Iceberg warehouse (Iceberg 1.10.1)
|
||||||
|
|
||||||
|
Tickers use Nautilus format: "BTC/USDT.BINANCE"
|
||||||
|
All timestamps are nanoseconds since epoch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional, List, Tuple
|
from typing import Optional, List, Tuple
|
||||||
@@ -39,7 +42,6 @@ class IcebergClient:
|
|||||||
s3_endpoint: Optional[str] = None,
|
s3_endpoint: Optional[str] = None,
|
||||||
s3_access_key: Optional[str] = None,
|
s3_access_key: Optional[str] = None,
|
||||||
s3_secret_key: Optional[str] = None,
|
s3_secret_key: Optional[str] = None,
|
||||||
metadata_client=None, # SymbolMetadataClient (avoid circular import)
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Initialize Iceberg client.
|
Initialize Iceberg client.
|
||||||
@@ -50,11 +52,9 @@ class IcebergClient:
|
|||||||
s3_endpoint: S3/MinIO endpoint URL (e.g., "http://localhost:9000")
|
s3_endpoint: S3/MinIO endpoint URL (e.g., "http://localhost:9000")
|
||||||
s3_access_key: S3/MinIO access key
|
s3_access_key: S3/MinIO access key
|
||||||
s3_secret_key: S3/MinIO secret key
|
s3_secret_key: S3/MinIO secret key
|
||||||
metadata_client: SymbolMetadataClient for price/volume conversion
|
|
||||||
"""
|
"""
|
||||||
self.catalog_uri = catalog_uri
|
self.catalog_uri = catalog_uri
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
self.metadata_client = metadata_client
|
|
||||||
|
|
||||||
catalog_props = {"uri": catalog_uri}
|
catalog_props = {"uri": catalog_uri}
|
||||||
if s3_endpoint:
|
if s3_endpoint:
|
||||||
@@ -80,15 +80,14 @@ class IcebergClient:
|
|||||||
Query OHLC data for a specific ticker, period, and time range.
|
Query OHLC data for a specific ticker, period, and time range.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier (e.g., "BINANCE:BTC/USDT")
|
ticker: Market identifier in Nautilus format (e.g., "BTC/USDT.BINANCE")
|
||||||
period_seconds: OHLC period in seconds (60, 300, 3600, etc.)
|
period_seconds: OHLC period in seconds (60, 300, 3600, etc.)
|
||||||
start_time: Start timestamp in microseconds
|
start_time: Start timestamp in nanoseconds
|
||||||
end_time: End timestamp in microseconds
|
end_time: End timestamp in nanoseconds (exclusive)
|
||||||
columns: Optional list of columns to select. If None, returns all columns.
|
columns: Optional list of columns to select.
|
||||||
Example: ["timestamp", "open", "high", "low", "close", "volume"]
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
DataFrame with OHLC data sorted by timestamp
|
DataFrame with OHLC data sorted by timestamp, with a DatetimeIndex (UTC).
|
||||||
"""
|
"""
|
||||||
# Reload table metadata to pick up snapshots committed after this client was initialized
|
# Reload table metadata to pick up snapshots committed after this client was initialized
|
||||||
self.table = self.catalog.load_table(f"{self.namespace}.ohlc")
|
self.table = self.catalog.load_table(f"{self.namespace}.ohlc")
|
||||||
@@ -102,7 +101,6 @@ class IcebergClient:
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Select specific columns if requested
|
|
||||||
if columns is not None:
|
if columns is not None:
|
||||||
scan = scan.select(*columns)
|
scan = scan.select(*columns)
|
||||||
|
|
||||||
@@ -110,52 +108,10 @@ class IcebergClient:
|
|||||||
|
|
||||||
if not df.empty:
|
if not df.empty:
|
||||||
df = df.sort_values("timestamp")
|
df = df.sort_values("timestamp")
|
||||||
# Convert integer microsecond timestamps to DatetimeIndex
|
# Convert integer nanosecond timestamps to DatetimeIndex
|
||||||
df.index = pd.to_datetime(df["timestamp"], unit="us", utc=True)
|
df.index = pd.to_datetime(df["timestamp"], unit="ns", utc=True)
|
||||||
df.index.name = "datetime"
|
df.index.name = "datetime"
|
||||||
df = df.drop(columns=["timestamp"])
|
df = df.drop(columns=["timestamp"])
|
||||||
# Apply price/volume conversion if metadata client available
|
|
||||||
if self.metadata_client is not None:
|
|
||||||
df = self._apply_denominators(df, ticker)
|
|
||||||
|
|
||||||
return df
|
|
||||||
|
|
||||||
def _apply_denominators(self, df: pd.DataFrame, ticker: str) -> pd.DataFrame:
|
|
||||||
"""
|
|
||||||
Convert integer prices and volumes to decimal floats using market metadata.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
df: DataFrame with integer OHLC data
|
|
||||||
ticker: Market identifier for metadata lookup
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
DataFrame with decimal prices and volumes
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If metadata not found for ticker
|
|
||||||
"""
|
|
||||||
if df.empty:
|
|
||||||
return df
|
|
||||||
|
|
||||||
# Get metadata for this ticker
|
|
||||||
metadata = self.metadata_client.get_metadata(ticker)
|
|
||||||
|
|
||||||
# Convert price columns (divide by tick_denom)
|
|
||||||
price_columns = ["open", "high", "low", "close"]
|
|
||||||
for col in price_columns:
|
|
||||||
if col in df.columns:
|
|
||||||
df[col] = df[col].astype(float) / metadata.tick_denom
|
|
||||||
|
|
||||||
# Convert volume columns (divide by base_denom)
|
|
||||||
volume_columns = ["volume", "buy_vol", "sell_vol"]
|
|
||||||
for col in volume_columns:
|
|
||||||
if col in df.columns and df[col].notna().any():
|
|
||||||
df[col] = df[col].astype(float) / metadata.base_denom
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
f"Applied denominators to {ticker}: tick_denom={metadata.tick_denom}, "
|
|
||||||
f"base_denom={metadata.base_denom} ({len(df)} rows)"
|
|
||||||
)
|
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
@@ -169,32 +125,28 @@ class IcebergClient:
|
|||||||
"""
|
"""
|
||||||
Identify missing data ranges in the requested time period.
|
Identify missing data ranges in the requested time period.
|
||||||
|
|
||||||
Returns list of (start, end) tuples for missing ranges.
|
|
||||||
Expected candles are calculated based on period_seconds.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier
|
ticker: Market identifier in Nautilus format
|
||||||
period_seconds: OHLC period in seconds
|
period_seconds: OHLC period in seconds
|
||||||
start_time: Start timestamp in microseconds
|
start_time: Start timestamp in nanoseconds
|
||||||
end_time: End timestamp in microseconds
|
end_time: End timestamp in nanoseconds
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
List of (start_time, end_time) tuples for missing ranges
|
List of (start_time, end_time) tuples for missing ranges (nanoseconds)
|
||||||
"""
|
"""
|
||||||
df = self.query_ohlc(ticker, period_seconds, start_time, end_time)
|
df = self.query_ohlc(ticker, period_seconds, start_time, end_time)
|
||||||
|
|
||||||
if df.empty:
|
if df.empty:
|
||||||
# All data is missing
|
|
||||||
return [(start_time, end_time)]
|
return [(start_time, end_time)]
|
||||||
|
|
||||||
# Convert period to microseconds
|
# Convert period to nanoseconds
|
||||||
period_micros = period_seconds * 1_000_000
|
period_nanos = period_seconds * 1_000_000_000
|
||||||
|
|
||||||
# Generate expected timestamps — end_time is exclusive
|
# Generate expected timestamps — end_time is exclusive
|
||||||
expected_timestamps = list(range(start_time, end_time, period_micros))
|
expected_timestamps = list(range(start_time, end_time, period_nanos))
|
||||||
actual_timestamps = set(df.index.view('int64') // 1000)
|
# DatetimeIndex backed by nanoseconds — view as int64 directly
|
||||||
|
actual_timestamps = set(df.index.view('int64'))
|
||||||
|
|
||||||
# Find gaps
|
|
||||||
missing = sorted(set(expected_timestamps) - actual_timestamps)
|
missing = sorted(set(expected_timestamps) - actual_timestamps)
|
||||||
|
|
||||||
if not missing:
|
if not missing:
|
||||||
@@ -206,15 +158,12 @@ class IcebergClient:
|
|||||||
prev_ts = missing[0]
|
prev_ts = missing[0]
|
||||||
|
|
||||||
for ts in missing[1:]:
|
for ts in missing[1:]:
|
||||||
if ts > prev_ts + period_micros:
|
if ts > prev_ts + period_nanos:
|
||||||
# Gap in missing data - close previous range
|
|
||||||
ranges.append((range_start, prev_ts))
|
ranges.append((range_start, prev_ts))
|
||||||
range_start = ts
|
range_start = ts
|
||||||
prev_ts = ts
|
prev_ts = ts
|
||||||
|
|
||||||
# Close final range
|
|
||||||
ranges.append((range_start, prev_ts))
|
ranges.append((range_start, prev_ts))
|
||||||
|
|
||||||
return ranges
|
return ranges
|
||||||
|
|
||||||
def has_data(
|
def has_data(
|
||||||
@@ -228,10 +177,10 @@ class IcebergClient:
|
|||||||
Check if any data exists for the given parameters.
|
Check if any data exists for the given parameters.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier
|
ticker: Market identifier in Nautilus format
|
||||||
period_seconds: OHLC period in seconds
|
period_seconds: OHLC period in seconds
|
||||||
start_time: Start timestamp in microseconds
|
start_time: Start timestamp in nanoseconds
|
||||||
end_time: End timestamp in microseconds
|
end_time: End timestamp in nanoseconds
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if at least one candle exists, False otherwise
|
True if at least one candle exists, False otherwise
|
||||||
|
|||||||
@@ -138,8 +138,8 @@ class ChartingAPIImpl(ChartingAPI):
|
|||||||
if col in df.columns:
|
if col in df.columns:
|
||||||
# Handle potential timestamp index (convert from microseconds)
|
# Handle potential timestamp index (convert from microseconds)
|
||||||
if df.index.name == 'timestamp' or 'timestamp' in str(df.index.dtype):
|
if df.index.name == 'timestamp' or 'timestamp' in str(df.index.dtype):
|
||||||
# Assume microseconds, convert to datetime
|
# Assume nanoseconds, convert to datetime
|
||||||
plot_index = pd.to_datetime(df.index, unit='us')
|
plot_index = pd.to_datetime(df.index, unit='ns')
|
||||||
else:
|
else:
|
||||||
plot_index = df.index
|
plot_index = df.index
|
||||||
|
|
||||||
@@ -206,18 +206,18 @@ class ChartingAPIImpl(ChartingAPI):
|
|||||||
"""
|
"""
|
||||||
df_copy = df.copy()
|
df_copy = df.copy()
|
||||||
|
|
||||||
# Handle timestamp column (in microseconds) -> DatetimeIndex
|
# Handle timestamp column (in nanoseconds) -> DatetimeIndex
|
||||||
if 'timestamp' in df_copy.columns:
|
if 'timestamp' in df_copy.columns:
|
||||||
df_copy.index = pd.to_datetime(df_copy['timestamp'], unit='us')
|
df_copy.index = pd.to_datetime(df_copy['timestamp'], unit='ns')
|
||||||
df_copy = df_copy.drop(columns=['timestamp'])
|
df_copy = df_copy.drop(columns=['timestamp'])
|
||||||
elif df_copy.index.name == 'timestamp' or 'int' in str(df_copy.index.dtype):
|
elif df_copy.index.name == 'timestamp' or 'int' in str(df_copy.index.dtype):
|
||||||
# Index is timestamp in microseconds
|
# Index is timestamp in nanoseconds
|
||||||
df_copy.index = pd.to_datetime(df_copy.index, unit='us')
|
df_copy.index = pd.to_datetime(df_copy.index, unit='ns')
|
||||||
|
|
||||||
# Ensure index is DatetimeIndex
|
# Ensure index is DatetimeIndex
|
||||||
if not isinstance(df_copy.index, pd.DatetimeIndex):
|
if not isinstance(df_copy.index, pd.DatetimeIndex):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"DataFrame must have a DatetimeIndex or a 'timestamp' column in microseconds"
|
"DataFrame must have a DatetimeIndex or a 'timestamp' column in nanoseconds"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Normalize column names to lowercase
|
# Normalize column names to lowercase
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import pandas as pd
|
|||||||
|
|
||||||
from dexorder.api.data_api import DataAPI
|
from dexorder.api.data_api import DataAPI
|
||||||
from dexorder.ohlc_client import OHLCClient
|
from dexorder.ohlc_client import OHLCClient
|
||||||
from dexorder.utils import TimestampInput, to_microseconds
|
from dexorder.utils import TimestampInput, to_nanoseconds
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -105,12 +105,12 @@ class DataAPIImpl(DataAPI):
|
|||||||
if not self._started:
|
if not self._started:
|
||||||
await self.start()
|
await self.start()
|
||||||
|
|
||||||
# Convert timestamps to microseconds
|
# Convert timestamps to nanoseconds
|
||||||
start_micros = to_microseconds(start_time)
|
start_nanos = to_nanoseconds(start_time)
|
||||||
end_micros = to_microseconds(end_time)
|
end_nanos = to_nanoseconds(end_time)
|
||||||
|
|
||||||
log.debug(f"Fetching OHLC: {ticker}, period={period_seconds}s, "
|
log.debug(f"Fetching OHLC: {ticker}, period={period_seconds}s, "
|
||||||
f"start={start_time} ({start_micros}), end={end_time} ({end_micros})")
|
f"start={start_time} ({start_nanos}ns), end={end_time} ({end_nanos}ns)")
|
||||||
|
|
||||||
# Validate extra_columns
|
# Validate extra_columns
|
||||||
if extra_columns:
|
if extra_columns:
|
||||||
@@ -131,8 +131,8 @@ class DataAPIImpl(DataAPI):
|
|||||||
df = await self.ohlc_client.fetch_ohlc(
|
df = await self.ohlc_client.fetch_ohlc(
|
||||||
ticker=ticker,
|
ticker=ticker,
|
||||||
period_seconds=period_seconds,
|
period_seconds=period_seconds,
|
||||||
start_time=start_micros,
|
start_time=start_nanos,
|
||||||
end_time=end_micros,
|
end_time=end_nanos,
|
||||||
request_timeout=self.request_timeout
|
request_timeout=self.request_timeout
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import logging
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from .iceberg_client import IcebergClient
|
from .iceberg_client import IcebergClient
|
||||||
from .history_client import HistoryClient
|
from .history_client import HistoryClient
|
||||||
from .symbol_metadata_client import SymbolMetadataClient
|
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -53,26 +52,14 @@ class OHLCClient:
|
|||||||
s3_access_key: S3/MinIO access key
|
s3_access_key: S3/MinIO access key
|
||||||
s3_secret_key: S3/MinIO secret key
|
s3_secret_key: S3/MinIO secret key
|
||||||
"""
|
"""
|
||||||
# Initialize symbol metadata client for price/volume conversion
|
|
||||||
self.metadata = SymbolMetadataClient(
|
|
||||||
iceberg_catalog_uri,
|
|
||||||
namespace=namespace,
|
|
||||||
s3_endpoint=s3_endpoint,
|
|
||||||
s3_access_key=s3_access_key,
|
|
||||||
s3_secret_key=s3_secret_key,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Initialize Iceberg client with metadata client for automatic conversion
|
|
||||||
self.iceberg = IcebergClient(
|
self.iceberg = IcebergClient(
|
||||||
iceberg_catalog_uri, namespace,
|
iceberg_catalog_uri, namespace,
|
||||||
s3_endpoint=s3_endpoint,
|
s3_endpoint=s3_endpoint,
|
||||||
s3_access_key=s3_access_key,
|
s3_access_key=s3_access_key,
|
||||||
s3_secret_key=s3_secret_key,
|
s3_secret_key=s3_secret_key,
|
||||||
metadata_client=self.metadata,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.history = HistoryClient(relay_endpoint, notification_endpoint)
|
self.history = HistoryClient(relay_endpoint, notification_endpoint)
|
||||||
log.info("OHLCClient initialized with automatic price/volume conversion")
|
log.info("OHLCClient initialized")
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
"""
|
"""
|
||||||
@@ -107,10 +94,10 @@ class OHLCClient:
|
|||||||
6. Return results
|
6. Return results
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier (e.g., "BINANCE:BTC/USDT")
|
ticker: Market identifier in Nautilus format (e.g., "BTC/USDT.BINANCE")
|
||||||
period_seconds: OHLC period in seconds (60, 300, 3600, etc.)
|
period_seconds: OHLC period in seconds (60, 300, 3600, etc.)
|
||||||
start_time: Start timestamp in microseconds
|
start_time: Start timestamp in nanoseconds
|
||||||
end_time: End timestamp in microseconds
|
end_time: End timestamp in nanoseconds
|
||||||
request_timeout: Timeout for historical data requests (default: 30s)
|
request_timeout: Timeout for historical data requests (default: 30s)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -121,9 +108,9 @@ class OHLCClient:
|
|||||||
ValueError: If request fails
|
ValueError: If request fails
|
||||||
"""
|
"""
|
||||||
# Align times to period boundaries: [ceil(start), ceil(end)) exclusive
|
# Align times to period boundaries: [ceil(start), ceil(end)) exclusive
|
||||||
period_micros = period_seconds * 1_000_000
|
period_nanos = period_seconds * 1_000_000_000
|
||||||
start_time = ((start_time + period_micros - 1) // period_micros) * period_micros
|
start_time = ((start_time + period_nanos - 1) // period_nanos) * period_nanos
|
||||||
end_time = ((end_time + period_micros - 1) // period_micros) * period_micros # exclusive
|
end_time = ((end_time + period_nanos - 1) // period_nanos) * period_nanos # exclusive
|
||||||
|
|
||||||
# Step 1: Check Iceberg for existing data
|
# Step 1: Check Iceberg for existing data
|
||||||
df = self.iceberg.query_ohlc(ticker, period_seconds, start_time, end_time)
|
df = self.iceberg.query_ohlc(ticker, period_seconds, start_time, end_time)
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
SymbolMetadataClient - Query symbol metadata from Iceberg for price/volume conversion.
|
SymbolMetadataClient - Query symbol metadata from Iceberg.
|
||||||
|
|
||||||
Provides lazy-loaded, cached access to symbol metadata including denominators
|
Tickers use Nautilus format: "BTC/USDT.BINANCE" (market_id.exchange_id).
|
||||||
used to convert integer OHLC data to decimal prices and volumes.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -13,23 +12,67 @@ from pyiceberg.expressions import EqualTo, And
|
|||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def format_ticker(exchange_id: str, market_id: str) -> str:
|
||||||
|
"""Format a ticker in Nautilus convention: 'BTC/USDT.BINANCE'."""
|
||||||
|
return f"{market_id}.{exchange_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ticker(ticker: str) -> tuple[str, str]:
|
||||||
|
"""
|
||||||
|
Parse a Nautilus-format ticker into (exchange_id, market_id).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
ticker: e.g. "BTC/USDT.BINANCE"
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(exchange_id, market_id) e.g. ("BINANCE", "BTC/USDT")
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: if the ticker does not contain a dot separator
|
||||||
|
"""
|
||||||
|
if "." not in ticker:
|
||||||
|
raise ValueError(
|
||||||
|
f"Invalid ticker format '{ticker}'. Expected Nautilus format: 'MARKET.EXCHANGE' "
|
||||||
|
f"(e.g., 'BTC/USDT.BINANCE')"
|
||||||
|
)
|
||||||
|
# Split on the LAST dot to handle market IDs that could theoretically contain dots
|
||||||
|
dot_pos = ticker.rfind(".")
|
||||||
|
market_id = ticker[:dot_pos]
|
||||||
|
exchange_id = ticker[dot_pos + 1:]
|
||||||
|
return exchange_id, market_id
|
||||||
|
|
||||||
|
|
||||||
class SymbolMetadata(NamedTuple):
|
class SymbolMetadata(NamedTuple):
|
||||||
"""Symbol metadata containing denominators for price/volume conversion."""
|
"""Symbol metadata for Nautilus Instrument construction and order validation."""
|
||||||
exchange_id: str
|
exchange_id: str
|
||||||
market_id: str
|
market_id: str
|
||||||
tick_denom: int # Denominator for price fields (open, high, low, close)
|
|
||||||
base_denom: int # Denominator for base asset (volume in base terms)
|
|
||||||
quote_denom: int # Denominator for quote asset
|
|
||||||
market_type: Optional[str] = None
|
market_type: Optional[str] = None
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
|
base_asset: Optional[str] = None
|
||||||
|
quote_asset: Optional[str] = None
|
||||||
|
# Nautilus Instrument fields
|
||||||
|
price_precision: Optional[int] = None # decimal places for prices
|
||||||
|
size_precision: Optional[int] = None # decimal places for quantities
|
||||||
|
tick_size: Optional[float] = None # minimum price increment
|
||||||
|
lot_size: Optional[float] = None # minimum order size
|
||||||
|
min_notional: Optional[float] = None # minimum order value in quote currency
|
||||||
|
margin_init: Optional[float] = None # initial margin (futures/perps only)
|
||||||
|
margin_maint: Optional[float] = None # maintenance margin (futures/perps only)
|
||||||
|
maker_fee: Optional[float] = None # maker fee rate (e.g., 0.001 = 0.1%)
|
||||||
|
taker_fee: Optional[float] = None # taker fee rate
|
||||||
|
contract_multiplier: Optional[float] = None # for derivatives (default 1.0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ticker(self) -> str:
|
||||||
|
"""Nautilus-format ticker: 'BTC/USDT.BINANCE'."""
|
||||||
|
return format_ticker(self.exchange_id, self.market_id)
|
||||||
|
|
||||||
|
|
||||||
class SymbolMetadataClient:
|
class SymbolMetadataClient:
|
||||||
"""
|
"""
|
||||||
Client for querying symbol metadata from Iceberg.
|
Client for querying symbol metadata from Iceberg.
|
||||||
|
|
||||||
Provides lazy-loaded, cached access to market metadata including
|
Tickers use Nautilus format: "BTC/USDT.BINANCE"
|
||||||
denominators needed to convert integer OHLC prices/volumes to decimals.
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
@@ -40,16 +83,6 @@ class SymbolMetadataClient:
|
|||||||
s3_access_key: Optional[str] = None,
|
s3_access_key: Optional[str] = None,
|
||||||
s3_secret_key: Optional[str] = None,
|
s3_secret_key: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
Initialize symbol metadata client.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
catalog_uri: URI of the Iceberg catalog
|
|
||||||
namespace: Iceberg namespace (default: "trading")
|
|
||||||
s3_endpoint: S3/MinIO endpoint URL
|
|
||||||
s3_access_key: S3/MinIO access key
|
|
||||||
s3_secret_key: S3/MinIO secret key
|
|
||||||
"""
|
|
||||||
self.catalog_uri = catalog_uri
|
self.catalog_uri = catalog_uri
|
||||||
self.namespace = namespace
|
self.namespace = namespace
|
||||||
|
|
||||||
@@ -63,55 +96,39 @@ class SymbolMetadataClient:
|
|||||||
catalog_props["s3.secret-access-key"] = s3_secret_key
|
catalog_props["s3.secret-access-key"] = s3_secret_key
|
||||||
|
|
||||||
self.catalog = load_catalog("trading", **catalog_props)
|
self.catalog = load_catalog("trading", **catalog_props)
|
||||||
|
|
||||||
# Lazy load the table
|
|
||||||
self._table = None
|
self._table = None
|
||||||
|
|
||||||
# Cache: ticker -> SymbolMetadata
|
|
||||||
self._cache: Dict[str, SymbolMetadata] = {}
|
self._cache: Dict[str, SymbolMetadata] = {}
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self):
|
def table(self):
|
||||||
"""Lazy load the symbol_metadata table."""
|
|
||||||
if self._table is None:
|
if self._table is None:
|
||||||
try:
|
try:
|
||||||
self._table = self.catalog.load_table(f"{self.namespace}.symbol_metadata")
|
self._table = self.catalog.load_table(f"{self.namespace}.symbol_metadata")
|
||||||
log.info(f"Loaded symbol_metadata table from {self.namespace}")
|
log.info(f"Loaded symbol_metadata table from {self.namespace}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Failed to load symbol_metadata table from {self.namespace}.symbol_metadata. "
|
f"Failed to load symbol_metadata table from {self.namespace}.symbol_metadata: {e}"
|
||||||
f"This table is required for price/volume conversion. Error: {e}"
|
|
||||||
) from e
|
) from e
|
||||||
return self._table
|
return self._table
|
||||||
|
|
||||||
def get_metadata(self, ticker: str) -> SymbolMetadata:
|
def get_metadata(self, ticker: str) -> SymbolMetadata:
|
||||||
"""
|
"""
|
||||||
Get metadata for a ticker (e.g., "BINANCE:BTC/USDT").
|
Get metadata for a ticker (e.g., "BTC/USDT.BINANCE").
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier in format "EXCHANGE:SYMBOL"
|
ticker: Market identifier in Nautilus format "MARKET.EXCHANGE"
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
SymbolMetadata with denominators and market info
|
SymbolMetadata with Nautilus instrument fields
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If ticker format is invalid or metadata not found
|
ValueError: If ticker format is invalid or metadata not found
|
||||||
RuntimeError: If symbol_metadata table cannot be loaded
|
|
||||||
"""
|
"""
|
||||||
# Check cache first
|
|
||||||
if ticker in self._cache:
|
if ticker in self._cache:
|
||||||
return self._cache[ticker]
|
return self._cache[ticker]
|
||||||
|
|
||||||
# Parse ticker into exchange_id and market_id
|
exchange_id, market_id = parse_ticker(ticker)
|
||||||
if ":" not in ticker:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid ticker format '{ticker}'. Expected format: 'EXCHANGE:SYMBOL' "
|
|
||||||
f"(e.g., 'BINANCE:BTC/USDT')"
|
|
||||||
)
|
|
||||||
|
|
||||||
exchange_id, market_id = ticker.split(":", 1)
|
|
||||||
|
|
||||||
# Query Iceberg for this symbol
|
|
||||||
try:
|
try:
|
||||||
df = self.table.scan(
|
df = self.table.scan(
|
||||||
row_filter=And(
|
row_filter=And(
|
||||||
@@ -122,9 +139,9 @@ class SymbolMetadataClient:
|
|||||||
|
|
||||||
if df.empty:
|
if df.empty:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"No metadata found for ticker '{ticker}' (exchange_id='{exchange_id}', "
|
f"No metadata found for ticker '{ticker}' "
|
||||||
f"market_id='{market_id}'). The symbol may not be configured in the system. "
|
f"(exchange_id='{exchange_id}', market_id='{market_id}'). "
|
||||||
f"Available tickers can be queried from the symbol_metadata table."
|
f"The symbol may not be configured in the system."
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(df) > 1:
|
if len(df) > 1:
|
||||||
@@ -132,55 +149,44 @@ class SymbolMetadataClient:
|
|||||||
|
|
||||||
row = df.iloc[0]
|
row = df.iloc[0]
|
||||||
|
|
||||||
# Extract denominators (required fields)
|
def _opt_int(col):
|
||||||
tick_denom = row.get("tick_denom")
|
v = row.get(col)
|
||||||
base_denom = row.get("base_denom")
|
return int(v) if v is not None and not (isinstance(v, float) and v != v) else None
|
||||||
quote_denom = row.get("quote_denom")
|
|
||||||
|
|
||||||
if tick_denom is None or tick_denom == 0:
|
def _opt_float(col):
|
||||||
raise ValueError(
|
v = row.get(col)
|
||||||
f"Invalid tick_denom for {ticker}: {tick_denom}. "
|
return float(v) if v is not None and not (isinstance(v, float) and v != v) else None
|
||||||
f"Denominator must be a positive integer."
|
|
||||||
)
|
|
||||||
|
|
||||||
if base_denom is None or base_denom == 0:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid base_denom for {ticker}: {base_denom}. "
|
|
||||||
f"Denominator must be a positive integer."
|
|
||||||
)
|
|
||||||
|
|
||||||
if quote_denom is None or quote_denom == 0:
|
|
||||||
raise ValueError(
|
|
||||||
f"Invalid quote_denom for {ticker}: {quote_denom}. "
|
|
||||||
f"Denominator must be a positive integer."
|
|
||||||
)
|
|
||||||
|
|
||||||
metadata = SymbolMetadata(
|
metadata = SymbolMetadata(
|
||||||
exchange_id=exchange_id,
|
exchange_id=exchange_id,
|
||||||
market_id=market_id,
|
market_id=market_id,
|
||||||
tick_denom=int(tick_denom),
|
|
||||||
base_denom=int(base_denom),
|
|
||||||
quote_denom=int(quote_denom),
|
|
||||||
market_type=row.get("market_type"),
|
market_type=row.get("market_type"),
|
||||||
description=row.get("description"),
|
description=row.get("description"),
|
||||||
|
base_asset=row.get("base_asset"),
|
||||||
|
quote_asset=row.get("quote_asset"),
|
||||||
|
price_precision=_opt_int("price_precision"),
|
||||||
|
size_precision=_opt_int("size_precision"),
|
||||||
|
tick_size=_opt_float("tick_size"),
|
||||||
|
lot_size=_opt_float("lot_size"),
|
||||||
|
min_notional=_opt_float("min_notional"),
|
||||||
|
margin_init=_opt_float("margin_init"),
|
||||||
|
margin_maint=_opt_float("margin_maint"),
|
||||||
|
maker_fee=_opt_float("maker_fee"),
|
||||||
|
taker_fee=_opt_float("taker_fee"),
|
||||||
|
contract_multiplier=_opt_float("contract_multiplier"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Cache the result
|
|
||||||
self._cache[ticker] = metadata
|
self._cache[ticker] = metadata
|
||||||
log.debug(
|
log.debug(
|
||||||
f"Loaded metadata for {ticker}: tick_denom={metadata.tick_denom}, "
|
f"Loaded metadata for {ticker}: price_precision={metadata.price_precision}, "
|
||||||
f"base_denom={metadata.base_denom}, quote_denom={metadata.quote_denom}"
|
f"tick_size={metadata.tick_size}, maker_fee={metadata.maker_fee}"
|
||||||
)
|
)
|
||||||
|
|
||||||
return metadata
|
return metadata
|
||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
# Re-raise ValueError as-is (ticker not found, invalid format, etc.)
|
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(f"Failed to query metadata for ticker '{ticker}': {e}") from e
|
||||||
f"Failed to query metadata for ticker '{ticker}': {e}"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
def clear_cache(self):
|
def clear_cache(self):
|
||||||
"""Clear the metadata cache (useful for testing or forcing reloads)."""
|
"""Clear the metadata cache (useful for testing or forcing reloads)."""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Utility functions for dexorder.
|
Utility functions for dexorder.
|
||||||
|
|
||||||
Includes timestamp conversions, date parsing, and other common utilities.
|
Includes timestamp conversions, date parsing, and other common utilities.
|
||||||
|
All internal timestamps use nanoseconds since epoch (UTC).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@@ -15,13 +16,15 @@ log = logging.getLogger(__name__)
|
|||||||
# Type alias for flexible timestamp input
|
# Type alias for flexible timestamp input
|
||||||
TimestampInput = Union[int, float, str, datetime, pd.Timestamp]
|
TimestampInput = Union[int, float, str, datetime, pd.Timestamp]
|
||||||
|
|
||||||
|
NANOS_PER_SECOND = 1_000_000_000
|
||||||
|
|
||||||
def to_microseconds(timestamp: TimestampInput) -> int:
|
|
||||||
|
def to_nanoseconds(timestamp: TimestampInput) -> int:
|
||||||
"""
|
"""
|
||||||
Convert various timestamp formats to microseconds since epoch.
|
Convert various timestamp formats to nanoseconds since epoch.
|
||||||
|
|
||||||
This is the canonical way to convert user-friendly timestamps (unix seconds,
|
This is the canonical way to convert user-friendly timestamps (unix seconds,
|
||||||
date strings, datetime objects) into the internal microsecond format used
|
date strings, datetime objects) into the internal nanosecond format used
|
||||||
throughout the dexorder system.
|
throughout the dexorder system.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -32,87 +35,69 @@ def to_microseconds(timestamp: TimestampInput) -> int:
|
|||||||
- pandas Timestamp
|
- pandas Timestamp
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Microseconds since epoch as integer
|
Nanoseconds since epoch as integer
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> to_microseconds(1640000000) # Unix timestamp in seconds
|
>>> to_nanoseconds(1640000000) # Unix timestamp in seconds
|
||||||
1640000000000000
|
1640000000000000000
|
||||||
>>> to_microseconds(1640000000.5) # Unix timestamp with fractional seconds
|
>>> to_nanoseconds(1640000000.5) # Unix timestamp with fractional seconds
|
||||||
1640000000500000
|
1640000000500000000
|
||||||
>>> to_microseconds("2021-12-20") # Date string
|
>>> to_nanoseconds("2021-12-20")
|
||||||
1640000000000000
|
1639958400000000000
|
||||||
>>> to_microseconds("2021-12-20 12:00:00") # Date string with time
|
|
||||||
1640000000000000
|
|
||||||
>>> to_microseconds(datetime(2021, 12, 20, 12, 0, 0)) # datetime object
|
|
||||||
1640000000000000
|
|
||||||
>>> to_microseconds(pd.Timestamp("2021-12-20 12:00:00")) # pandas Timestamp
|
|
||||||
1640000000000000
|
|
||||||
"""
|
"""
|
||||||
if isinstance(timestamp, (int, float)):
|
if isinstance(timestamp, (int, float)):
|
||||||
# Assume Unix timestamp in seconds
|
return int(timestamp * NANOS_PER_SECOND)
|
||||||
return int(timestamp * 1_000_000)
|
|
||||||
elif isinstance(timestamp, str):
|
elif isinstance(timestamp, str):
|
||||||
# Parse date string
|
|
||||||
dt = dateparser.parse(timestamp)
|
dt = dateparser.parse(timestamp)
|
||||||
if dt is None:
|
if dt is None:
|
||||||
raise ValueError(f"Could not parse date string: {timestamp}")
|
raise ValueError(f"Could not parse date string: {timestamp}")
|
||||||
return int(dt.timestamp() * 1_000_000)
|
return int(dt.timestamp() * NANOS_PER_SECOND)
|
||||||
elif isinstance(timestamp, datetime):
|
elif isinstance(timestamp, datetime):
|
||||||
return int(timestamp.timestamp() * 1_000_000)
|
return int(timestamp.timestamp() * NANOS_PER_SECOND)
|
||||||
elif isinstance(timestamp, pd.Timestamp):
|
elif isinstance(timestamp, pd.Timestamp):
|
||||||
return int(timestamp.timestamp() * 1_000_000)
|
return int(timestamp.timestamp() * NANOS_PER_SECOND)
|
||||||
else:
|
else:
|
||||||
raise TypeError(f"Unsupported timestamp type: {type(timestamp)}")
|
raise TypeError(f"Unsupported timestamp type: {type(timestamp)}")
|
||||||
|
|
||||||
|
|
||||||
def to_seconds(timestamp_micros: int) -> float:
|
def to_seconds(timestamp_nanos: int) -> float:
|
||||||
"""
|
"""
|
||||||
Convert microseconds since epoch to Unix timestamp in seconds.
|
Convert nanoseconds since epoch to Unix timestamp in seconds.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
timestamp_micros: Timestamp in microseconds since epoch
|
timestamp_nanos: Timestamp in nanoseconds since epoch
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Unix timestamp in seconds (float)
|
Unix timestamp in seconds (float)
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
>>> to_seconds(1640000000000000)
|
>>> to_seconds(1640000000000000000)
|
||||||
1640000000.0
|
1640000000.0
|
||||||
>>> to_seconds(1640000000500000)
|
|
||||||
1640000000.5
|
|
||||||
"""
|
"""
|
||||||
return timestamp_micros / 1_000_000
|
return timestamp_nanos / NANOS_PER_SECOND
|
||||||
|
|
||||||
|
|
||||||
def to_datetime(timestamp_micros: int) -> datetime:
|
def to_datetime(timestamp_nanos: int) -> datetime:
|
||||||
"""
|
"""
|
||||||
Convert microseconds since epoch to datetime object.
|
Convert nanoseconds since epoch to datetime object (UTC).
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
timestamp_micros: Timestamp in microseconds since epoch
|
timestamp_nanos: Timestamp in nanoseconds since epoch
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
datetime object in UTC
|
datetime object in UTC
|
||||||
|
|
||||||
Examples:
|
|
||||||
>>> to_datetime(1640000000000000)
|
|
||||||
datetime.datetime(2021, 12, 20, 12, 0, tzinfo=datetime.timezone.utc)
|
|
||||||
"""
|
"""
|
||||||
return datetime.fromtimestamp(timestamp_micros / 1_000_000)
|
return datetime.fromtimestamp(timestamp_nanos / NANOS_PER_SECOND)
|
||||||
|
|
||||||
|
|
||||||
def to_timestamp(timestamp_micros: int) -> pd.Timestamp:
|
def to_timestamp(timestamp_nanos: int) -> pd.Timestamp:
|
||||||
"""
|
"""
|
||||||
Convert microseconds since epoch to pandas Timestamp.
|
Convert nanoseconds since epoch to pandas Timestamp.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
timestamp_micros: Timestamp in microseconds since epoch
|
timestamp_nanos: Timestamp in nanoseconds since epoch
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
pandas Timestamp
|
pandas Timestamp
|
||||||
|
|
||||||
Examples:
|
|
||||||
>>> to_timestamp(1640000000000000)
|
|
||||||
Timestamp('2021-12-20 12:00:00')
|
|
||||||
"""
|
"""
|
||||||
return pd.Timestamp(timestamp_micros, unit='us')
|
return pd.Timestamp(timestamp_nanos, unit='ns')
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ This will:
|
|||||||
### Expected Flow
|
### Expected Flow
|
||||||
|
|
||||||
1. **Client** sends OHLCRequest to Flink (REQ/REP)
|
1. **Client** sends OHLCRequest to Flink (REQ/REP)
|
||||||
- Ticker: `BINANCE:BTC/USDT`
|
- Ticker: `BTC/USDT.BINANCE`
|
||||||
- Period: 3600s (1 hour)
|
- Period: 3600s (1 hour)
|
||||||
- Range: Jan 1-7, 2026
|
- Range: Jan 1-7, 2026
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ python client.py
|
|||||||
## What it does
|
## What it does
|
||||||
|
|
||||||
1. Connects to Flink's client request endpoint (REQ/REP on port 5559)
|
1. Connects to Flink's client request endpoint (REQ/REP on port 5559)
|
||||||
2. Requests 1-hour OHLC candles for BINANCE:BTC/USDT
|
2. Requests 1-hour OHLC candles for BTC/USDT.BINANCE
|
||||||
3. Time range: January 1-7, 2026 (168 candles)
|
3. Time range: January 1-7, 2026 (168 candles)
|
||||||
4. Waits for Flink to respond (up to 30 seconds)
|
4. Waits for Flink to respond (up to 30 seconds)
|
||||||
5. Displays the response status and sample data
|
5. Displays the response status and sample data
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class HistoryClient:
|
|||||||
Request historical OHLC data via Relay.
|
Request historical OHLC data via Relay.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier (e.g., "BINANCE:BTC/USDT")
|
ticker: Market identifier (e.g., "BTC/USDT.BINANCE")
|
||||||
start_time: Start timestamp in microseconds since epoch
|
start_time: Start timestamp in microseconds since epoch
|
||||||
end_time: End timestamp in microseconds since epoch
|
end_time: End timestamp in microseconds since epoch
|
||||||
period_seconds: OHLC period in seconds (e.g., 3600 for 1h)
|
period_seconds: OHLC period in seconds (e.g., 3600 for 1h)
|
||||||
@@ -161,7 +161,7 @@ def main():
|
|||||||
# Connect to Relay
|
# Connect to Relay
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
# Request BINANCE:BTC/USDT 1h candles for first 7 days of January 2026
|
# Request BTC/USDT.BINANCE 1h candles for first 7 days of January 2026
|
||||||
# January 1, 2026 00:00:00 UTC = 1735689600 seconds = 1735689600000000 microseconds
|
# January 1, 2026 00:00:00 UTC = 1735689600 seconds = 1735689600000000 microseconds
|
||||||
# January 7, 2026 23:59:59 UTC = 1736294399 seconds = 1736294399000000 microseconds
|
# January 7, 2026 23:59:59 UTC = 1736294399 seconds = 1736294399000000 microseconds
|
||||||
|
|
||||||
@@ -169,7 +169,7 @@ def main():
|
|||||||
end_time_us = 1736294399 * 1_000_000 # Jan 7, 2026 23:59:59 UTC
|
end_time_us = 1736294399 * 1_000_000 # Jan 7, 2026 23:59:59 UTC
|
||||||
|
|
||||||
response = client.request_historical_ohlc(
|
response = client.request_historical_ohlc(
|
||||||
ticker='BINANCE:BTC/USDT',
|
ticker='BTC/USDT.BINANCE',
|
||||||
start_time=start_time_us,
|
start_time=start_time_us,
|
||||||
end_time=end_time_us,
|
end_time=end_time_us,
|
||||||
period_seconds=3600, # 1 hour
|
period_seconds=3600, # 1 hour
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ class AsyncHistoryClient:
|
|||||||
3. Query Iceberg with the table information (or notification includes data)
|
3. Query Iceberg with the table information (or notification includes data)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ticker: Market identifier (e.g., "BINANCE:BTC/USDT")
|
ticker: Market identifier (e.g., "BTC/USDT.BINANCE")
|
||||||
start_time: Start timestamp in microseconds since epoch
|
start_time: Start timestamp in microseconds since epoch
|
||||||
end_time: End timestamp in microseconds since epoch
|
end_time: End timestamp in microseconds since epoch
|
||||||
period_seconds: OHLC period in seconds (e.g., 3600 for 1h)
|
period_seconds: OHLC period in seconds (e.g., 3600 for 1h)
|
||||||
@@ -263,12 +263,12 @@ def main():
|
|||||||
# Connect
|
# Connect
|
||||||
client.connect()
|
client.connect()
|
||||||
|
|
||||||
# Request BINANCE:BTC/USDT 1h candles for first 7 days of January 2026
|
# Request BTC/USDT.BINANCE 1h candles for first 7 days of January 2026
|
||||||
start_time_us = 1735689600 * 1_000_000 # Jan 1, 2026 00:00:00 UTC
|
start_time_us = 1735689600 * 1_000_000 # Jan 1, 2026 00:00:00 UTC
|
||||||
end_time_us = 1736294399 * 1_000_000 # Jan 7, 2026 23:59:59 UTC
|
end_time_us = 1736294399 * 1_000_000 # Jan 7, 2026 23:59:59 UTC
|
||||||
|
|
||||||
notification = client.request_historical_ohlc(
|
notification = client.request_historical_ohlc(
|
||||||
ticker='BINANCE:BTC/USDT',
|
ticker='BTC/USDT.BINANCE',
|
||||||
start_time=start_time_us,
|
start_time=start_time_us,
|
||||||
end_time=end_time_us,
|
end_time=end_time_us,
|
||||||
period_seconds=3600, # 1 hour
|
period_seconds=3600, # 1 hour
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ async def main():
|
|||||||
print("✅ Client started\n")
|
print("✅ Client started\n")
|
||||||
|
|
||||||
# Request parameters
|
# Request parameters
|
||||||
ticker = "BINANCE:BTC/USDT"
|
ticker = "BTC/USDT.BINANCE"
|
||||||
period_seconds = 3600 # 1-hour candles
|
period_seconds = 3600 # 1-hour candles
|
||||||
|
|
||||||
# Request 7 days of data (Jan 1-7, 2026)
|
# Request 7 days of data (Jan 1-7, 2026)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export interface ChartState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useChartStore = defineStore('chartState', () => {
|
export const useChartStore = defineStore('chartState', () => {
|
||||||
const symbol = ref<string>('BINANCE:BTC/USDT')
|
const symbol = ref<string>('BTC/USDT.BINANCE')
|
||||||
const start_time = ref<number | null>(null)
|
const start_time = ref<number | null>(null)
|
||||||
const end_time = ref<number | null>(null)
|
const end_time = ref<number | null>(null)
|
||||||
const period = ref<number>(900) // seconds; default 15 minutes
|
const period = ref<number>(900) // seconds; default 15 minutes
|
||||||
|
|||||||
Reference in New Issue
Block a user