Skip to content

Commit

Permalink
feat: useCounterParty and usePositionLimit (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
zccz14 authored Oct 24, 2023
1 parent 1ca1685 commit 9d30d9f
Show file tree
Hide file tree
Showing 7 changed files with 161 additions and 21 deletions.
2 changes: 2 additions & 0 deletions @libs/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
export * from "./useCounterParty";
export * from "./useDelay";
export * from "./usePositionLimit";
export * from "./useSeriesMap";
export * from "./useThrottle";
export * from "./useTopK";
40 changes: 40 additions & 0 deletions @libs/utils/useCounterParty.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/**
* Derive a Counter Party account from the source account.
*
* The derived account has the same positions as the source account, but the positions' direction is reversed.
*/
export function useCounterParty(source_account_id: string) {
const src = useAccountInfo(source_account_id);
const tar = useAccountInfo(`${source_account_id}-CP`);
const ex = useExchange();
useEffect(() => {
const orders: IOrder[] = [];
for (const order of ex.listOrders()) {
if (order.account_id === src.account_id) {
const theOrder = {
...order,
account_id: tar.account_id,
client_order_id: UUID(),
direction:
order.direction === OrderDirection.OPEN_LONG
? OrderDirection.OPEN_SHORT
: order.direction === OrderDirection.OPEN_SHORT
? OrderDirection.OPEN_LONG
: order.direction === OrderDirection.CLOSE_LONG
? OrderDirection.CLOSE_SHORT
: order.direction === OrderDirection.CLOSE_SHORT
? OrderDirection.CLOSE_LONG
: order.direction,
};
ex.submitOrder(theOrder);
orders.push(theOrder);
}
}
return () => {
for (const order of orders) {
ex.cancelOrder(order.client_order_id);
}
};
});
return tar;
}
94 changes: 94 additions & 0 deletions @libs/utils/usePositionLimit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* Derive a PositionLimit account from the source account.
*
* The derived account has a specified position limit. Its positions' volume will be limited to the position limit.
*
*/
export function usePositionLimit(
source_account_id: string,
positionLimit: number
) {
const src = useAccountInfo(source_account_id);
const tar = useAccountInfo(`${source_account_id}-PL_${positionLimit}`);
const ex = useExchange();
useEffect(() => {
const orders: IOrder[] = [];
const mapProductToPosition = Object.fromEntries(
mergePositions(tar.positions).map((position) => [
`${position.product_id}-${position.variant}`,
position,
])
);
for (const order of ex.listOrders()) {
if (order.account_id === src.account_id) {
const positionVolume =
mapProductToPosition[
`${order.product_id}-${
[OrderDirection.CLOSE_LONG, OrderDirection.OPEN_LONG].includes(
order.direction
)
? PositionVariant.LONG
: PositionVariant.SHORT
}`
]?.volume ?? 0;
const volume = [
OrderDirection.CLOSE_LONG,
OrderDirection.CLOSE_SHORT,
].includes(order.direction)
? Math.min(order.volume, positionVolume)
: Math.min(order.volume, positionLimit - positionVolume);
if (volume > 0) {
const theOrder = {
...order,
account_id: tar.account_id,
client_order_id: UUID(),
volume,
};
ex.submitOrder(theOrder);
orders.push(theOrder);
}
}
}
return () => {
for (const order of orders) {
ex.cancelOrder(order.client_order_id);
}
};
});
return tar;
}

/**
* Merge Positions by product_id/variant
* @param positions - List of Positions
* @returns - Merged Positions
*
* @public
*/
const mergePositions = (positions: IPosition[]): IPosition[] => {
const mapProductIdToPosition = positions.reduce((acc, cur) => {
const { product_id, variant } = cur;
if (!acc[`${product_id}-${variant}`]) {
acc[`${product_id}-${variant}`] = { ...cur };
} else {
let thePosition = acc[`${product_id}-${variant}`];
thePosition = {
...thePosition,
volume: thePosition.volume + cur.volume,
free_volume: thePosition.free_volume + cur.free_volume,
position_price:
(thePosition.position_price * thePosition.volume +
cur.position_price * cur.volume) /
(thePosition.volume + cur.volume),
floating_profit: thePosition.floating_profit + cur.floating_profit,
closable_price:
(thePosition.closable_price * thePosition.volume +
cur.closable_price * cur.volume) /
(thePosition.volume + cur.volume),
};
acc[`${product_id}-${variant}`] = thePosition;
}
return acc;
}, {} as Record<string, IPosition>);
return Object.values(mapProductIdToPosition);
};
5 changes: 4 additions & 1 deletion @models/double-ma.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// 双均线策略 (Double Moving Average)
// 当短期均线由下向上穿越长期均线时做多 (金叉)
// 当短期均线由上向下穿越长期均线时做空 (死叉)
import { useSMA } from "@libs";
import { useCounterParty, useSMA } from "@libs";

export default () => {
// 使用收盘价序列
Expand All @@ -13,6 +13,8 @@ export default () => {
const sma20 = useSMA(close, 20);
const sma60 = useSMA(close, 60);

const accountInfo = useAccountInfo();

// 设置仓位管理器
const pL = useSinglePosition(product_id, PositionVariant.LONG);
const pS = useSinglePosition(product_id, PositionVariant.SHORT);
Expand All @@ -30,4 +32,5 @@ export default () => {
pS.setTargetVolume(1);
}
}, [idx]);
useCounterParty(accountInfo.account_id);
};
24 changes: 10 additions & 14 deletions @models/double-ma.ts.batch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,15 @@ import { ChinaFutureOHLCKeys } from "@libs";
// 需要在模块中 default 导出一个函数,该函数返回一个可迭代的配置 (数组, 生成函数, 异步生成函数均可,推荐使用生成函数)
export default function* () {
for (const SomeKey of ChinaFutureOHLCKeys) {
for (const as_counterparty of [true, false]) {
yield {
// 模型代码入口
entry: "/@models/double-ma.ts",
// account_id 用以区分不同的回测结果
account_id: `Model-2MA-${SomeKey}${as_counterparty ? "-CP" : ""}`,
// 模型参数
agent_params: {
SomeKey,
},
// 是否作为模型的对手方
as_counterparty,
};
}
yield {
// 模型代码入口
entry: "/@models/double-ma.ts",
// account_id 用以区分不同的回测结果
account_id: `Model-2MA-${SomeKey}`,
// 模型参数
agent_params: {
SomeKey,
},
};
}
}
8 changes: 4 additions & 4 deletions @models/turtle-long.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// 当价格突破20日价格的最高价的时候,开仓做多
// 开仓后,当多头头寸在突破过去10日最低价处止盈离市。
// 开仓后,当市价继续向盈利方向突破1/2 ATR时加仓,止损位为2ATR, 每加仓一次,止损位就提高1/2 ATR
import { useMAX, useMIN, useATR } from "@libs";
import { useATR, useMAX, useMIN } from "@libs";

