feat: add buy CL at 10s, multi-duration support, and multi-asset concurrent maker
- CL at 10s before close: cancel unfilled buy orders, place sells for filled positions with 3x retry for on-chain settlement delay - Sell orders stay live until market close (no early cancellation) - Support multiple durations simultaneously (MAKER_DURATION=5m,15m) - Queue key changed to asset+duration so BTC/5m and BTC/15m run concurrently - Update AGENT.MD and README.md with Maker v2 documentation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
930dffafbd
commit
5f4aa04d13
+3
-3
@@ -167,10 +167,10 @@ SNIPER_SCHEDULE_XRP=18:40-20:40,08:40-09:50
|
|||||||
# Only one side will fill — the other gets cancelled.
|
# Only one side will fill — the other gets cancelled.
|
||||||
# ─────────────────────────────────────────────
|
# ─────────────────────────────────────────────
|
||||||
# Comma-separated assets
|
# Comma-separated assets
|
||||||
MAKER_ASSETS=btc
|
MAKER_ASSETS=btc,eth,sol,xrp
|
||||||
|
|
||||||
# Market duration: "5m" or "15m"
|
# Market duration: comma-separated, e.g. "5m" or "5m,15m" for both
|
||||||
MAKER_DURATION=5m
|
MAKER_DURATION=5m,15m
|
||||||
|
|
||||||
# Limit BUY price (e.g. 0.02 = 2 cents per share)
|
# Limit BUY price (e.g. 0.02 = 2 cents per share)
|
||||||
MAKER_BUY_PRICE=0.02
|
MAKER_BUY_PRICE=0.02
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ Developer and AI agent reference for understanding, extending, and maintaining t
|
|||||||
| Bot | Entry Point | Command | Purpose |
|
| Bot | Entry Point | Command | Purpose |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Copy Trade | `src/index.js` | `npm start` | Mirror trades from a target trader wallet |
|
| Copy Trade | `src/index.js` | `npm start` | Mirror trades from a target trader wallet |
|
||||||
| Market Maker | `src/mm.js` | `npm run mm` | Provide liquidity on 5m/15m binary markets |
|
| Market Maker v1 | `src/mm.js` | `npm run mm` | Provide liquidity via splitPosition on 5m/15m markets |
|
||||||
|
| Market Maker v2 | `src/maker.js` | `npm run maker` | Buy low, sell high — pure orderbook maker (no splitPosition) |
|
||||||
| Orderbook Sniper | `src/sniper.js` | `npm run sniper` | Place low-price GTC orders to catch panic dumps |
|
| Orderbook Sniper | `src/sniper.js` | `npm run sniper` | Place low-price GTC orders to catch panic dumps |
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -91,9 +92,10 @@ All files use **ES Modules** (`import`/`export`). Do not use `require()`.
|
|||||||
|
|
||||||
- Loads all settings from `.env` via `dotenv`
|
- Loads all settings from `.env` via `dotenv`
|
||||||
- Parses and validates required fields per bot type
|
- Parses and validates required fields per bot type
|
||||||
- Exports two validator functions:
|
- Exports three validator functions:
|
||||||
- `validateCopyTradeConfig()` — for `src/index.js`
|
- `validateCopyTradeConfig()` — for `src/index.js`
|
||||||
- `validateMMConfig()` — for `src/mm.js` and `src/sniper.js`
|
- `validateMMConfig()` — for `src/mm.js` and `src/sniper.js`
|
||||||
|
- `validateMakerConfig()` — for `src/maker.js`
|
||||||
- Key exported fields: `privateKey`, `proxyWallet`, `traderAddress`, `dryRun`, `mmTradeSize`, etc.
|
- Key exported fields: `privateKey`, `proxyWallet`, `traderAddress`, `dryRun`, `mmTradeSize`, etc.
|
||||||
|
|
||||||
### Client (`src/services/client.js`)
|
### Client (`src/services/client.js`)
|
||||||
@@ -171,6 +173,23 @@ Used exclusively by the Market Maker bot. Interacts with the Gnosis Safe proxy w
|
|||||||
- `cutLoss(...)` — Cancels remaining orders; merges back or market-sells residual tokens
|
- `cutLoss(...)` — Cancels remaining orders; merges back or market-sells residual tokens
|
||||||
- `attemptRecoveryBuy(...)` — Optional directional bet post cut-loss (controlled by `MM_RECOVERY_BUY`)
|
- `attemptRecoveryBuy(...)` — Optional directional bet post cut-loss (controlled by `MM_RECOVERY_BUY`)
|
||||||
|
|
||||||
|
### Maker Detector (`src/services/makerDetector.js`)
|
||||||
|
|
||||||
|
- Slot-based detection for the Maker v2 bot (buy low, sell high)
|
||||||
|
- Supports **multiple durations** simultaneously (e.g. `MAKER_DURATION=5m,15m`)
|
||||||
|
- Polls all `asset × duration` combinations in parallel
|
||||||
|
- Each detected market includes a `duration` field for concurrent queue management
|
||||||
|
- Same slug format as other bots: `{asset}-updown-{duration}-{timestamp}`
|
||||||
|
|
||||||
|
### Maker Executor (`src/services/makerExecutor.js`)
|
||||||
|
|
||||||
|
- `executeMakerStrategy(market)` — Full flow: place limit BUY on UP+DOWN → monitor fills → place limit SELL → monitor sells
|
||||||
|
- **CL at 10s before close:** Cancels unfilled buy orders, places sells for any filled positions (retry 3x for settlement delays)
|
||||||
|
- **Sell phase:** Sell orders stay live until market close (no early cancellation)
|
||||||
|
- **Multi-market concurrent:** Each position keyed by `conditionId`, queue keyed by `${asset}-${duration}`
|
||||||
|
- Simulation mode with full P&L tracking (win/loss/skip stats)
|
||||||
|
- `placeLimitSellWithRetry()` — Retries sell placement 3x with increasing delay for on-chain settlement
|
||||||
|
|
||||||
### Sniper Detector (`src/services/sniperDetector.js`)
|
### Sniper Detector (`src/services/sniperDetector.js`)
|
||||||
|
|
||||||
- Scans for 5-minute markets for a configurable list of assets
|
- Scans for 5-minute markets for a configurable list of assets
|
||||||
|
|||||||
@@ -34,13 +34,23 @@
|
|||||||
- **Per-Market Queue** — Concurrent events for the same market are serialized to prevent duplicate buys
|
- **Per-Market Queue** — Concurrent events for the same market are serialized to prevent duplicate buys
|
||||||
- **Dry Run Mode** — Simulate the full flow without placing real orders
|
- **Dry Run Mode** — Simulate the full flow without placing real orders
|
||||||
|
|
||||||
### Market Maker Bot
|
### Market Maker v1 Bot (Split Position)
|
||||||
- **Automated Liquidity** — Splits USDC into YES+NO tokens and places limit sells on both sides at $0.50 entry
|
- **Automated Liquidity** — Splits USDC into YES+NO tokens and places limit sells on both sides at $0.50 entry
|
||||||
- **Cut-Loss Protection** — Merges unsold tokens back to USDC before market close
|
- **Cut-Loss Protection** — Merges unsold tokens back to USDC before market close
|
||||||
- **Recovery Buy** — Optional directional bet after a cut-loss triggers
|
- **Recovery Buy** — Optional directional bet after a cut-loss triggers
|
||||||
- **Multi-Asset** — Supports BTC, ETH, SOL, and any 5m/15m Polymarket market
|
- **Multi-Asset** — Supports BTC, ETH, SOL, and any 5m/15m Polymarket market
|
||||||
- **Simulation Mode** — Full dry-run with P&L tracking
|
- **Simulation Mode** — Full dry-run with P&L tracking
|
||||||
|
|
||||||
|
### Market Maker v2 Bot (Buy Low, Sell High)
|
||||||
|
- **Pure Orderbook** — Places limit BUY on both UP+DOWN at low price (e.g. 2c), sells at higher price (e.g. 3c) when filled. No splitPosition needed
|
||||||
|
- **Cut-Loss at 10s** — Cancels unfilled buy orders 10 seconds before market close; sells positions as much as possible
|
||||||
|
- **Sell Retry 3x** — Retries sell placement with delay for on-chain token settlement
|
||||||
|
- **Multi-Asset** — Supports BTC, ETH, SOL, XRP simultaneously
|
||||||
|
- **Multi-Duration** — Run 5m and 15m markets concurrently (`MAKER_DURATION=5m,15m`)
|
||||||
|
- **Concurrent Markets** — Each asset+duration combination runs independently (e.g. BTC/5m and BTC/15m don't block each other)
|
||||||
|
- **Simulation Mode** — Full dry-run with win/loss/skip tracking and P&L stats
|
||||||
|
- **Proxy Support** — All API calls go through `PROXY_URL` if configured
|
||||||
|
|
||||||
### Orderbook Sniper Bot
|
### Orderbook Sniper Bot
|
||||||
- **3-Tier Strategy** — Places GTC BUY orders at 3c, 2c, and 1c with weighted sizing (20%/30%/50%)
|
- **3-Tier Strategy** — Places GTC BUY orders at 3c, 2c, and 1c with weighted sizing (20%/30%/50%)
|
||||||
- **Multi-Asset** — Targets ETH, SOL, XRP, and more simultaneously
|
- **Multi-Asset** — Targets ETH, SOL, XRP, and more simultaneously
|
||||||
@@ -138,6 +148,20 @@ Leave these blank to have the client auto-derive credentials from your private k
|
|||||||
| `MM_RECOVERY_THRESHOLD` | Minimum dominant-side price to qualify for recovery | `0.70` |
|
| `MM_RECOVERY_THRESHOLD` | Minimum dominant-side price to qualify for recovery | `0.70` |
|
||||||
| `MM_RECOVERY_SIZE` | USDC for recovery buy (0 = use `MM_TRADE_SIZE`) | `0` |
|
| `MM_RECOVERY_SIZE` | USDC for recovery buy (0 = use `MM_TRADE_SIZE`) | `0` |
|
||||||
|
|
||||||
|
### Market Maker v2 (Maker) Settings
|
||||||
|
|
||||||
|
| Variable | Description | Default |
|
||||||
|
|---|---|---|
|
||||||
|
| `MAKER_ASSETS` | Comma-separated assets (e.g. `btc,eth,sol,xrp`) | `btc` |
|
||||||
|
| `MAKER_DURATION` | Comma-separated durations (e.g. `5m` or `5m,15m`) | `5m` |
|
||||||
|
| `MAKER_BUY_PRICE` | Limit BUY price per share (e.g. `0.02` = 2c) | `0.02` |
|
||||||
|
| `MAKER_SELL_PRICE` | Limit SELL price per share (e.g. `0.03` = 3c) | `0.03` |
|
||||||
|
| `MAKER_TRADE_SIZE` | Shares per side (e.g. `50` × $0.02 = $1.00/side) | `50` |
|
||||||
|
| `MAKER_POLL_INTERVAL` | Seconds between new market polls | `10` |
|
||||||
|
| `MAKER_MONITOR_MS` | Milliseconds between order fill checks | `2000` |
|
||||||
|
|
||||||
|
**Cut-Loss:** At 10 seconds before market close, unfilled buy orders are cancelled. Any filled positions get sell orders placed (retry 3x). Sell orders stay live until market close.
|
||||||
|
|
||||||
### Orderbook Sniper Settings
|
### Orderbook Sniper Settings
|
||||||
|
|
||||||
**3-Tier Strategy:** Places orders at 3 price levels with weighted sizing
|
**3-Tier Strategy:** Places orders at 3 price levels with weighted sizing
|
||||||
@@ -179,6 +203,11 @@ npm run mm # live trading
|
|||||||
npm run mm-sim # simulation (DRY_RUN=true)
|
npm run mm-sim # simulation (DRY_RUN=true)
|
||||||
npm run mm-dev # simulation + auto-reload
|
npm run mm-dev # simulation + auto-reload
|
||||||
|
|
||||||
|
# Market Maker v2 (Maker) Bot
|
||||||
|
npm run maker # live trading
|
||||||
|
npm run maker-sim # simulation
|
||||||
|
npm run maker-dev # simulation + auto-reload
|
||||||
|
|
||||||
# Orderbook Sniper Bot
|
# Orderbook Sniper Bot
|
||||||
npm run sniper # live trading
|
npm run sniper # live trading
|
||||||
npm run sniper-sim # simulation
|
npm run sniper-sim # simulation
|
||||||
@@ -195,10 +224,15 @@ npm run bot # live trading
|
|||||||
npm run bot-sim # simulation
|
npm run bot-sim # simulation
|
||||||
npm run bot-dev # simulation + auto-reload
|
npm run bot-dev # simulation + auto-reload
|
||||||
|
|
||||||
# Market Maker Bot
|
# Market Maker v1 Bot
|
||||||
npm run mm-bot # live trading
|
npm run mm-bot # live trading
|
||||||
npm run mm-bot-sim # simulation
|
npm run mm-bot-sim # simulation
|
||||||
npm run mm-bot-dev # simulation + auto-reload
|
npm run mm-bot-dev # simulation + auto-reload
|
||||||
|
|
||||||
|
# Market Maker v2 (Maker) Bot
|
||||||
|
npm run maker-bot # live trading
|
||||||
|
npm run maker-bot-sim # simulation
|
||||||
|
npm run maker-bot-dev # simulation + auto-reload
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Always test with `DRY_RUN=true` (or `*-sim` scripts) first** before committing real funds.
|
> **Always test with `DRY_RUN=true` (or `*-sim` scripts) first** before committing real funds.
|
||||||
@@ -288,7 +322,7 @@ Redeemer loop (every REDEEM_INTERVAL seconds)
|
|||||||
→ Check on-chain payout → redeemPositions via Gnosis Safe
|
→ Check on-chain payout → redeemPositions via Gnosis Safe
|
||||||
```
|
```
|
||||||
|
|
||||||
### Market Maker Flow
|
### Market Maker v1 Flow (Split Position)
|
||||||
|
|
||||||
```
|
```
|
||||||
New Market Detected
|
New Market Detected
|
||||||
@@ -311,6 +345,37 @@ Collect Cancel orders → Merge YES+NO back to USDC
|
|||||||
profit (recovery buy optional)
|
profit (recovery buy optional)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Market Maker v2 Flow (Buy Low, Sell High)
|
||||||
|
|
||||||
|
```
|
||||||
|
Detector polls all asset × duration combos (e.g. BTC/5m, ETH/15m)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Place limit BUY on UP + DOWN @ MAKER_BUY_PRICE (e.g. $0.02)
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
Monitor both sides concurrently
|
||||||
|
│
|
||||||
|
┌────┴─────────────────┐
|
||||||
|
│ │
|
||||||
|
One side fills CL at 10s before close
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
Cancel other buy Cancel unfilled buys
|
||||||
|
Place SELL @ 3c Place SELL for any fills (retry 3x)
|
||||||
|
│ │
|
||||||
|
└──────┬───────────────┘
|
||||||
|
▼
|
||||||
|
Monitor sells until market close
|
||||||
|
│
|
||||||
|
┌────┴────┐
|
||||||
|
│ │
|
||||||
|
Sell fills Market closes
|
||||||
|
│ │
|
||||||
|
▼ ▼
|
||||||
|
WIN $ Tokens resolve on-chain
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
@@ -320,8 +385,10 @@ polymarket-terminal/
|
|||||||
├── src/
|
├── src/
|
||||||
│ ├── index.js — Copy trade bot (TUI)
|
│ ├── index.js — Copy trade bot (TUI)
|
||||||
│ ├── bot.js — Copy trade bot (plain log / PM2)
|
│ ├── bot.js — Copy trade bot (plain log / PM2)
|
||||||
│ ├── mm.js — Market maker bot (TUI)
|
│ ├── mm.js — Market maker v1 bot (TUI)
|
||||||
│ ├── mm-bot.js — Market maker bot (plain log / PM2)
|
│ ├── mm-bot.js — Market maker v1 bot (plain log / PM2)
|
||||||
|
│ ├── maker.js — Market maker v2 bot (TUI)
|
||||||
|
│ ├── maker-bot.js — Market maker v2 bot (plain log / PM2)
|
||||||
│ ├── sniper.js — Orderbook sniper bot
|
│ ├── sniper.js — Orderbook sniper bot
|
||||||
│ │
|
│ │
|
||||||
│ ├── config/
|
│ ├── config/
|
||||||
@@ -336,8 +403,11 @@ polymarket-terminal/
|
|||||||
│ │ ├── autoSell.js — Auto limit-sell placement
|
│ │ ├── autoSell.js — Auto limit-sell placement
|
||||||
│ │ ├── redeemer.js — Market resolution check & CTF redemption
|
│ │ ├── redeemer.js — Market resolution check & CTF redemption
|
||||||
│ │ ├── ctf.js — On-chain CTF contract interactions
|
│ │ ├── ctf.js — On-chain CTF contract interactions
|
||||||
│ │ ├── mmDetector.js — Market detection for market maker
|
│ │ ├── mmDetector.js — Market detection for MM v1
|
||||||
│ │ ├── mmExecutor.js — Market maker strategy execution
|
│ │ ├── mmExecutor.js — MM v1 strategy execution
|
||||||
|
│ │ ├── makerDetector.js — Market detection for Maker v2 (multi-duration)
|
||||||
|
│ │ ├── makerExecutor.js — Maker v2 strategy execution
|
||||||
|
│ │ ├── makerWs.js — Orderbook WebSocket for Maker v2
|
||||||
│ │ ├── sniperDetector.js — Market detection for sniper
|
│ │ ├── sniperDetector.js — Market detection for sniper
|
||||||
│ │ └── sniperExecutor.js — Orderbook sniper order placement
|
│ │ └── sniperExecutor.js — Orderbook sniper order placement
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
+2
-1
@@ -108,7 +108,8 @@ const config = {
|
|||||||
// No splitPosition — pure orderbook strategy.
|
// No splitPosition — pure orderbook strategy.
|
||||||
makerAssets: (process.env.MAKER_ASSETS || 'btc')
|
makerAssets: (process.env.MAKER_ASSETS || 'btc')
|
||||||
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
||||||
makerDuration: process.env.MAKER_DURATION || '5m',
|
makerDurations: (process.env.MAKER_DURATION || '5m')
|
||||||
|
.split(',').map((s) => s.trim().toLowerCase()).filter(Boolean),
|
||||||
makerBuyPrice: parseFloat(process.env.MAKER_BUY_PRICE || '0.02'),
|
makerBuyPrice: parseFloat(process.env.MAKER_BUY_PRICE || '0.02'),
|
||||||
makerSellPrice: parseFloat(process.env.MAKER_SELL_PRICE || '0.03'),
|
makerSellPrice: parseFloat(process.env.MAKER_SELL_PRICE || '0.03'),
|
||||||
makerTradeSize: parseFloat(process.env.MAKER_TRADE_SIZE || '50'), // shares per side
|
makerTradeSize: parseFloat(process.env.MAKER_TRADE_SIZE || '50'), // shares per side
|
||||||
|
|||||||
+21
-12
@@ -98,7 +98,14 @@ async function printStatus() {
|
|||||||
|
|
||||||
const pendingByAsset = new Map();
|
const pendingByAsset = new Map();
|
||||||
|
|
||||||
|
function slotKey(market) {
|
||||||
|
return `${market.asset}-${market.duration || '5m'}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function runStrategy(market) {
|
async function runStrategy(market) {
|
||||||
|
const key = slotKey(market);
|
||||||
|
const tag = `${market.asset?.toUpperCase()}/${market.duration || '5m'}`;
|
||||||
|
|
||||||
if (config.dryRun) {
|
if (config.dryRun) {
|
||||||
orderbookWs.subscribe(market.conditionId, [market.yesTokenId, market.noTokenId]);
|
orderbookWs.subscribe(market.conditionId, [market.yesTokenId, market.noTokenId]);
|
||||||
}
|
}
|
||||||
@@ -106,30 +113,32 @@ async function runStrategy(market) {
|
|||||||
try {
|
try {
|
||||||
await executeMakerStrategy(market);
|
await executeMakerStrategy(market);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`MAKER strategy error (${market.asset?.toUpperCase()}): ${err.message}`);
|
logger.error(`MAKER strategy error (${tag}): ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const queued = pendingByAsset.get(market.asset);
|
const queued = pendingByAsset.get(key);
|
||||||
if (queued) {
|
if (queued) {
|
||||||
pendingByAsset.delete(market.asset);
|
pendingByAsset.delete(key);
|
||||||
const secsLeft = Math.round((new Date(queued.endTime).getTime() - Date.now()) / 1000);
|
const secsLeft = Math.round((new Date(queued.endTime).getTime() - Date.now()) / 1000);
|
||||||
|
|
||||||
if (secsLeft > 30) {
|
if (secsLeft > 30) {
|
||||||
logger.success(`MAKER[${market.asset?.toUpperCase()}]: executing queued market (${secsLeft}s left)`);
|
logger.success(`MAKER[${tag}]: executing queued market (${secsLeft}s left)`);
|
||||||
runStrategy(queued);
|
runStrategy(queued);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`MAKER[${market.asset?.toUpperCase()}]: queued market expired (${secsLeft}s left)`);
|
logger.warn(`MAKER[${tag}]: queued market expired (${secsLeft}s left)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleNewMarket(market) {
|
async function handleNewMarket(market) {
|
||||||
|
const key = slotKey(market);
|
||||||
|
const tag = `${market.asset?.toUpperCase()}/${market.duration || '5m'}`;
|
||||||
const active = getActiveMakerPositions();
|
const active = getActiveMakerPositions();
|
||||||
const isAssetBusy = active.some((p) => p.asset === market.asset);
|
const isSlotBusy = active.some((p) => p.asset === market.asset && p.duration === (market.duration || '5m'));
|
||||||
|
|
||||||
if (isAssetBusy) {
|
if (isSlotBusy) {
|
||||||
pendingByAsset.set(market.asset, market);
|
pendingByAsset.set(key, market);
|
||||||
logger.warn(`MAKER[${market.asset?.toUpperCase()}]: queued — will enter after current position clears`);
|
logger.warn(`MAKER[${tag}]: queued — will enter after current position clears`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -161,14 +170,14 @@ const profitPerCycle = (config.makerSellPrice - config.makerBuyPrice) * config.m
|
|||||||
|
|
||||||
logger.info(`=== Market Maker v2 [${mode}] ===`);
|
logger.info(`=== Market Maker v2 [${mode}] ===`);
|
||||||
logger.info(`Assets : ${config.makerAssets.join(', ').toUpperCase()}`);
|
logger.info(`Assets : ${config.makerAssets.join(', ').toUpperCase()}`);
|
||||||
logger.info(`Duration : ${config.makerDuration}`);
|
logger.info(`Duration : ${config.makerDurations.join(', ')}`);
|
||||||
logger.info(`Buy @ : $${config.makerBuyPrice} per share`);
|
logger.info(`Buy @ : $${config.makerBuyPrice} per share`);
|
||||||
logger.info(`Sell @ : $${config.makerSellPrice} per share`);
|
logger.info(`Sell @ : $${config.makerSellPrice} per share`);
|
||||||
logger.info(`Size : ${config.makerTradeSize} shares/side`);
|
logger.info(`Size : ${config.makerTradeSize} shares/side`);
|
||||||
logger.info(`Cost/side : $${costPerSide.toFixed(2)}`);
|
logger.info(`Cost/side : $${costPerSide.toFixed(2)}`);
|
||||||
logger.info(`Profit : $${profitPerCycle.toFixed(2)} per cycle`);
|
logger.info(`Profit : $${profitPerCycle.toFixed(2)} per cycle`);
|
||||||
logger.info(`Cut loss : NONE (hold to resolution)`);
|
logger.info(`CL : cancel buys at 10s, sell until close`);
|
||||||
logger.info('==========================================');
|
logger.info('==========================================');
|
||||||
|
|
||||||
startMakerDetector(handleNewMarket);
|
startMakerDetector(handleNewMarket);
|
||||||
logger.success(`MAKER bot started — watching for ${config.makerDuration} ${config.makerAssets.join('/')} markets...`);
|
logger.success(`MAKER bot started — watching for ${config.makerDurations.join('+')} ${config.makerAssets.join('/')} markets...`);
|
||||||
|
|||||||
+22
-13
@@ -93,13 +93,13 @@ async function buildStatusContent() {
|
|||||||
// Maker Config
|
// Maker Config
|
||||||
lines.push('{bold}MAKER CONFIG{/bold}');
|
lines.push('{bold}MAKER CONFIG{/bold}');
|
||||||
lines.push(` Assets : ${config.makerAssets.join(', ').toUpperCase()}`);
|
lines.push(` Assets : ${config.makerAssets.join(', ').toUpperCase()}`);
|
||||||
lines.push(` Duration : ${config.makerDuration}`);
|
lines.push(` Duration : ${config.makerDurations.join(', ')}`);
|
||||||
lines.push(` Buy @ : $${config.makerBuyPrice} per share`);
|
lines.push(` Buy @ : $${config.makerBuyPrice} per share`);
|
||||||
lines.push(` Sell @ : $${config.makerSellPrice} per share`);
|
lines.push(` Sell @ : $${config.makerSellPrice} per share`);
|
||||||
lines.push(` Size : ${config.makerTradeSize} shares/side`);
|
lines.push(` Size : ${config.makerTradeSize} shares/side`);
|
||||||
lines.push(` Cost/side: $${(config.makerTradeSize * config.makerBuyPrice).toFixed(2)}`);
|
lines.push(` Cost/side: $${(config.makerTradeSize * config.makerBuyPrice).toFixed(2)}`);
|
||||||
lines.push(` Profit : $${((config.makerSellPrice - config.makerBuyPrice) * config.makerTradeSize).toFixed(2)}/cycle`);
|
lines.push(` Profit : $${((config.makerSellPrice - config.makerBuyPrice) * config.makerTradeSize).toFixed(2)}/cycle`);
|
||||||
lines.push(` No CL : hold to resolution if sell unfilled`);
|
lines.push(` CL : cancel buys at ${config.makerCLSeconds || 10}s, sell until close`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
// Active positions
|
// Active positions
|
||||||
@@ -110,7 +110,7 @@ async function buildStatusContent() {
|
|||||||
lines.push(' {gray-fg}Waiting for market...{/gray-fg}');
|
lines.push(' {gray-fg}Waiting for market...{/gray-fg}');
|
||||||
} else {
|
} else {
|
||||||
for (const pos of positions) {
|
for (const pos of positions) {
|
||||||
const assetTag = pos.asset ? `[${pos.asset.toUpperCase()}] ` : '';
|
const assetTag = pos.asset ? `[${pos.asset.toUpperCase()}/${pos.duration || '5m'}] ` : '';
|
||||||
const label = pos.question.substring(0, 32);
|
const label = pos.question.substring(0, 32);
|
||||||
const msLeft = new Date(pos.endTime).getTime() - Date.now();
|
const msLeft = new Date(pos.endTime).getTime() - Date.now();
|
||||||
const secsLeft = Math.max(0, Math.round(msLeft / 1000));
|
const secsLeft = Math.max(0, Math.round(msLeft / 1000));
|
||||||
@@ -200,7 +200,14 @@ function startRefresh() {
|
|||||||
|
|
||||||
const pendingByAsset = new Map();
|
const pendingByAsset = new Map();
|
||||||
|
|
||||||
|
function slotKey(market) {
|
||||||
|
return `${market.asset}-${market.duration || '5m'}`;
|
||||||
|
}
|
||||||
|
|
||||||
async function runStrategy(market) {
|
async function runStrategy(market) {
|
||||||
|
const key = slotKey(market);
|
||||||
|
const tag = `${market.asset?.toUpperCase()}/${market.duration || '5m'}`;
|
||||||
|
|
||||||
// Connect WebSocket for orderbook visualization in sim mode
|
// Connect WebSocket for orderbook visualization in sim mode
|
||||||
if (config.dryRun) {
|
if (config.dryRun) {
|
||||||
activeWsTokens = { up: market.yesTokenId, down: market.noTokenId };
|
activeWsTokens = { up: market.yesTokenId, down: market.noTokenId };
|
||||||
@@ -210,7 +217,7 @@ async function runStrategy(market) {
|
|||||||
try {
|
try {
|
||||||
await executeMakerStrategy(market);
|
await executeMakerStrategy(market);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`MAKER strategy error (${market.asset?.toUpperCase()}): ${err.message}`);
|
logger.error(`MAKER strategy error (${tag}): ${err.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disconnect WS after strategy ends
|
// Disconnect WS after strategy ends
|
||||||
@@ -219,28 +226,30 @@ async function runStrategy(market) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Process queued market
|
// Process queued market
|
||||||
const queued = pendingByAsset.get(market.asset);
|
const queued = pendingByAsset.get(key);
|
||||||
if (queued) {
|
if (queued) {
|
||||||
pendingByAsset.delete(market.asset);
|
pendingByAsset.delete(key);
|
||||||
const endMs = new Date(queued.endTime).getTime();
|
const endMs = new Date(queued.endTime).getTime();
|
||||||
const secsLeft = Math.round((endMs - Date.now()) / 1000);
|
const secsLeft = Math.round((endMs - Date.now()) / 1000);
|
||||||
|
|
||||||
if (secsLeft > 30) {
|
if (secsLeft > 30) {
|
||||||
logger.success(`MAKER[${market.asset?.toUpperCase()}]: executing queued market (${secsLeft}s left)`);
|
logger.success(`MAKER[${tag}]: executing queued market (${secsLeft}s left)`);
|
||||||
runStrategy(queued);
|
runStrategy(queued);
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`MAKER[${market.asset?.toUpperCase()}]: queued market expired (${secsLeft}s left)`);
|
logger.warn(`MAKER[${tag}]: queued market expired (${secsLeft}s left)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleNewMarket(market) {
|
async function handleNewMarket(market) {
|
||||||
|
const key = slotKey(market);
|
||||||
|
const tag = `${market.asset?.toUpperCase()}/${market.duration || '5m'}`;
|
||||||
const active = getActiveMakerPositions();
|
const active = getActiveMakerPositions();
|
||||||
const isAssetBusy = active.some((p) => p.asset === market.asset);
|
const isSlotBusy = active.some((p) => p.asset === market.asset && p.duration === (market.duration || '5m'));
|
||||||
|
|
||||||
if (isAssetBusy) {
|
if (isSlotBusy) {
|
||||||
pendingByAsset.set(market.asset, market);
|
pendingByAsset.set(key, market);
|
||||||
logger.warn(`MAKER[${market.asset?.toUpperCase()}]: queued — will enter after current position clears`);
|
logger.warn(`MAKER[${tag}]: queued — will enter after current position clears`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,7 +274,7 @@ process.on('SIGTERM', shutdown);
|
|||||||
const costPerSide = config.makerTradeSize * config.makerBuyPrice;
|
const costPerSide = config.makerTradeSize * config.makerBuyPrice;
|
||||||
const profitPerCycle = (config.makerSellPrice - config.makerBuyPrice) * config.makerTradeSize;
|
const profitPerCycle = (config.makerSellPrice - config.makerBuyPrice) * config.makerTradeSize;
|
||||||
logger.info(`MAKER starting — ${config.dryRun ? 'SIMULATION' : 'LIVE'}`);
|
logger.info(`MAKER starting — ${config.dryRun ? 'SIMULATION' : 'LIVE'}`);
|
||||||
logger.info(`Assets: ${config.makerAssets.join(', ').toUpperCase()} | BUY @ $${config.makerBuyPrice} → SELL @ $${config.makerSellPrice}`);
|
logger.info(`Assets: ${config.makerAssets.join(', ').toUpperCase()} | Durations: ${config.makerDurations.join(', ')} | BUY @ $${config.makerBuyPrice} → SELL @ $${config.makerSellPrice}`);
|
||||||
logger.info(`Size: ${config.makerTradeSize} sh/side | Cost: $${costPerSide.toFixed(2)}/side | Profit: $${profitPerCycle.toFixed(2)}/cycle`);
|
logger.info(`Size: ${config.makerTradeSize} sh/side | Cost: $${costPerSide.toFixed(2)}/side | Profit: $${profitPerCycle.toFixed(2)}/cycle`);
|
||||||
|
|
||||||
startRefresh();
|
startRefresh();
|
||||||
|
|||||||
@@ -1,29 +1,34 @@
|
|||||||
/**
|
/**
|
||||||
* makerDetector.js
|
* makerDetector.js
|
||||||
* Detects upcoming markets for the Maker strategy (buy low, sell high).
|
* Detects upcoming markets for the Maker strategy (buy low, sell high).
|
||||||
* Same slug-based detection as mmDetector but reads from MAKER_* config.
|
* Supports multiple assets AND multiple durations (e.g. 5m,15m).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import config from '../config/index.js';
|
import config from '../config/index.js';
|
||||||
import logger from '../utils/logger.js';
|
import logger from '../utils/logger.js';
|
||||||
import { proxyFetch } from '../utils/proxy.js';
|
import { proxyFetch } from '../utils/proxy.js';
|
||||||
|
|
||||||
const SLOT_SEC = config.makerDuration === '15m' ? 900 : 300;
|
const DURATION_SECS = { '5m': 300, '15m': 900 };
|
||||||
|
|
||||||
let pollTimer = null;
|
let pollTimer = null;
|
||||||
let onMarketCb = null;
|
let onMarketCb = null;
|
||||||
const seenKeys = new Set();
|
const seenKeys = new Set();
|
||||||
|
|
||||||
function currentSlot() {
|
function slotSec(duration) {
|
||||||
return Math.floor(Date.now() / 1000 / SLOT_SEC) * SLOT_SEC;
|
return DURATION_SECS[duration] || 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextSlot() {
|
function currentSlot(duration) {
|
||||||
return currentSlot() + SLOT_SEC;
|
const sec = slotSec(duration);
|
||||||
|
return Math.floor(Date.now() / 1000 / sec) * sec;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchBySlug(asset, slotTimestamp) {
|
function nextSlot(duration) {
|
||||||
const slug = `${asset}-updown-${config.makerDuration}-${slotTimestamp}`;
|
return currentSlot(duration) + slotSec(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBySlug(asset, duration, slotTimestamp) {
|
||||||
|
const slug = `${asset}-updown-${duration}-${slotTimestamp}`;
|
||||||
try {
|
try {
|
||||||
const resp = await proxyFetch(`${config.gammaHost}/markets/slug/${slug}`);
|
const resp = await proxyFetch(`${config.gammaHost}/markets/slug/${slug}`);
|
||||||
if (!resp.ok) return null;
|
if (!resp.ok) return null;
|
||||||
@@ -34,7 +39,7 @@ async function fetchBySlug(asset, slotTimestamp) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractMarketData(market, asset) {
|
function extractMarketData(market, asset, duration) {
|
||||||
const conditionId = market.conditionId || market.condition_id || '';
|
const conditionId = market.conditionId || market.condition_id || '';
|
||||||
if (!conditionId) return null;
|
if (!conditionId) return null;
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ function extractMarketData(market, asset) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
asset,
|
||||||
|
duration,
|
||||||
conditionId,
|
conditionId,
|
||||||
question: market.question || market.title || '',
|
question: market.question || market.title || '',
|
||||||
endTime: market.endDate || market.end_date_iso || market.endDateIso,
|
endTime: market.endDate || market.end_date_iso || market.endDateIso,
|
||||||
@@ -66,16 +72,16 @@ function extractMarketData(market, asset) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function scheduleAsset(asset, slotTimestamp) {
|
async function scheduleAsset(asset, duration, slotTimestamp) {
|
||||||
const key = `${asset}-${slotTimestamp}`;
|
const key = `${asset}-${duration}-${slotTimestamp}`;
|
||||||
if (seenKeys.has(key)) return;
|
if (seenKeys.has(key)) return;
|
||||||
|
|
||||||
const market = await fetchBySlug(asset, slotTimestamp);
|
const market = await fetchBySlug(asset, duration, slotTimestamp);
|
||||||
if (!market) return;
|
if (!market) return;
|
||||||
|
|
||||||
const data = extractMarketData(market, asset);
|
const data = extractMarketData(market, asset, duration);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
logger.warn(`MAKER: skipping ${asset.toUpperCase()} slot ${slotTimestamp} — missing token IDs`);
|
logger.warn(`MAKER: skipping ${asset.toUpperCase()} ${duration} slot ${slotTimestamp} — missing token IDs`);
|
||||||
seenKeys.add(key);
|
seenKeys.add(key);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -85,15 +91,15 @@ async function scheduleAsset(asset, slotTimestamp) {
|
|||||||
const openAt = data.eventStartTime ? new Date(data.eventStartTime).getTime() : slotTimestamp * 1000;
|
const openAt = data.eventStartTime ? new Date(data.eventStartTime).getTime() : slotTimestamp * 1000;
|
||||||
const elapsedSec = Math.round((Date.now() - openAt) / 1000);
|
const elapsedSec = Math.round((Date.now() - openAt) / 1000);
|
||||||
if (elapsedSec > 15) {
|
if (elapsedSec > 15) {
|
||||||
logger.info(`MAKER: ${asset.toUpperCase()} next slot already ${elapsedSec}s old — skipping`);
|
logger.info(`MAKER: ${asset.toUpperCase()} ${duration} next slot already ${elapsedSec}s old — skipping`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secsUntilOpen = Math.round((openAt - Date.now()) / 1000);
|
const secsUntilOpen = Math.round((openAt - Date.now()) / 1000);
|
||||||
if (secsUntilOpen > 0) {
|
if (secsUntilOpen > 0) {
|
||||||
logger.success(`MAKER: ${asset.toUpperCase()} found "${data.question.slice(0, 40)}" — placing orders (${secsUntilOpen}s before open)`);
|
logger.success(`MAKER: ${asset.toUpperCase()} ${duration} found "${data.question.slice(0, 40)}" — placing orders (${secsUntilOpen}s before open)`);
|
||||||
} else {
|
} else {
|
||||||
logger.success(`MAKER: ${asset.toUpperCase()} found "${data.question.slice(0, 40)}" — placing orders now`);
|
logger.success(`MAKER: ${asset.toUpperCase()} ${duration} found "${data.question.slice(0, 40)}" — placing orders now`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onMarketCb) onMarketCb(data);
|
if (onMarketCb) onMarketCb(data);
|
||||||
@@ -101,8 +107,14 @@ async function scheduleAsset(asset, slotTimestamp) {
|
|||||||
|
|
||||||
async function poll() {
|
async function poll() {
|
||||||
try {
|
try {
|
||||||
const next = nextSlot();
|
const tasks = [];
|
||||||
await Promise.all(config.makerAssets.map((asset) => scheduleAsset(asset, next)));
|
for (const duration of config.makerDurations) {
|
||||||
|
const next = nextSlot(duration);
|
||||||
|
for (const asset of config.makerAssets) {
|
||||||
|
tasks.push(scheduleAsset(asset, duration, next));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(tasks);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error('MAKER detector poll error:', err.message);
|
logger.error('MAKER detector poll error:', err.message);
|
||||||
}
|
}
|
||||||
@@ -115,10 +127,13 @@ export function startMakerDetector(onNewMarket) {
|
|||||||
poll();
|
poll();
|
||||||
pollTimer = setInterval(poll, config.makerPollInterval);
|
pollTimer = setInterval(poll, config.makerPollInterval);
|
||||||
|
|
||||||
const ns = nextSlot();
|
const durStr = config.makerDurations.join(', ');
|
||||||
const secsUntil = ns - Math.floor(Date.now() / 1000);
|
for (const duration of config.makerDurations) {
|
||||||
logger.info(`MAKER detector started — assets: ${config.makerAssets.join(', ').toUpperCase()} | duration: ${config.makerDuration}`);
|
const ns = nextSlot(duration);
|
||||||
logger.info(`Next slot: *-updown-${config.makerDuration}-${ns} (opens in ${secsUntil}s)`);
|
const secsUntil = ns - Math.floor(Date.now() / 1000);
|
||||||
|
logger.info(`MAKER detector — ${duration}: next slot *-updown-${duration}-${ns} (opens in ${secsUntil}s)`);
|
||||||
|
}
|
||||||
|
logger.info(`MAKER detector started — assets: ${config.makerAssets.join(', ').toUpperCase()} | durations: ${durStr}`);
|
||||||
logger.info(`Strategy: BUY @ $${config.makerBuyPrice} → SELL @ $${config.makerSellPrice} | ${config.makerTradeSize} shares/side`);
|
logger.info(`Strategy: BUY @ $${config.makerBuyPrice} → SELL @ $${config.makerSellPrice} | ${config.makerTradeSize} shares/side`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,9 @@
|
|||||||
* b. Place limit SELL for filled shares at makerSellPrice (e.g. 3c)
|
* b. Place limit SELL for filled shares at makerSellPrice (e.g. 3c)
|
||||||
* c. Cancel the other side's buy order
|
* c. Cancel the other side's buy order
|
||||||
* 5. Partial fills → partial sells placed immediately
|
* 5. Partial fills → partial sells placed immediately
|
||||||
* 6. Monitor sell orders until filled or 10s before market close
|
* 6. CL at 10s before close: cancel unfilled buy orders
|
||||||
* 7. CL at 10s: cancel unfilled sell orders (tokens resolve on-chain)
|
* 7. Place sells for any filled positions (retry 3x if settlement pending)
|
||||||
|
* 8. Monitor sell orders until filled or market close
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Side, OrderType } from '@polymarket/clob-client';
|
import { Side, OrderType } from '@polymarket/clob-client';
|
||||||
@@ -188,8 +189,8 @@ async function simCheckFill(tokenId, side, price) {
|
|||||||
// ── Core strategy ─────────────────────────────────────────────────────────────
|
// ── Core strategy ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function executeMakerStrategy(market) {
|
export async function executeMakerStrategy(market) {
|
||||||
const { asset, conditionId, question, endTime, yesTokenId, noTokenId, negRisk, tickSize } = market;
|
const { asset, duration, conditionId, question, endTime, yesTokenId, noTokenId, negRisk, tickSize } = market;
|
||||||
const tag = asset ? `[${asset.toUpperCase()}]` : '';
|
const tag = asset ? `[${asset.toUpperCase()}/${duration || '5m'}]` : '';
|
||||||
const label = question.substring(0, 40);
|
const label = question.substring(0, 40);
|
||||||
const sim = config.dryRun ? '[SIM] ' : '';
|
const sim = config.dryRun ? '[SIM] ' : '';
|
||||||
const { makerBuyPrice, makerSellPrice, makerTradeSize, makerMonitorMs } = config;
|
const { makerBuyPrice, makerSellPrice, makerTradeSize, makerMonitorMs } = config;
|
||||||
@@ -228,6 +229,7 @@ export async function executeMakerStrategy(market) {
|
|||||||
// ── 2. Build position state ──────────────────────────────────
|
// ── 2. Build position state ──────────────────────────────────
|
||||||
const pos = {
|
const pos = {
|
||||||
asset: asset || 'btc',
|
asset: asset || 'btc',
|
||||||
|
duration: duration || '5m',
|
||||||
conditionId,
|
conditionId,
|
||||||
question,
|
question,
|
||||||
endTime,
|
endTime,
|
||||||
@@ -306,7 +308,15 @@ async function monitorBuyPhase(pos, tag, sim) {
|
|||||||
|
|
||||||
while (!pos.winner) {
|
while (!pos.winner) {
|
||||||
const msLeft = new Date(pos.endTime).getTime() - Date.now();
|
const msLeft = new Date(pos.endTime).getTime() - Date.now();
|
||||||
if (msLeft <= 0) break;
|
// CL: cancel unfilled buy at 10s before market close
|
||||||
|
if (msLeft <= CL_SECONDS * 1000) {
|
||||||
|
if (side.buyOrderId && !side.cancelled) {
|
||||||
|
logger.warn(`MAKER${tag}: CL ${CL_SECONDS}s — cancelling ${sideName} buy`);
|
||||||
|
await cancelOrder(side.buyOrderId);
|
||||||
|
side.cancelled = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// Check fill
|
// Check fill
|
||||||
let fill;
|
let fill;
|
||||||
@@ -376,6 +386,45 @@ async function monitorBuyPhase(pos, tag, sim) {
|
|||||||
monitorSide('down'),
|
monitorSide('down'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Cleanup: cancel any remaining unfilled buy orders
|
||||||
|
for (const key of ['up', 'down']) {
|
||||||
|
const s = pos[key];
|
||||||
|
if (s.buyOrderId && !s.cancelled) {
|
||||||
|
await cancelOrder(s.buyOrderId);
|
||||||
|
s.cancelled = true;
|
||||||
|
logger.info(`MAKER${tag}: cancelled ${key.toUpperCase()} buy order`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Place sells for filled buys that don't have sell orders yet
|
||||||
|
for (const key of ['up', 'down']) {
|
||||||
|
const s = pos[key];
|
||||||
|
const soldShares = s.sellOrders.reduce((sum, so) => sum + so.shares, 0);
|
||||||
|
const unsold = s.buyFilled - soldShares;
|
||||||
|
if (unsold > 0) {
|
||||||
|
logger.info(`MAKER${tag}: placing sell for ${key.toUpperCase()} ${unsold.toFixed(2)} unsold shares`);
|
||||||
|
if (!config.dryRun) {
|
||||||
|
logger.info(`MAKER${tag}: waiting ${SELL_DELAY_MS / 1000}s for on-chain settlement...`);
|
||||||
|
await sleep(SELL_DELAY_MS);
|
||||||
|
}
|
||||||
|
const sellResult = await placeLimitSellWithRetry(
|
||||||
|
s.tokenId, unsold, makerSellPrice,
|
||||||
|
pos.tickSize, pos.negRisk, tag,
|
||||||
|
);
|
||||||
|
if (sellResult.success) {
|
||||||
|
s.sellOrders.push({
|
||||||
|
orderId: sellResult.orderId,
|
||||||
|
shares: unsold,
|
||||||
|
filled: false,
|
||||||
|
fillPrice: null,
|
||||||
|
});
|
||||||
|
logger.trade(`MAKER${tag}: ${sim}${key.toUpperCase()} SELL placed ${unsold.toFixed(2)} shares @ $${makerSellPrice}`);
|
||||||
|
} else {
|
||||||
|
logger.error(`MAKER${tag}: ${key.toUpperCase()} SELL failed after ${MAX_SELL_RETRIES} retries — tokens held to resolution`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!pos.winner) {
|
if (!pos.winner) {
|
||||||
const anyFill = pos.up.buyFilled > 0 || pos.down.buyFilled > 0;
|
const anyFill = pos.up.buyFilled > 0 || pos.down.buyFilled > 0;
|
||||||
if (anyFill) {
|
if (anyFill) {
|
||||||
@@ -389,7 +438,7 @@ async function monitorBuyPhase(pos, tag, sim) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Sell phase: monitor sell orders, CL at 10s before close ──────────────────
|
// ── Sell phase: monitor sell orders until market close ────────────────────────
|
||||||
|
|
||||||
async function monitorSellPhase(pos, tag, sim) {
|
async function monitorSellPhase(pos, tag, sim) {
|
||||||
if (pos.status === 'done') return;
|
if (pos.status === 'done') return;
|
||||||
@@ -413,19 +462,12 @@ async function monitorSellPhase(pos, tag, sim) {
|
|||||||
while (true) {
|
while (true) {
|
||||||
const msLeft = new Date(pos.endTime).getTime() - Date.now();
|
const msLeft = new Date(pos.endTime).getTime() - Date.now();
|
||||||
|
|
||||||
// ── CL at 10s: cancel unfilled sells, let tokens resolve on-chain ──
|
// Market closed — unfilled sells resolve on-chain
|
||||||
if (msLeft <= CL_SECONDS * 1000) {
|
if (msLeft <= 0) {
|
||||||
let unfilledCount = 0;
|
|
||||||
for (const so of side.sellOrders) {
|
|
||||||
if (!so.filled) {
|
|
||||||
await cancelOrder(so.orderId);
|
|
||||||
unfilledCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unfilledCount > 0) {
|
|
||||||
logger.warn(`MAKER${tag}: CL ${CL_SECONDS}s — cancelled ${unfilledCount} unfilled sell order(s), held to resolution`);
|
|
||||||
}
|
|
||||||
pos.status = side.totalSellFilled > 0 ? 'done' : 'expired-holding';
|
pos.status = side.totalSellFilled > 0 ? 'done' : 'expired-holding';
|
||||||
|
if (pos.status === 'expired-holding') {
|
||||||
|
logger.warn(`MAKER${tag}: market closed — unfilled sells held to resolution`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user