package br.com.kronus.ibkr.api;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import com.ib.client.Bar;
import com.ib.client.Contract;
import com.ib.client.Decimal;
import com.ib.client.DefaultEWrapper;
import com.ib.client.EClientSocket;
import com.ib.client.EJavaSignal;
import com.ib.client.EReader;
import com.ib.client.Execution;
import com.ib.client.Order;
import com.ib.client.OrderState;
import com.ib.client.TickAttrib;
import br.com.kronus.core.StatusOrdemFuturos;
/**
* Cliente IBKR alinhado com a API nova (JavaClient),
* centralizando:
* - próximo orderId;
* - futures de status de ordens;
* - futures de preços (tickPrice);
* - listeners de realtimeBar;
* - futures de histórico (historicalData).
*/
public class IbkrClient
extends DefaultEWrapper
{
private final EJavaSignal signal =
new EJavaSignal
();
private final EClientSocket client =
new EClientSocket
(this, signal
);
// ===== Ordens =====
private final Map<Integer, CompletableFuture
<StatusOrdemFuturos
>> ordemFutures =
new ConcurrentHashMap<>();
// ===== Preço "snapshot" via tickPrice =====
private final Map<Integer, CompletableFuture
<BigDecimal>> precoFutures =
new ConcurrentHashMap<>();
// ===== Realtime bars =====
public interface RealtimeBarListener
{
void onRealtimeBar
(int reqId,
long time,
double open,
double high,
double low,
double close,
long volume,
double wap,
int count
);
}
private final Map<Integer, RealtimeBarListener
> realtimeBarListeners =
new ConcurrentHashMap<>();
// ===== Histórico =====
private final Map<Integer,
List<Bar
>> historicalBars =
new ConcurrentHashMap<>();
private final Map<Integer, CompletableFuture
<List<Bar
>>> historicalFutures =
new ConcurrentHashMap<>();
private volatile int nextOrderId = -
1;
// =========================================================
// Conexão
// =========================================================
public void conectar
(String host,
int port,
int clientId
) {
client.
eConnect(host, port, clientId
);
EReader reader =
new EReader
(client, signal
);
reader.
start();
Thread t =
new Thread(() -
> {
while (client.
isConnected()) {
signal.
waitForSignal();
try {
reader.
processMsgs();
} catch (Exception e
) {
e.
printStackTrace();
}
}
},
"IbkrClient-Reader");
t.
setDaemon(true);
t.
start();
client.
reqMarketDataType(1);
}
public void desconectar
() {
client.
eDisconnect();
}
public EClientSocket getClient
() {
return client
;
}
// =========================================================
// Helpers gerais
// =========================================================
public int getNextOrderIdBlocking
(Duration timeout
) {
Instant inicio = Instant.
now();
while (nextOrderId
<=
0 &&
Duration.
between(inicio, Instant.
now()).
compareTo(timeout
) < 0) {
try {
Thread.
sleep(50);
} catch (InterruptedException e
) {
Thread.
currentThread().
interrupt();
break;
}
}
if (nextOrderId
<=
0) {
throw new IllegalStateException("IBKR não retornou nextValidId dentro do timeout.");
}
return nextOrderId++
;
}
// ===== Ordens =====
public CompletableFuture
<StatusOrdemFuturos
> registrarFutureOrdem
(int orderId
) {
CompletableFuture
<StatusOrdemFuturos
> future =
new CompletableFuture
<>();
ordemFutures.
put(orderId, future
);
return future
;
}
// ===== Preço (tickPrice) =====
public CompletableFuture
<BigDecimal> registrarFuturePreco
(int tickerId
) {
CompletableFuture
<BigDecimal> future =
new CompletableFuture
<>();
precoFutures.
put(tickerId, future
);
return future
;
}
// ===== Realtime bars =====
public void registrarRealtimeBarListener
(int reqId, RealtimeBarListener listener
) {
realtimeBarListeners.
put(reqId, listener
);
}
// ===== Histórico =====
public CompletableFuture
<List<Bar
>> prepararFutureHistorico
(int reqId
) {
CompletableFuture
<List<Bar
>> fut =
new CompletableFuture
<>();
historicalFutures.
put(reqId, fut
);
historicalBars.
put(reqId,
new ArrayList<>());
return fut
;
}
// =========================================================
// Callbacks obrigatórios / usados
// =========================================================
@
Override
public void nextValidId
(int orderId
) {
this.
nextOrderId = orderId
;
}
@
Override
public void orderStatus
(int orderId,
String status,
Decimal filled,
Decimal remaining,
double avgFillPrice,
long permId,
int parentId,
double lastFillPrice,
int clientId,
String whyHeld,
double mktCapPrice
) {
double filledD =
(filled
!=
null && filled.
isValid()) ? filled.
value().
doubleValue() :
0.0;
double remainingD =
(remaining
!=
null && remaining.
isValid()) ? remaining.
value().
doubleValue() :
0.0;
StatusOrdemFuturos s =
new StatusOrdemFuturos
();
s.
setOrderId((long) orderId
);
s.
setStatus(status
!=
null ? status.
toUpperCase() :
null);
s.
setExecutedQty(BigDecimal.
valueOf(filledD
));
s.
setOrigQty(BigDecimal.
valueOf(filledD + remainingD
));
s.
setAvgPrice(BigDecimal.
valueOf(avgFillPrice
));
CompletableFuture
<StatusOrdemFuturos
> future = ordemFutures.
get(orderId
);
if (future
!=
null && !future.
isDone()) {
future.
complete(s
);
}
}
@
Override
public void openOrder
(int orderId, Contract contract, Order order, OrderState orderState
) {
StatusOrdemFuturos s =
new StatusOrdemFuturos
();
s.
setOrderId((long) orderId
);
s.
setSymbol(contract.
symbol());
s.
setStatus(orderState.
status() !=
null ? orderState.
status().
toString().
toUpperCase() :
null);
s.
setPrice(BigDecimal.
valueOf(order.
lmtPrice()));
// totalQuantity é Decimal no JavaClient novo
s.
setOrigQty(BigDecimal.
valueOf(order.
totalQuantity().
longValue()));
CompletableFuture
<StatusOrdemFuturos
> future = ordemFutures.
get(orderId
);
if (future
!=
null && !future.
isDone()) {
future.
complete(s
);
}
}
@
Override
public void tickPrice
(int tickerId,
int field,
double price, TickAttrib attribs
) {
if (price
<=
0) return;
CompletableFuture
<BigDecimal> future = precoFutures.
get(tickerId
);
if (future
!=
null && !future.
isDone()) {
future.
complete(BigDecimal.
valueOf(price
));
}
}
// ===== Realtime bars =====
@
Override
public void realtimeBar
(int reqId,
long time,
double open,
double high,
double low,
double close,
Decimal volume,
Decimal wap,
int count
) {
RealtimeBarListener l = realtimeBarListeners.
get(reqId
);
if (l
!=
null) {
long vol =
(volume
!=
null && volume.
isValid()) ? volume.
longValue() : 0L
;
double wapD =
(wap
!=
null && wap.
isValid()) ? wap.
value().
doubleValue() :
0.0;
l.
onRealtimeBar(reqId, time, open, high, low, close, vol, wapD, count
);
}
}
// ===== Histórico =====
@
Override
public void historicalData
(int reqId, Bar bar
) {
List<Bar
> lista = historicalBars.
computeIfAbsent(reqId, k -
> new ArrayList<>());
lista.
add(bar
);
}
@
Override
public void historicalDataEnd
(int reqId,
String startDateStr,
String endDateStr
) {
List<Bar
> lista = historicalBars.
get(reqId
);
CompletableFuture
<List<Bar
>> fut = historicalFutures.
get(reqId
);
if (fut
!=
null && lista
!=
null && !fut.
isDone()) {
fut.
complete(lista
);
}
}
// ===== Erros =====
@
Override
public void error
(Exception e
) {
e.
printStackTrace();
}
@
Override
public void error
(String str
) {
System.
err.
println("IBKR ERROR: " + str
);
}
@
Override
public void error
(int id,
long errorTime,
int errorCode,
String errorMsg,
String advancedOrderRejectJson
) {
// Mensagens informativas de conexão de data farm
if (errorCode ==
2158 // Sec-def data farm connection is OK
|| errorCode ==
2157 // Sec-def data farm connection is broken
|| errorCode ==
2104 // Market data farm connection is OK
|| errorCode ==
2106 // HMDS data farm connection is OK
) {
System.
out.
println("[IBKR INFO] time=" + errorTime +
", id=" + id +
", code=" + errorCode +
", msg=" + errorMsg
);
return;
}
System.
err.
println("IBKR ERROR: time=" + errorTime +
", id=" + id +
", code=" + errorCode +
", msg=" + errorMsg
);
CompletableFuture
<StatusOrdemFuturos
> of = ordemFutures.
get(id
);
if (of
!=
null && !of.
isDone()) {
of.
completeExceptionally(
new RuntimeException("IBKR error " + errorCode +
": " + errorMsg
));
}
CompletableFuture
<BigDecimal> pf = precoFutures.
get(id
);
if (pf
!=
null && !pf.
isDone()) {
pf.
completeExceptionally(
new RuntimeException("IBKR error " + errorCode +
": " + errorMsg
));
}
CompletableFuture
<List<Bar
>> hf = historicalFutures.
get(id
);
if (hf
!=
null && !hf.
isDone()) {
hf.
completeExceptionally(
new RuntimeException("IBKR error " + errorCode +
": " + errorMsg
));
}
}
// Execuções (se quiser aproveitar depois)
@
Override
public void execDetails
(int reqId, Contract contract, Execution execution
) {
// opcional: logar execuções
}
}