export default () => {
const { product_id, high, low, close } = useParamOHLC("SomeKey");
Expand All @@ -12,12 +12,12 @@ export default () => {
const LL = useMIN(low, N);

const pL = useSinglePosition(product_id, PositionVariant.LONG);

let price_break = useRef(0);
const atr = useATR(high, low, close, 14);
const price_break = useRef(0);
useEffect(() => {
const idx = close.length - 2;
if (idx < N) return;
const atr = useATR(high, low, close, 14);

const atrValue = atr[idx];

// 当价格突破20日价格的最高价时,首次开仓做多,同时记录当前价格
Expand Down
9 changes: 7 additions & 2 deletions global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,13 +531,14 @@ declare const useParamOHLC: (key: string) => ReturnType<typeof useOHLC> & {
period_in_sec: number;
};
/** Use Account Info */
declare const useAccountInfo: () => IAccountInfo;
declare const useAccountInfo: (account_id?: string) => IAccountInfo;
/**
* Use a position manager for a specified product and direction
*/
declare const useSinglePosition: (
product_id: string,
variant: PositionVariant
variant: PositionVariant,
account_id?: string
) => {
targetVolume: number;
takeProfitPrice: number;
Expand All @@ -564,6 +565,10 @@ declare const useLog: () => (...params: any[]) => void;
declare const useRecordTable: <T extends Record<string, any>>(
title: string
) => T[];
/** Get Time String */
declare const formatTime: (timestamp: number) => string;
/** Generate a UUID (Universal-Unique-ID) */
declare const UUID: () => string;

// Deployment script context
/**
Expand Down

0 comments on commit 9d30d9f

Please sign in to comment.