Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
cc6df55
feat(cmd): bootstrap multi-venue emitters
terwey-agent Nov 4, 2025
35472d7
fix: register default hyperliquid emitter alias
terwey-agent Nov 4, 2025
55d97d7
Merge pull request #70 from recomma/codex/github-mention-feat-bootstr…
terwey-agent Nov 4, 2025
b229bc5
fix: skip duplicate hyperliquid venue upsert
terwey-agent Nov 4, 2025
1566477
fix: alias hyperliquid primary clients
terwey-agent Nov 4, 2025
db030bb
fix: align default hyperliquid venue aliasing
terwey-agent Nov 4, 2025
bb97cf9
docs: clarify default hyperliquid wallet handling
terwey-agent Nov 4, 2025
575af6b
Merge pull request #71 from recomma/codex/update-multi_venue_emission…
terwey-agent Nov 4, 2025
365b460
fix: align default hyperliquid sentinel guard
terwey-agent Nov 4, 2025
9ce9cbe
fix(storage): prefer primary venue in default assignment
terwey-agent Nov 4, 2025
438fcd3
fix: bootstrap default hyperliquid websocket venue
terwey-agent Nov 4, 2025
cd763cf
sql: remove duplicates
terwey Nov 5, 2025
aeaf0c2
chore: clarify orderid purpose
terwey Nov 5, 2025
32b3173
chore: add CLAUDE.md
terwey Nov 5, 2025
073b3c4
chore: specify go version
terwey Nov 5, 2025
a4b8c76
fix: remove pointer fields from Action struct to make OrderWork compa…
claude Nov 5, 2025
d90e115
fix: correct remaining pointer-to-value conversions
claude Nov 5, 2025
30a763f
fix: simplify cloneAction since Action fields are now values
claude Nov 5, 2025
7092f95
refactor: remove not needed cloneAction
terwey Nov 5, 2025
c5899a4
Merge pull request #75 from recomma/claude/fix-orderwork-comparabilit…
terwey Nov 5, 2025
a2f5fd0
fix: resolve multi-venue scaled order bugs
claude Nov 5, 2025
5fd46f2
fix: update tests to use OrderIdentifier
claude Nov 5, 2025
ed82f06
chore: gofmt and go generate
terwey Nov 5, 2025
5808b73
fix: match scaled orders by venue in filltracker
claude Nov 5, 2025
c12d10e
Merge pull request #76 from recomma/claude/fix-multi-venue-emission-0…
terwey Nov 5, 2025
c569912
sql: add CloneScaledOrdersToWallet
terwey Nov 5, 2025
88216d0
fix(storage): migrate scaled_orders during wallet changes
claude Nov 5, 2025
51cf587
Merge pull request #77 from recomma/claude/fix-scaled-orders-wallet-m…
terwey Nov 5, 2025
29f7841
fix: resolve multi-venue reconciliation and memory issues
claude Nov 5, 2025
60f1114
test: update filltracker tests for ActiveTakeProfits slice
claude Nov 5, 2025
08680ac
fix: update engine to use ActiveTakeProfits slice
claude Nov 5, 2025
2e77f20
fix: calculate per-venue positions for take-profit reconciliation
claude Nov 5, 2025
5a73adf
fix: properly aggregate positions by venue+wallet, not by OrderId
claude Nov 5, 2025
cc74e20
fix: use correct TP OrderId when creating venue take-profits
claude Nov 5, 2025
752696e
fix: include filled reduce-only orders in venue position calculation
claude Nov 5, 2025
2945981
fix: defer multi-venue TP sizing to reconciliation in engine
claude Nov 5, 2025
bb4da9f
Merge pull request #78 from recomma/claude/fix-multi-venue-reconcilia…
terwey Nov 5, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Introduction
This file is a helper to prepare your environment for the task at hand. Refer to `AGENTS.md` for more detailed instructions.

# Setup
To be able to run the go tests we need to ensure everything is available.
We require Go version 1.25

## Commands
go mod download

# OpenAPI
This project requires some Go tools for code generation. To pre-warm the generation process you can do the following commands.

## Commands
git submodule update --init --recursive
go tool github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen -version

# SQLc
The schema and queries are stored in package storage and require sqlc

