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

Skip to content

Conversation

@terwey-agent
Copy link
Collaborator

@terwey-agent terwey-agent commented Nov 4, 2025

Summary

  • iterate decrypted venues to persist configuration, create Hyperliquid clients, and register emitters per venue
  • multiplex websocket price sources and status refreshers across venues while logging configuration and closure
  • route fill-tracker dispatch through the queue emitter so order workers respect venue-aware identifiers

Reviewing

Read specs/multi_venue_emission.adoc

Testing

  • go test ./...

https://chatgpt.com/codex/tasks/task_e_690a21ce97cc83299e5542771a251051

@terwey-agent terwey-agent marked this pull request as ready for review November 4, 2025 16:21
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 291 to 377
for _, venue := range secrets.Secrets.Venues {
if !strings.EqualFold(venue.Type, "hyperliquid") {
runtimeLogger.Warn("unsupported venue type in secrets", slog.String("venue", venue.ID), slog.String("type", venue.Type))
continue
}

venueID := strings.TrimSpace(venue.ID)
if venueID == "" {
fatal("load hyperliquid configuration", errors.New("hyperliquid venue missing identifier"))
}

wallet := strings.TrimSpace(venue.Wallet)
if wallet == "" {
fatal("load hyperliquid configuration", errors.New("hyperliquid venue missing wallet"))
}

displayName := strings.TrimSpace(venue.DisplayName)
if displayName == "" {
displayName = venueID
}

privateKey := strings.TrimSpace(venue.PrivateKey)
if privateKey == "" {
fatal("load hyperliquid configuration", errors.New("hyperliquid venue missing private key"))
}

apiURL := strings.TrimSpace(venue.APIURL)
if apiURL == "" {
apiURL = primaryAPIURL
}

payload := api.VenueUpsertRequest{
Type: "hyperliquid",
DisplayName: displayName,
Wallet: wallet,
}
if flags := cloneVenueFlags(venue.Flags); flags != nil {
payload.Flags = &flags
}

if _, err := store.UpsertVenue(appCtx, venueID, payload); err != nil {
fatal("persist venue configuration", err)
}

exchange, err := hl.NewExchange(appCtx, hl.ClientConfig{
BaseURL: apiURL,
Wallet: wallet,
Key: privateKey,
})
if err != nil {
fatal("create hyperliquid exchange", err)
}

info := hl.NewInfo(appCtx, hl.ClientConfig{
BaseURL: apiURL,
Wallet: wallet,
})
venueIdent := recomma.VenueID(venueID)
statusClients[venueIdent] = info
if constraintsInfo == nil || venueIdent == recomma.VenueID(primaryVenueID) {
constraintsInfo = info
}

wsClient, err := ws.New(appCtx, store, fillTracker, venueIdent, wallet, apiURL)
if err != nil {
fatal("create hyperliquid websocket", err)
}
wsClients[venueIdent] = wsClient
venueOrder = append(venueOrder, venueIdent)

client := wsClient
closeVenue := func() {
if err := client.Close(); err != nil {
runtimeLogger.Debug("websocket close failed", slog.String("venue", venueID), slog.String("error", err.Error()))
}
}
venueClosers = append(venueClosers, closeVenue)

submitter := emitter.NewHyperLiquidEmitter(exchange, wsClient, store,
emitter.WithHyperLiquidEmitterConfig(emitter.HyperLiquidEmitterConfig{
InitialIOCOffsetBps: cfg.HyperliquidIOCInitialOffsetBps,
}),
emitter.WithHyperLiquidEmitterLogger(emitterLogger.With(slog.String("venue", venueID), slog.String("wallet", wallet))),
)

engineEmitter.Register(venueIdent, submitter)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P0 Badge Register primary emitter under default venue identifier

Emitters are now registered only under the IDs provided in the secrets (engineEmitter.Register(venueIdent, submitter)). The engine still creates order work with storage.DefaultHyperliquidIdentifier when a bot has no explicit venue assignment, which hard-codes the constant hyperliquid:default. After this change there is no emitter registered for that identifier unless the secret happens to reuse the same ID, so QueueEmitter.Emit will return ErrUnregisteredVenueEmitter and all orders for unassigned bots will be dropped. This effectively disables order submission in existing deployments. Consider also registering the primary venue under the default identifier (or updating the engine to emit the same venue IDs) so the default path continues to work.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex provide a fix. also if needed extract some of the logic from out of the main func into a file so tests can be added.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Testing

  • go test ./...

View task →

…ap-multi-venue-emitters

fix: register default hyperliquid emitter alias for primary venue
@terwey-agent
Copy link
Collaborator Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 326 to 336
payload := api.VenueUpsertRequest{
Type: "hyperliquid",
DisplayName: displayName,
Wallet: wallet,
}
if flags := cloneVenueFlags(venue.Flags); flags != nil {
payload.Flags = &flags
}

if _, err := store.UpsertVenue(appCtx, venueID, payload); err != nil {
fatal("persist venue configuration", err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid duplicate insert for primary hyperliquid venue

The startup path first calls EnsureDefaultVenueWallet to upsert the default Hyperliquid venue with the primary wallet address. The loop that follows then calls store.UpsertVenue for every configured venue, including the primary one. Because the venues table enforces a unique (type, wallet) pair, inserting a second row with the same wallet as the default row violates the constraint and causes the process to fatal when multiple venues are present. Consider skipping the upsert for the primary venue or updating/removing the default record before inserting the primary so that each (type, wallet) remains unique.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex provide a fix, we can check if it's marked as default and skip inserting it a secondary time.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Testing

  • go test ./...

View task →

@terwey-agent
Copy link
Collaborator Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 352 to 365
info := hl.NewInfo(appCtx, hl.ClientConfig{
BaseURL: apiURL,
Wallet: wallet,
})
statusClients[venueIdent] = info
if constraintsInfo == nil || venueIdent == primaryIdent {
constraintsInfo = info
}

wsClient, err := ws.New(appCtx, store, fillTracker, venueIdent, wallet, apiURL)
if err != nil {
fatal("create hyperliquid websocket", err)
}
wsClients[venueIdent] = wsClient

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Alias default venue in status/websocket clients

Only the decrypted venue ID is used when registering the Hyperliquid Info and websocket clients. Orders that are emitted without an explicit venue assignment still use storage.DefaultHyperliquidIdentifier (e.g. hyperliquid:default), so existing submissions are stored under that identifier while new status updates are recorded under the actual venue ID (e.g. hyperliquid:primary). The foreign key on hyperliquid_status_history will reject those status inserts and the fill tracker never sees updates for default-venue orders. The emitter already aliases the primary venue to the default identifier; the same aliasing is needed here so statusClients and wsClients also include the default venue key for the primary client.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex this is a serious problem please propose a fix. keep the spec in mind

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Aliased the primary Hyperliquid info and websocket clients under both the configured venue ID and the default identifier so legacy default-venue orders keep receiving status updates and websocket events. cmd/recomma/main.goL352-L367
  • Centralized Hyperliquid bootstrap helpers that register emitters, status clients, and websocket clients while applying the primary/default aliasing rules. cmd/recomma/hyperliquid_bootstrap.goL3-L66
  • Expanded bootstrap unit tests to cover the new status/websocket aliasing helpers alongside the existing primary venue upsert heuristic. cmd/recomma/hyperliquid_bootstrap_test.goL3-L170

Testing

  • go test ./...

View task →

@terwey-agent
Copy link
Collaborator Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 337 to 339
if !shouldSkipHyperliquidVenueUpsert(venueIdent, primaryIdent, wallet, primaryWallet, venue.Primary) {
if _, err := store.UpsertVenue(appCtx, venueID, payload); err != nil {
fatal("persist venue configuration", err)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Skip upsert prevents primary venue record from being created

The new loop only persists a venue when shouldSkipHyperliquidVenueUpsert returns false. For the primary Hyperliquid venue this helper returns true whenever the wallet equals primaryWallet, so the call to store.UpsertVenue is skipped in exactly the case where a fresh database needs the primary record inserted. With an empty DB and a secrets file whose primary venue ID differs from hyperliquid:default, the only persisted row remains the default venue created by EnsureDefaultVenueWallet, leaving no venues entry for the actual primary ID. Subsequent API calls or bot assignments referencing that venue ID will fail with ErrVenueNotFound. The primary venue should still be upserted so its identifier, display name, and flags exist in storage even when its wallet matches the default.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex that makes no sense, this should be handled in

recomma/cmd/recomma/main.go

Lines 277 to 279 in 1566477

if err := store.EnsureDefaultVenueWallet(appCtx, primaryWallet); err != nil {
fatal("update default venue wallet", err)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Updated the Hyperliquid bootstrap to keep the default venue wallet sentinel when a dedicated primary shares the wallet and to persist primary flags while upserting venues so storage no longer skips primary records. cmd/recomma/main.goL277-L343 cmd/recomma/main.goL604-L633
  • Extended storage to surface the new primary flag, resolve the real primary venue when the default row is just an alias, and decode venue flag metadata safely. storage/storage.goL26-L395
  • Added a regression test confirming bots without explicit assignments fall back to the flagged primary venue even when the default row remains an alias. storage/sqlite_test.goL558-L579

Testing

  • go test ./...

View task →

@terwey-agent
Copy link
Collaborator Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines 277 to 284
defaultHyperliquidIdent := storage.DefaultHyperliquidIdentifier(orderid.OrderId{}).VenueID
defaultVenueWallet := primaryWallet
if shouldUseSentinelDefaultHyperliquidWallet(secrets.Secrets.Venues, primaryWallet, defaultHyperliquidIdent) {
defaultVenueWallet = ""
}
if err := store.EnsureDefaultVenueWallet(appCtx, defaultVenueWallet); err != nil {
fatal("update default venue wallet", err)
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Default Hyperliquid alias stores submissions under sentinel wallet

The new bootstrap path unconditionally forces the "default" Hyperliquid venue to use an empty wallet when shouldUseSentinelDefaultHyperliquidWallet returns true, which happens for the primary venue itself. storage.DefaultHyperliquidIdentifier still emits order identifiers with wallet "default", so order work is enqueued and persisted under that sentinel wallet. However, the websocket and exchange clients created later in the loop are initialised with the real wallet from the decrypted secrets and record statuses under that real wallet. Because the fill tracker and storage key orders by (venue, wallet), subsequent status lookups for default identifiers will never match the statuses written by the websocket client, leaving orders unreconciled and preventing take‑profit cancellation. Either keep the default venue wallet aligned with the websocket’s wallet for single‑wallet setups or alias the websocket/store to the sentinel identifier.

Useful? React with 👍 / 👎.

@terwey-agent
Copy link
Collaborator Author

@codex please address the issue raised in all reviews from this PR, it's about the default Hyperliquid wallet. See #69 (comment) #69 (comment) #69 (comment)

compare to the implementation and the spec

@chatgpt-codex-connector
Copy link

Summary

Testing

  • go test ./...

View task →

@terwey-agent
Copy link
Collaborator Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +300 to +307
assignment := VenueAssignment{
VenueID: recomma.VenueID(row.ID),
Wallet: wallet,
IsPrimary: true,
}, nil
}

if wallet != defaultHyperliquidWallet {
return assignment, nil

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Align default venue assignment with websocket identifier

When only a primary Hyperliquid venue is configured, defaultVenueAssignmentLocked returns an assignment for the hyperliquid:default venue whenever the stored wallet is non‑sentinel. However, cmd/recomma/main.go always boots the websocket client with the real venue identifier from secrets, so order status events are written to storage under that real ID while submissions for unassigned bots are recorded under hyperliquid:default. The foreign key on hyperliquid_status_history then rejects those status rows and the fill tracker never sees updates. Either the default assignment should return the primary venue identifier whenever a primary exists or the websocket/emitter clients must use the same identifier. Otherwise single‑venue deployments drop all status refreshes for default bots.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex we should remove the default ws bootstrap and initialize it with the rest of the venues in the loop. This is causing conflicts. Main is now doing too much logic instead of controllers we can test.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

  • Updated defaultVenueAssignmentLocked to prefer the flagged primary Hyperliquid venue, ensuring default bots adopt the runtime identifier while still falling back to the alias when no primary record exists. storage/storage.goL282-L311

Testing

  • go test ./...

View task →

@terwey-agent
Copy link
Collaborator Author

@codex review

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +430 to +431
registerHyperliquidEmitter(engineEmitter, submitter, venueIdent, primaryIdent, defaultHyperliquidIdent)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop overwriting explicit default venue emitters

When registering Hyperliquid venues the loop unconditionally calls registerHyperliquidEmitter(engineEmitter, submitter, venueIdent, primaryIdent, defaultHyperliquidIdent) for every venue. For the primary venue this helper always aliases defaultHyperliquidIdent to the same emitter. If secrets.Secrets.Venues already contains an explicit hyperliquid:default entry and it happens to be iterated before the primary entry, this alias registration overwrites the previously registered default emitter, so any work queued for hyperliquid:default (or status refreshes via the similar aliasing call above) will be sent to the primary wallet instead of the configured default wallet. This depends solely on slice ordering in the secrets payload and will misroute orders when the default venue is defined first. The alias should be skipped whenever defaultVenueConfigured is true so explicit venues are never clobbered by the primary alias.

Useful? React with 👍 / 👎.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codex fix this

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have reached your Codex usage limits. You can see your limits in the Codex usage dashboard.

terwey and others added 24 commits November 5, 2025 17:23
…rable

The Action struct had pointer fields (Create, Modify, Cancel) which made
it non-comparable in Go. Since OrderWork embeds Action and the typed queue
requires comparable structs, this violated Go's comparability requirements.

The fix removes the pointers and uses value types instead. The ActionType
field already indicates which action is active, making nil checks redundant.

This change:
- Changes Action fields from pointers to values
- Updates all action creation sites to use values instead of pointers
- Removes all nil pointer checks, replacing them with ActionType checks
- Updates all dereferences to use values directly
- Updates test assertions to remove NotNil checks

Resolves Bug #1: OrderWork Comparability Violation as described in
specs/multi_venue_emission.adoc
Fixed compilation errors from the Action struct pointer removal:
- storage/storage.go: Return &action.Create from LoadHyperliquidRequest
- filltracker/service.go: Remove & when assigning to Action fields (4 places)
- storage/sqlite_test.go: Remove nil checks and dereferences for value types
The cloneAction function no longer needs to check for nil or dereference
fields since Action now uses value types instead of pointers. A simple
struct copy is sufficient as Go will copy all value fields.
…y-011CUq9XdzPA2Jj9kyhTqGJK

fix: remove pointer fields from Action struct to make OrderWork comparable
This commit fixes three critical bugs in the scaled order system related to
multi-venue support:

Bug #2: Hard-coded Default Venue in Queries
- Remove venue_id filter from ListScaledOrdersByOrderId and ListScaledOrdersByDeal
- These queries now return records from all venues instead of just the default

Bug #3: Wrong Venue Assignment in InsertScaledOrderAudit
- Change InsertScaledOrderAudit to use venue/wallet from the OrderIdentifier parameter
- Add fallback to default venue for backward compatibility
- Each audit record now correctly reflects the actual submission venue

Bug #4: Missing Venue Context in Parameters
- Update ScaledOrderAuditParams and RecordScaledOrderParams to use OrderIdentifier
- Add Identifier field to orderscaler.Request
- Update BuildRequest to accept OrderIdentifier parameter
- Change applyScaling to accept OrderIdentifier and scale per-venue
- Refactor engine emission loop to scale each venue independently

The key architectural change is moving from "scale once, apply to all venues"
to "scale per-venue" which ensures each venue gets its own audit record with
the correct venue and wallet information.

Note: After pulling this commit, run `go generate ./...` to regenerate sqlc code.

Fixes #2-4
- Update orderscaler service tests to create OrderIdentifier for BuildRequest
- Update storage tests to use Identifier field instead of OrderId
- All tests now properly construct multi-venue identifiers
- Add VenueID and Wallet fields to ScaledOrderAudit struct
- Update convertScaledOrder to populate venue fields
- Fix filltracker reloadDeal to match scaled orders by identifier's venue
- Prevents applying wrong venue's multiplier/size to identifiers

This fixes a bug where ListScaledOrdersByOrderId returns audits from all
venues but filltracker was only using the last one for all identifiers,
causing venue mismatches when different venues have different scaled sizes.
…11CUqG7yAhQWJXAxWWLLa8g

Fix Multi-Venue Scaled Order Architecture
Add scaled_orders table migration to migrateDefaultVenueWalletLocked:
- Clone scaled_orders from old wallet to new wallet
- Delete scaled_orders associated with old wallet

This ensures the scaled order audit trail is preserved when a user
changes their Hyperliquid wallet address, matching the behavior of
hyperliquid_submissions and hyperliquid_status_history tables.

Fixes Bug #1: Missing scaled_orders in Wallet Migration
…igration-011CUogbE1CwSfpsy7mJXZme

fix: scaled orders wallet migration
This commit addresses three bugs related to multi-venue support:

Bug #2 (Medium): Take Profit Reconciliation Doesn't Fan Out
- Changed DealSnapshot.ActiveTakeProfit from *OrderSnapshot to []OrderSnapshot
- Updated snapshot building to collect ALL active take-profits across venues
- Modified ReconcileTakeProfits to iterate and reconcile each venue independently
- Added cancelTakeProfitBySnapshot and reconcileActiveTakeProfitBySnapshot helpers
- Location: filltracker/service.go:156-282

Bug #3 (Medium): Fill Tracker Unbounded Memory Growth
- Added CleanupStaleDeals method to remove completed/inactive deals
- Integrated periodic cleanup (every 10 minutes) in main event loop
- Removes deals with no activity for >1 hour and all orders complete
- Location: filltracker/service.go + cmd/recomma/main.go

Bug #4 (Low): Replay Logic Edge Case
- Documented edge case where venue additions mid-deal may miss replays
- Added inline comment suggesting periodic reconciliation for production
- Location: engine/engine.go:286-292

The take-profit fan-out fix ensures that when a deal has orders across
multiple venues, all venues' take-profits are reconciled correctly,
preventing stale orders on non-primary venues.

The memory cleanup prevents unbounded growth of the fill tracker's
in-memory state by periodically removing fully completed deals.
Updated all test assertions to use the new ActiveTakeProfits slice instead
of the single ActiveTakeProfit pointer. Changes include:
- Replace NotNil checks with Len(slice, 1) assertions
- Replace Nil checks with Empty(slice) assertions
- Access first element via [0] where needed
Updated adjustActionWithTracker to iterate through all active take-profits
when checking if a take-profit already matches the desired position. This
ensures proper handling of multi-venue scenarios where multiple take-profits
may exist.
Critical fixes for multi-venue take-profit reconciliation:

Issue #1 (P1): Each venue's take-profit was being sized to the GLOBAL net
position instead of that specific venue's position. This caused oversized
exit orders (e.g., if venue A had 60 and venue B had 40, both would try
to exit 100, totaling 200 and overshooting inventory).

Fix: Added calculateVenuePositions() to compute net position per venue
by summing filled buy/sell quantities for each venue identifier. Each
venue's take-profit is now correctly sized to match only its position.

Issue #2 (P1): ensureTakeProfit was only called when NO active take-profits
existed. If one venue had an active TP, other venues without TPs would
never get them created, leaving the multi-venue fan-out incomplete.

Fix: Changed logic to iterate through ALL venue positions and reconcile
or create take-profits for each venue independently. Now checks per-venue:
- If venue has position but no TP: create one
- If venue has position and TP: reconcile size
- If venue has no position but TP exists: cancel it

Location: filltracker/service.go:155-215
Critical fix for position calculation:

Problem: calculateVenuePositions was using full OrderIdentifier (including
OrderId with specific BotEventID) as map key. This created separate entries
for each order even if they were from the same venue. For example:
- baseOrder (BotEventID=1) at venue A: +5
- closeOrder (BotEventID=3) at venue A: -5
Result: Two separate map entries instead of one aggregated position of 0

Also, activeTPs map couldn't match against venuePositions because the
OrderIds differed (TP has its own BotEventID).

Fix:
1. Added venueKey struct to key positions by venue+wallet only
2. Modified calculateVenuePositions to aggregate all orders per venue
3. Changed activeTPs lookup to use venueKey instead of full OrderIdentifier
4. Added cleanup loop to cancel TPs for venues with no position

This ensures:
- Orders from same venue aggregate correctly (5 buy + 5 sell = 0 net)
- TPs can be matched to their venue's position for reconciliation
- TPs without positions get canceled
- Each venue's TP is sized to that venue's specific holdings

Location: filltracker/service.go:155-247
Test failures showed that when creating TPs for venues, the code was using
base order OrderIds instead of the take-profit's OrderId.

Problem: In venuePositions map, identifiers come from non-TP orders (base
orders, safety orders, etc). When creating a TP for a venue without one,
passing this identifier meant using the wrong OrderId (e.g., base order's
BotEventID=1 instead of TP's BotEventID=2).

Fix: Added buildTakeProfitIdentifier() helper that:
1. Searches snapshot for any TP order to get the correct TP OrderId
2. Falls back to loading TP metadata from storage
3. Constructs identifier with target venue+wallet but TP's OrderId
4. Returns nil if no TP metadata exists (lets ensureTakeProfit handle lookup)

This ensures:
- Created TPs use the correct take-profit OrderId
- Each venue gets a TP with matching OrderId but venue-specific venue+wallet
- Tests expecting specific TP OrderIds now pass

Fixes failing tests:
- TestUpdateStatusIgnoresOlderTimestamps/recreates_missing_take_profit
- TestEnsureTakeProfitRecreatesAfterStaleSubmission

Location: filltracker/service.go:211-238
Critical bug: calculateVenuePositions was skipping ALL reduce-only orders,
including filled take-profits. This caused venues with fully-exited positions
to still report positive net quantities, leading to infinite TP recreation.

Example of the bug:
  Buy order:    +100 units (filled)
  TP order:     -100 units (filled, reduce-only)

  WRONG (before): Skip TP because reduce-only → venueNetQty = +100
  ReconcileTakeProfits sees +100, creates new TP
  Next cycle: still +100, creates another TP (infinite loop!)

  CORRECT (now): Include TP filled qty → venueNetQty = 0
  ReconcileTakeProfits sees 0, cancels any active TPs

The confusion was thinking we should exclude TPs from position calculation,
but we should only exclude UNFILLED quantities. Using FilledQty automatically
handles this:
- Filled TPs: FilledQty > 0, correctly reduces position
- Active TPs: FilledQty = 0 or partial, doesn't affect net calculation

This matches the original behavior where snapshot.Position.NetQty included
all fills (including reduce-only).

Location: filltracker/service.go:246-278
Critical P1 bug: adjustActionWithTracker was comparing individual venue
take-profits against the global Position.NetQty, causing mismatches in
multi-venue scenarios.

Problem:
  Venue A: 60 units position, TP remaining = 60
  Venue B: 40 units position, TP remaining = 40
  Global:  100 units net

  adjustActionWithTracker checks:
    Does 60 == 100? No
    Does 40 == 100? No

  Falls through to sizing logic → tries to set TP.Size = 100
  Oversizes venue TPs until ReconcileTakeProfits corrects them

Root cause: adjustActionWithTracker is called BEFORE fanning out to venues,
so it doesn't have per-venue position data. It only sees global net qty.

Solution: Detect multi-venue scenario (multiple active TPs) and skip the
fast-path sizing logic. Let ReconcileTakeProfits handle all multi-venue
sizing correctly with its per-venue position calculation.

Behavior:
- Single venue (0-1 active TPs): Fast path applies global sizing ✓
- Multi-venue (2+ active TPs): Fast path defers to reconciliation ✓
- ReconcileTakeProfits: Always uses correct per-venue sizing ✓

This prevents the engine from oversizing venue TPs while still allowing
the fast path to work for single-venue deals.

Location: engine/engine.go:524-604
…tion-011CUqSqMgqYrhQuWUx8Usei

fix: resolve multi-venue reconciliation and memory issues
@terwey terwey merged commit a486a57 into codex/investigate-multi-wallet-support-for-hyperliquid Nov 5, 2025
2 checks passed
@terwey terwey deleted the codex/extend-cmd/recomma-to-handle-multiple-venues branch November 5, 2025 23:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants