fix: Stale-Event-Guard — Events älter 2h werden nur geloggt, nie gehandelt (Final-Review C1)
Erster Prod-Start: RSS liefert sofort die letzten ~100 Posts; ohne Guard würde ein tagealter Coin-Post zum Tagespreis gekauft (nicht von der Event-Study gedeckt). Gilt symmetrisch für on-chain (Downtime-Aufholjagd) und Truth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ export const positions = pgTable('positions', {
|
||||
|
||||
export const paperTrades = pgTable('paper_trades', {
|
||||
id: serial('id').primaryKey(),
|
||||
bot: text('bot').notNull().default('trend'), // 'trend' | 'grid'
|
||||
bot: text('bot').notNull().default('trend'), // 'trend' | 'grid' | 'trump'
|
||||
pair: varchar('pair', { length: 16 }).notNull(),
|
||||
side: text('side').notNull(),
|
||||
entryTs: timestamp('entry_ts', { withTimezone: true }).notNull(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { dedupeTruthEvents, passesNotional } from './poller';
|
||||
import { consumedAtForInsert, dedupeTruthEvents, passesNotional, STALE_EVENT_MS } from './poller';
|
||||
|
||||
describe('passesNotional', () => {
|
||||
test('amount × Preis gegen MIN_NOTIONAL_USD (50k)', () => {
|
||||
@@ -22,3 +22,12 @@ describe('dedupeTruthEvents', () => {
|
||||
expect(dedupeTruthEvents(batch, existing).map((e) => e.url)).toEqual(['u2', 'u4']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('consumedAtForInsert', () => {
|
||||
const NOW = 1_750_000_000_000;
|
||||
test('frisches Event → null (handelbar), zu altes → sofort consumed (nur Log)', () => {
|
||||
expect(consumedAtForInsert(NOW - 5 * 60_000, NOW)).toBeNull(); // 5 min alt
|
||||
expect(consumedAtForInsert(NOW - STALE_EVENT_MS + 1, NOW)).toBeNull(); // knapp drunter
|
||||
expect(consumedAtForInsert(NOW - STALE_EVENT_MS - 1, NOW)).toEqual(new Date(NOW)); // drüber → consumed
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,11 +10,21 @@ import type { Pair } from '../types';
|
||||
const M15 = 15 * 60 * 1000;
|
||||
/** Obergrenze Blöcke je Zyklus (~4 getLogs-Calls); Ethereum macht ~25 Blöcke/5min — reichlich Aufholpuffer. */
|
||||
const MAX_BLOCKS_PER_CYCLE = 20_000;
|
||||
/** Events, die beim Einsammeln älter sind, werden nur geloggt (consumed), nie gehandelt —
|
||||
* die Study misst Entries am Event-Open; ein Tage später entdecktes Event zum Tagespreis
|
||||
* zu kaufen wäre eine andere (ungetestete) Strategie. Deckt v. a. den ersten Prod-Start ab:
|
||||
* der RSS-Feed liefert sofort die letzten ~100 Posts, die dürfen keine Käufe auslösen. */
|
||||
export const STALE_EVENT_MS = 2 * 3600_000;
|
||||
|
||||
export function passesNotional(amount: number, price: number | null): boolean {
|
||||
return price !== null && amount * price >= MIN_NOTIONAL_USD;
|
||||
}
|
||||
|
||||
/** consumedAt-Wert für einen frischen Insert: jetzt (= nur loggen) wenn das Event zu alt ist, sonst null. */
|
||||
export function consumedAtForInsert(eventTs: number, now: number): Date | null {
|
||||
return now - eventTs > STALE_EVENT_MS ? new Date(now) : null;
|
||||
}
|
||||
|
||||
/** existing: Coin → eventTs des jüngsten Truth-Events in der DB. Batch muss ts-aufsteigend sein. */
|
||||
export function dedupeTruthEvents(
|
||||
batch: { symbol: string; ts: number; url: string }[],
|
||||
@@ -69,6 +79,7 @@ export async function pollOnchain(): Promise<number> {
|
||||
.values({
|
||||
source: 'onchain', token: t.symbol, instrument: t.instrument,
|
||||
eventTs: new Date(ts), ref: t.txHash, notionalUsd: t.amount * price!,
|
||||
consumedAt: consumedAtForInsert(ts, Date.now()),
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
inserted++;
|
||||
@@ -103,7 +114,10 @@ export async function pollTruth(): Promise<number> {
|
||||
const kw = COIN_KEYWORDS.find((c) => c.symbol === e.symbol)!;
|
||||
await db
|
||||
.insert(trumpEvents)
|
||||
.values({ source: 'truth', token: e.symbol, instrument: kw.instrument, eventTs: new Date(e.ts), ref: e.url })
|
||||
.values({
|
||||
source: 'truth', token: e.symbol, instrument: kw.instrument, eventTs: new Date(e.ts), ref: e.url,
|
||||
consumedAt: consumedAtForInsert(e.ts, Date.now()),
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
inserted++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user