## Commands
go tool github.com/sqlc-dev/sqlc/cmd/sqlc version
6 changes: 3 additions & 3 deletions adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,20 +108,20 @@ func BuildAction(
case prev == nil && nextState == stateActive:
// New order we have never submitted → create
req := ToCreateOrderRequest(currency, next, oid)
return recomma.Action{Type: recomma.ActionCreate, Create: &req}
return recomma.Action{Type: recomma.ActionCreate, Create: req}
case prev == nil:
// Never saw it active; HL won’t have it either
return recomma.Action{Type: recomma.ActionNone, Reason: "skipped: no prior order submitted"}
case prevState == stateActive && nextState == stateCancelled:
cancel := ToCancelByCloidRequest(currency, oid)
return recomma.Action{Type: recomma.ActionCancel, Cancel: &cancel}
return recomma.Action{Type: recomma.ActionCancel, Cancel: cancel}
case prevState == stateActive && nextState == stateFilled:
// Nothing to do on Hyperliquid, but tell caller to persist updated snapshot
return recomma.Action{Type: recomma.ActionNone, Reason: "filled locally"}
case prevState == stateActive && nextState == stateActive:
if needsModify(prev, &next) {
modify := ToModifyOrderRequest(currency, next, oid)
return recomma.Action{Type: recomma.ActionModify, Modify: &modify}
return recomma.Action{Type: recomma.ActionModify, Modify: modify}
}
return recomma.Action{Type: recomma.ActionNone, Reason: "no material change"}
default:
Expand Down
57 changes: 57 additions & 0 deletions cmd/recomma/hyperliquid_bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package main

import (
"context"

"github.com/recomma/recomma/emitter"
"github.com/recomma/recomma/hl"
"github.com/recomma/recomma/hl/ws"
"github.com/recomma/recomma/recomma"
"github.com/sonirico/go-hyperliquid"
)

type hyperliquidStatusClient interface {
QueryOrderByCloid(ctx context.Context, cloid string) (*hyperliquid.OrderQueryResult, error)
}

func registerHyperliquidEmitter(queue *emitter.QueueEmitter, submitter recomma.Emitter, venueIdent, primaryIdent, defaultIdent recomma.VenueID) {
if queue == nil || submitter == nil {
return
}

queue.Register(venueIdent, submitter)

if venueIdent != primaryIdent {
return
}
if defaultIdent == "" || defaultIdent == venueIdent {
return
}

queue.Register(defaultIdent, submitter)
}

func registerHyperliquidStatusClient(reg hl.StatusClientRegistry, client hyperliquidStatusClient, venueIdent, primaryIdent, defaultIdent recomma.VenueID) {
if reg == nil || client == nil {
return
}

reg[venueIdent] = client

if venueIdent != primaryIdent {
return
}
if defaultIdent == "" || defaultIdent == venueIdent {
return
}

reg[defaultIdent] = client
}

func registerHyperliquidWsClient(reg map[recomma.VenueID]*ws.Client, client *ws.Client, venueIdent recomma.VenueID) {
if reg == nil || client == nil {
return
}

reg[venueIdent] = client
}
190 changes: 190 additions & 0 deletions cmd/recomma/hyperliquid_bootstrap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
package main

import (
"context"
"errors"
"testing"

"github.com/recomma/recomma/emitter"
"github.com/recomma/recomma/hl"
"github.com/recomma/recomma/hl/ws"
"github.com/recomma/recomma/internal/vault"
"github.com/recomma/recomma/orderid"
"github.com/recomma/recomma/recomma"
"github.com/recomma/recomma/storage"
"github.com/sonirico/go-hyperliquid"
)

type stubQueue struct {
items []recomma.OrderWork
}

func (q *stubQueue) Add(item recomma.OrderWork) {
q.items = append(q.items, item)
}

type stubEmitter struct {
calls int
last recomma.OrderWork
}

func (e *stubEmitter) Emit(_ context.Context, w recomma.OrderWork) error {
e.calls++
e.last = w
return nil
}

type stubStatusClient struct{}

func (stubStatusClient) QueryOrderByCloid(context.Context, string) (*hyperliquid.OrderQueryResult, error) {
return nil, nil
}

func TestRegisterHyperliquidEmitterRegistersDefaultAlias(t *testing.T) {
queue := emitter.NewQueueEmitter(&stubQueue{})
submitter := &stubEmitter{}

primaryIdent := recomma.VenueID("primary")
defaultIdent := storage.DefaultHyperliquidIdentifier(orderid.OrderId{}).VenueID

registerHyperliquidEmitter(queue, submitter, primaryIdent, primaryIdent, defaultIdent)

oid := orderid.OrderId{BotID: 1, DealID: 2, BotEventID: 3}
ident := storage.DefaultHyperliquidIdentifier(oid)
work := recomma.OrderWork{Identifier: ident, OrderId: oid}

if err := queue.Dispatch(context.Background(), work); err != nil {
t.Fatalf("dispatch failed: %v", err)
}

if submitter.calls != 1 {
t.Fatalf("expected submitter to be called once, got %d", submitter.calls)
}
if submitter.last.Identifier.VenueID != ident.VenueID {
t.Fatalf("unexpected venue dispatched: got %s want %s", submitter.last.Identifier.VenueID, ident.VenueID)
}
}

func TestRegisterHyperliquidEmitterDoesNotAliasNonPrimary(t *testing.T) {
queue := emitter.NewQueueEmitter(&stubQueue{})
submitter := &stubEmitter{}

venueIdent := recomma.VenueID("secondary")
primaryIdent := recomma.VenueID("primary")
defaultIdent := storage.DefaultHyperliquidIdentifier(orderid.OrderId{}).VenueID

registerHyperliquidEmitter(queue, submitter, venueIdent, primaryIdent, defaultIdent)

oid := orderid.OrderId{BotID: 4, DealID: 5, BotEventID: 6}
venueWork := recomma.OrderWork{Identifier: recomma.NewOrderIdentifier(venueIdent, "wallet", oid), OrderId: oid}
if err := queue.Dispatch(context.Background(), venueWork); err != nil {
t.Fatalf("expected direct venue dispatch to succeed: %v", err)
}
if submitter.calls != 1 {
t.Fatalf("expected submitter to be called once, got %d", submitter.calls)
}

submitter.calls = 0

defaultWork := recomma.OrderWork{Identifier: storage.DefaultHyperliquidIdentifier(oid), OrderId: oid}
err := queue.Dispatch(context.Background(), defaultWork)
if !errors.Is(err, emitter.ErrUnregisteredVenueEmitter) {
t.Fatalf("expected unregistered venue error, got %v", err)
}
if submitter.calls != 0 {
t.Fatalf("expected submitter not to be called, got %d", submitter.calls)
}
}

func TestRegisterHyperliquidStatusClientAliasesDefault(t *testing.T) {
registry := make(hl.StatusClientRegistry)
client := stubStatusClient{}

primaryIdent := recomma.VenueID("hyperliquid:primary")
defaultIdent := storage.DefaultHyperliquidIdentifier(orderid.OrderId{}).VenueID

registerHyperliquidStatusClient(registry, client, primaryIdent, primaryIdent, defaultIdent)

if _, ok := registry[primaryIdent]; !ok {
t.Fatalf("expected primary client to be registered")
}
alias, ok := registry[defaultIdent]
if !ok {
t.Fatalf("expected default alias to be registered")
}
if alias != client {
t.Fatalf("expected alias to reference same client")
}
}

func TestRegisterHyperliquidStatusClientDoesNotAliasNonPrimary(t *testing.T) {
registry := make(hl.StatusClientRegistry)
client := stubStatusClient{}

primaryIdent := recomma.VenueID("hyperliquid:primary")
secondaryIdent := recomma.VenueID("hyperliquid:secondary")
defaultIdent := storage.DefaultHyperliquidIdentifier(orderid.OrderId{}).VenueID

registerHyperliquidStatusClient(registry, client, secondaryIdent, primaryIdent, defaultIdent)

if _, ok := registry[secondaryIdent]; !ok {
t.Fatalf("expected secondary client to be registered")
}
if _, ok := registry[defaultIdent]; ok {
t.Fatalf("expected default alias not to be registered for non-primary venue")
}
}

func TestRegisterHyperliquidWsClientRegistersExactVenue(t *testing.T) {
registry := make(map[recomma.VenueID]*ws.Client)
client := &ws.Client{}

primaryIdent := recomma.VenueID("hyperliquid:primary")
defaultIdent := storage.DefaultHyperliquidIdentifier(orderid.OrderId{}).VenueID

registerHyperliquidWsClient(registry, client, primaryIdent)

if registry[primaryIdent] != client {
t.Fatalf("expected primary websocket client to be registered")
}
if _, ok := registry[defaultIdent]; ok {
t.Fatalf("expected default alias to remain unregistered")
}
}

func TestShouldUseSentinelDefaultHyperliquidWalletSkipsPrimary(t *testing.T) {
defaultIdent := storage.DefaultHyperliquidIdentifier(orderid.OrderId{}).VenueID
venues := []vault.VenueSecret{
{
ID: "hyperliquid:primary",
Type: "hyperliquid",
Wallet: "hl-primary-wallet",
Primary: true,
},
}

if shouldUseSentinelDefaultHyperliquidWallet(venues, recomma.VenueID("hyperliquid:primary"), "hl-primary-wallet", defaultIdent) {
t.Fatalf("expected sentinel wallet guard to ignore the primary venue")
}
}

func TestShouldUseSentinelDefaultHyperliquidWalletDetectsDuplicateWallet(t *testing.T) {
defaultIdent := storage.DefaultHyperliquidIdentifier(orderid.OrderId{}).VenueID
venues := []vault.VenueSecret{
{
ID: "hyperliquid:primary",
Type: "hyperliquid",
Wallet: "hl-primary-wallet",
Primary: true,
},
{
ID: "hyperliquid:alias",
Type: "hyperliquid",
Wallet: "hl-primary-wallet",
},
}

if !shouldUseSentinelDefaultHyperliquidWallet(venues, recomma.VenueID("hyperliquid:primary"), "hl-primary-wallet", defaultIdent) {
t.Fatalf("expected sentinel wallet guard to activate when duplicate wallets exist")
}
}
Loading