programgarden-community 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- programgarden_community-0.1.1/PKG-INFO +21 -0
- programgarden_community-0.1.1/README.md +1 -0
- programgarden_community-0.1.1/programgarden_community/__init__.py +126 -0
- programgarden_community-0.1.1/programgarden_community/overseas_stock/new_buy_conditions/stock_split_funds/README.md +0 -0
- programgarden_community-0.1.1/programgarden_community/overseas_stock/new_buy_conditions/stock_split_funds/__init__.py +102 -0
- programgarden_community-0.1.1/programgarden_community/overseas_stock/new_sell_conditions/loss_cut/__init__.py +56 -0
- programgarden_community-0.1.1/programgarden_community/overseas_stock/strategy_conditions/sma_golden_dead/README.md +0 -0
- programgarden_community-0.1.1/programgarden_community/overseas_stock/strategy_conditions/sma_golden_dead/__init__.py +256 -0
- programgarden_community-0.1.1/pyproject.toml +26 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: programgarden-community
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: 증권 분석에 필요한 외부 전략 모아둔 플러그인
|
|
5
|
+
Author: 프로그램동산
|
|
6
|
+
Author-email: coding@programgarden.com
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Dist: croniter (>=6.0.0,<7.0.0)
|
|
15
|
+
Requires-Dist: programgarden-core (>=0.1.0,<0.2.0)
|
|
16
|
+
Requires-Dist: programgarden-finance (>=0.1.0,<0.2.0)
|
|
17
|
+
Requires-Dist: pydantic (>=2.11.7,<3.0.0)
|
|
18
|
+
Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
외부 플러그인 모음
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
외부 플러그인 모음
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Programgarden community package root.
|
|
2
|
+
|
|
3
|
+
This module implements a LangChain-style lazy import surface: names listed in
|
|
4
|
+
``_MODULE_MAP`` are imported from their submodules on first access via
|
|
5
|
+
``__getattr__``. Use ``getCommunityTool(name)`` to dynamically retrieve a class
|
|
6
|
+
by its id string.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from importlib import metadata
|
|
10
|
+
import warnings
|
|
11
|
+
from typing import Any, List, Optional
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
__version__ = metadata.version(__package__)
|
|
15
|
+
except metadata.PackageNotFoundError:
|
|
16
|
+
__version__ = ""
|
|
17
|
+
del metadata
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"SMAGoldenDeadCross",
|
|
21
|
+
"StockSplitFunds",
|
|
22
|
+
"getCommunityTool",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _warn_on_import(name: str, replacement: Optional[str] = None) -> None:
|
|
27
|
+
"""Emit a warning when a name is imported from the package root.
|
|
28
|
+
|
|
29
|
+
This mirrors LangChain's behaviour: importing many symbols from the root
|
|
30
|
+
is convenient but we suggest importing from the actual submodule.
|
|
31
|
+
"""
|
|
32
|
+
if replacement:
|
|
33
|
+
warnings.warn(
|
|
34
|
+
f"Importing {name} from programgarden_community root is discouraged; "
|
|
35
|
+
f"prefer {replacement}",
|
|
36
|
+
stacklevel=3,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def __getattr__(name: str) -> Any:
|
|
41
|
+
"""LangChain-style explicit lazy import surface.
|
|
42
|
+
|
|
43
|
+
Each supported top-level name is handled with an explicit branch that
|
|
44
|
+
imports the real implementation from its submodule on first access.
|
|
45
|
+
"""
|
|
46
|
+
if name == "SMAGoldenDeadCross":
|
|
47
|
+
from programgarden_community.overseas_stock.strategy_conditions.sma_golden_dead import (
|
|
48
|
+
SMAGoldenDeadCross,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_warn_on_import(
|
|
52
|
+
name,
|
|
53
|
+
replacement=(
|
|
54
|
+
"programgarden_community.overseas_stock.strategy_conditions.sma_golden_dead.SMAGoldenDeadCross"
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
globals()[name] = SMAGoldenDeadCross
|
|
59
|
+
return SMAGoldenDeadCross
|
|
60
|
+
|
|
61
|
+
if name == "StockSplitFunds":
|
|
62
|
+
from programgarden_community.overseas_stock.new_buy_conditions.stock_split_funds import (
|
|
63
|
+
StockSplitFunds,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
_warn_on_import(
|
|
67
|
+
name,
|
|
68
|
+
replacement=(
|
|
69
|
+
"programgarden_community.overseas_stock.new_buy_conditions.stock_split_funds.StockSplitFunds"
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
globals()[name] = StockSplitFunds
|
|
74
|
+
return StockSplitFunds
|
|
75
|
+
|
|
76
|
+
if name == "TrailingStopManager":
|
|
77
|
+
from programgarden_community.overseas_stock.new_sell_conditions.trailing_stop import (
|
|
78
|
+
TrailingStopManager,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
_warn_on_import(
|
|
82
|
+
name,
|
|
83
|
+
replacement=(
|
|
84
|
+
"programgarden_community.overseas_stock.new_sell_conditions.trailing_stop.TrailingStopManager"
|
|
85
|
+
),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
globals()[name] = TrailingStopManager
|
|
89
|
+
return TrailingStopManager
|
|
90
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def __dir__() -> List[str]:
|
|
94
|
+
shown: List[str] = list(globals().keys())
|
|
95
|
+
shown.extend(["SMAGoldenDeadCross", "StockSplitFunds"])
|
|
96
|
+
return sorted(shown)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def getCommunityCondition(class_name: str) -> Any:
|
|
100
|
+
"""Dynamically import and return a class by its registered id.
|
|
101
|
+
|
|
102
|
+
This mirrors the explicit-branch behaviour above and avoids importing the
|
|
103
|
+
entire package root.
|
|
104
|
+
"""
|
|
105
|
+
if class_name == "SMAGoldenDeadCross":
|
|
106
|
+
from programgarden_community.overseas_stock.strategy_conditions.sma_golden_dead import (
|
|
107
|
+
SMAGoldenDeadCross,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return SMAGoldenDeadCross
|
|
111
|
+
|
|
112
|
+
if class_name == "StockSplitFunds":
|
|
113
|
+
from programgarden_community.overseas_stock.new_buy_conditions.stock_split_funds import (
|
|
114
|
+
StockSplitFunds,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return StockSplitFunds
|
|
118
|
+
|
|
119
|
+
if class_name == "TrailingStopManager":
|
|
120
|
+
from programgarden_community.overseas_stock.new_sell_conditions.trailing_stop import (
|
|
121
|
+
TrailingStopManager,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return TrailingStopManager
|
|
125
|
+
|
|
126
|
+
raise ValueError(f"{class_name} is not a valid community tool.")
|
|
File without changes
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""
|
|
2
|
+
균등하게 분할매수하기 위한 자금배분
|
|
3
|
+
"""
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
from programgarden_core import (
|
|
6
|
+
BaseBuyOverseasStock,
|
|
7
|
+
BaseBuyOverseasStockResponseType,
|
|
8
|
+
)
|
|
9
|
+
from programgarden_finance import LS, g3101
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class StockSplitFunds(BaseBuyOverseasStock):
|
|
13
|
+
|
|
14
|
+
id: str = "StockSplitFunds"
|
|
15
|
+
description: str = "주식 분할 자금"
|
|
16
|
+
securities: List[str] = ["ls-sec.co.kr"]
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
appkey: Optional[str] = None,
|
|
21
|
+
appsecretkey: Optional[str] = None,
|
|
22
|
+
percent_balance: float = 10.0,
|
|
23
|
+
max_symbols: float = 5,
|
|
24
|
+
):
|
|
25
|
+
"""
|
|
26
|
+
주식 분할 자금 초기화
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
appkey (Optional[str]): LS증권 앱키
|
|
30
|
+
appsecretkey (Optional[str]): LS증권 앱시크릿키
|
|
31
|
+
percent_balance (float): 현재 예수금의 몇 %를 사용할지
|
|
32
|
+
max_symbols (float): 최대 몇 종목까지 매수할지
|
|
33
|
+
"""
|
|
34
|
+
super().__init__()
|
|
35
|
+
|
|
36
|
+
self.appkey = appkey
|
|
37
|
+
self.appsecretkey = appsecretkey
|
|
38
|
+
self.percent_balance = percent_balance
|
|
39
|
+
self.max_symbols = max_symbols
|
|
40
|
+
|
|
41
|
+
async def execute(self) -> List[BaseBuyOverseasStockResponseType]:
|
|
42
|
+
|
|
43
|
+
ls = LS.get_instance()
|
|
44
|
+
if not ls.is_logged_in():
|
|
45
|
+
await ls.async_login(
|
|
46
|
+
appkey=self.appkey,
|
|
47
|
+
appsecretkey=self.appsecretkey
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
fcurr_dps = self.fcurr_dps * self.percent_balance
|
|
51
|
+
|
|
52
|
+
# 종목당 최대 매수 금액
|
|
53
|
+
per_max_amt = round(fcurr_dps / self.max_symbols, 2)
|
|
54
|
+
|
|
55
|
+
orders: List[BaseBuyOverseasStockResponseType] = []
|
|
56
|
+
for symbol in self.available_symbols:
|
|
57
|
+
if len(orders) >= self.max_symbols:
|
|
58
|
+
break
|
|
59
|
+
|
|
60
|
+
exchcd = symbol.get("exchcd")
|
|
61
|
+
symbol = symbol.get("symbol")
|
|
62
|
+
|
|
63
|
+
cur = await ls.overseas_stock().market().g3101(
|
|
64
|
+
body=g3101.G3101InBlock(
|
|
65
|
+
keysymbol=exchcd+symbol,
|
|
66
|
+
exchcd=exchcd,
|
|
67
|
+
symbol=symbol
|
|
68
|
+
)
|
|
69
|
+
).req_async()
|
|
70
|
+
|
|
71
|
+
# 계산된 금액으로 살 수 있는 최대 수량(정수)
|
|
72
|
+
price = round(float(cur.block.price), 1)
|
|
73
|
+
if price <= 0:
|
|
74
|
+
buy_qty = 0
|
|
75
|
+
else:
|
|
76
|
+
buy_qty = int(per_max_amt // price)
|
|
77
|
+
if buy_qty < 1:
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# 주문 생성
|
|
81
|
+
order: BaseBuyOverseasStockResponseType = {
|
|
82
|
+
"success": True,
|
|
83
|
+
"ord_ptn_code": "02",
|
|
84
|
+
"ord_mkt_code": exchcd,
|
|
85
|
+
"isu_no": symbol,
|
|
86
|
+
"ord_qty": buy_qty,
|
|
87
|
+
"ovrs_ord_prc": price,
|
|
88
|
+
"ordprc_ptn_code": "00",
|
|
89
|
+
"brk_tp_code": "01"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
orders.append(order)
|
|
93
|
+
|
|
94
|
+
return orders
|
|
95
|
+
|
|
96
|
+
async def on_real_order_receive(self, order_type, response):
|
|
97
|
+
print(f"매수 Community 주문 데이터 수신: {order_type}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = [
|
|
101
|
+
"StockSplitFunds"
|
|
102
|
+
]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import List
|
|
4
|
+
from programgarden_core import (
|
|
5
|
+
BaseSellOverseasStock, BaseSellOverseasStockResponseType
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BasicLossCutManager(BaseSellOverseasStock):
|
|
10
|
+
|
|
11
|
+
id: str = "BasicLossCutManager"
|
|
12
|
+
description: str = "기본 손절매 매니저"
|
|
13
|
+
securities: List[str] = ["ls-sec.co.kr"]
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
losscut: float = -5,
|
|
18
|
+
):
|
|
19
|
+
"""
|
|
20
|
+
기본 손절매 매니저 초기화
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
losscut (float): 손절매 비율
|
|
24
|
+
"""
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
self.losscut = losscut
|
|
28
|
+
|
|
29
|
+
async def execute(self) -> List[BaseSellOverseasStockResponseType]:
|
|
30
|
+
|
|
31
|
+
results: List[BaseSellOverseasStockResponseType] = []
|
|
32
|
+
for held in self.held_symbols:
|
|
33
|
+
shtn_isu_no = held.get("ShtnIsuNo")
|
|
34
|
+
fcurr_mkt_code = held.get("FcurrMktCode")
|
|
35
|
+
keysymbol = fcurr_mkt_code + shtn_isu_no
|
|
36
|
+
|
|
37
|
+
rnl_rat = float(held.get("RnlRat", 0))
|
|
38
|
+
|
|
39
|
+
if rnl_rat <= self.losscut:
|
|
40
|
+
print(f"손절매 조건 충족: {keysymbol} 손익률={rnl_rat:.2f}% <= {self.losscut}%")
|
|
41
|
+
|
|
42
|
+
result: BaseSellOverseasStockResponseType = {
|
|
43
|
+
"success": True,
|
|
44
|
+
"ord_ptn_code": "01",
|
|
45
|
+
"ord_mkt_code": fcurr_mkt_code,
|
|
46
|
+
"shtn_isu_no": shtn_isu_no,
|
|
47
|
+
"ord_qty": held.get("AstkSellAbleQty", 0),
|
|
48
|
+
"ovrs_ord_prc": 0.0,
|
|
49
|
+
"ordprc_ptn_code": "03",
|
|
50
|
+
"crcy_code": "USD",
|
|
51
|
+
"pnl_rat": rnl_rat,
|
|
52
|
+
"pchs_amt": held.get("PchsAmt", 0.0),
|
|
53
|
+
}
|
|
54
|
+
results.append(result)
|
|
55
|
+
|
|
56
|
+
return results
|
|
File without changes
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Moving average golden/dead cross detection conditions
|
|
3
|
+
|
|
4
|
+
1) Observed a dead->golden where golden_price > dead_price (candidate)
|
|
5
|
+
2) The golden occurred within the most recent 2 data points
|
|
6
|
+
3) The latest alignment is golden (still maintained)
|
|
7
|
+
"""
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import List, Literal, Optional, TypedDict
|
|
10
|
+
from programgarden_core import (
|
|
11
|
+
BaseConditionResponseType,
|
|
12
|
+
BaseCondition,
|
|
13
|
+
)
|
|
14
|
+
from programgarden_finance import LS, g3204
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ChartDay(TypedDict):
|
|
18
|
+
"""
|
|
19
|
+
차트 일별 데이터 타입
|
|
20
|
+
"""
|
|
21
|
+
date: str # 날짜
|
|
22
|
+
price: float # 종가
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class SMASignal:
|
|
27
|
+
"""
|
|
28
|
+
SMA 신호 데이터 클래스
|
|
29
|
+
"""
|
|
30
|
+
cross: Literal["golden", "dead", "none"]
|
|
31
|
+
price: float
|
|
32
|
+
volume: float
|
|
33
|
+
date: str
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SMAGoldenDeadCross(BaseCondition):
|
|
37
|
+
"""
|
|
38
|
+
SMA 해외 주식 클래스
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
id: str = "SMAGoldenDeadCross"
|
|
42
|
+
description: str = """
|
|
43
|
+
Moving average golden/dead cross detection conditions
|
|
44
|
+
|
|
45
|
+
1) Observed a dead->golden where golden_price > dead_price (candidate)
|
|
46
|
+
2) The golden occurred within the most recent 2 data points
|
|
47
|
+
3) The latest alignment is golden (still maintained)
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
start_date: Optional[str],
|
|
53
|
+
end_date: Optional[str],
|
|
54
|
+
long_period: int,
|
|
55
|
+
short_period: int,
|
|
56
|
+
time_category: Literal["months", "weeks", "days"] = "days",
|
|
57
|
+
days_prices: Optional[list[ChartDay]] = None,
|
|
58
|
+
use_ls: bool = True,
|
|
59
|
+
alignment: Literal["golden", "dead"] = "golden",
|
|
60
|
+
appkey: Optional[str] = None,
|
|
61
|
+
appsecretkey: Optional[str] = None,
|
|
62
|
+
):
|
|
63
|
+
"""
|
|
64
|
+
SMA 해외 주식 클래스 초기화
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
symbol (str): 종목 코드, ex) "82TSLA"
|
|
68
|
+
exchcd (str): LS증권에서 사용하는 거래소 코드, ex) "82"
|
|
69
|
+
start_date (Optional[str]): 시작 날짜, ex) 20230101
|
|
70
|
+
end_date (Optional[str]): 종료 날짜, ex) 20231231
|
|
71
|
+
long_period: int: 롱 포지션 기간
|
|
72
|
+
short_period: int: 숏 포지션 기간
|
|
73
|
+
time_category (Literal["months", "weeks", "days", "minutes"]): 카테고리
|
|
74
|
+
days_prices (Optional[list[ChartDay]]): 기간 동안의 종가 리스트
|
|
75
|
+
use_ls (bool): LS증권 데이터를 사용할지 여부
|
|
76
|
+
alignment (Literal["golden", "dead"]): 정렬 방식
|
|
77
|
+
appkey (Optional[str]): LS증권 앱키
|
|
78
|
+
appsecretkey (Optional[str]): LS증권 앱시크릿키
|
|
79
|
+
"""
|
|
80
|
+
super().__init__()
|
|
81
|
+
|
|
82
|
+
if not use_ls and not days_prices:
|
|
83
|
+
raise ValueError("LS증권 데이터를 사용하지 않는 경우 days_prices가 필요합니다.")
|
|
84
|
+
|
|
85
|
+
if use_ls and (not appkey or not appsecretkey):
|
|
86
|
+
raise ValueError("LS증권 데이터를 사용하려면 appkey와 appsecretkey가 필요합니다.")
|
|
87
|
+
|
|
88
|
+
self.start_date = start_date
|
|
89
|
+
self.end_date = end_date
|
|
90
|
+
|
|
91
|
+
# store provided SMA periods and helper list used by calculator
|
|
92
|
+
self.long_period = long_period
|
|
93
|
+
self.short_period = short_period
|
|
94
|
+
# list of SMA periods used throughout the calculator (short -> long)
|
|
95
|
+
self.sma_periods = [self.short_period, self.long_period]
|
|
96
|
+
|
|
97
|
+
# transition detection state:
|
|
98
|
+
# last observed dead cross price (None if not seen yet)
|
|
99
|
+
self._last_dead_price = None
|
|
100
|
+
# whether a valid dead->golden transition (golden price > dead price) was observed
|
|
101
|
+
self._transition_detected = False
|
|
102
|
+
|
|
103
|
+
self.time_category = time_category
|
|
104
|
+
self.days_prices = days_prices if days_prices is not None else []
|
|
105
|
+
self.use_ls = use_ls
|
|
106
|
+
self.alignment = alignment
|
|
107
|
+
self.appkey = appkey
|
|
108
|
+
self.appsecretkey = appsecretkey
|
|
109
|
+
|
|
110
|
+
async def execute(self) -> BaseConditionResponseType:
|
|
111
|
+
"""
|
|
112
|
+
SMA 해외 주식 전략을 실행합니다.
|
|
113
|
+
이 메서드는 비동기적으로 실행됩니다.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
self.ls = LS.get_instance()
|
|
117
|
+
if not self.ls.token_manager.is_token_available():
|
|
118
|
+
await self.ls.async_login()
|
|
119
|
+
exchcd = self.symbol.get("exchcd")
|
|
120
|
+
symbol = self.symbol.get("symbol")
|
|
121
|
+
|
|
122
|
+
gubun = "2"
|
|
123
|
+
if self.time_category == "days":
|
|
124
|
+
gubun = "2"
|
|
125
|
+
elif self.time_category == "weeks":
|
|
126
|
+
gubun = "3"
|
|
127
|
+
elif self.time_category == "months":
|
|
128
|
+
gubun = "4"
|
|
129
|
+
|
|
130
|
+
m_g3204 = self.ls.overseas_stock().chart().g3204(
|
|
131
|
+
g3204.G3204InBlock(
|
|
132
|
+
sdate=self.start_date,
|
|
133
|
+
edate=self.end_date,
|
|
134
|
+
keysymbol=exchcd + symbol,
|
|
135
|
+
exchcd=exchcd,
|
|
136
|
+
symbol=symbol,
|
|
137
|
+
gubun=gubun,
|
|
138
|
+
qrycnt=500,
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
occurs_result = await m_g3204.occurs_req_async()
|
|
143
|
+
all_blocks: List[g3204.blocks.G3204OutBlock1] = []
|
|
144
|
+
for response in occurs_result:
|
|
145
|
+
all_blocks.extend(response.block1)
|
|
146
|
+
all_blocks.sort(key=lambda x: x.date)
|
|
147
|
+
|
|
148
|
+
self.all_signal = self._calculator(all_blocks)
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
"condition_id": self.id,
|
|
152
|
+
# "success": getattr(self, "_transition_detected", False),
|
|
153
|
+
"success": True,
|
|
154
|
+
"exchange": self.symbol.get("exchcd", None),
|
|
155
|
+
"symbol": self.symbol.get("symbol", None),
|
|
156
|
+
"data": self.all_signal
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
def _calculator(
|
|
160
|
+
self,
|
|
161
|
+
blocks: List[g3204.blocks.G3204OutBlock1],
|
|
162
|
+
) -> List[SMASignal]:
|
|
163
|
+
"""
|
|
164
|
+
응답을 처리하는 메소드
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
response (g3204.blocks.G3204Response): 응답 객체
|
|
168
|
+
status (RequestStatus): 요청 상태
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
signals = []
|
|
172
|
+
|
|
173
|
+
# For simplicity we only handle a single short vs long SMA pair
|
|
174
|
+
short_period = int(self.short_period)
|
|
175
|
+
long_period = int(self.long_period)
|
|
176
|
+
max_period = max(self.sma_periods)
|
|
177
|
+
|
|
178
|
+
# tracking variables to support the new requirement:
|
|
179
|
+
# - golden must have occurred within the last 2 data points
|
|
180
|
+
# - golden alignment must still be maintained at the end
|
|
181
|
+
last_dead_price = None
|
|
182
|
+
golden_index = None
|
|
183
|
+
golden_price = None
|
|
184
|
+
transition_candidate = False
|
|
185
|
+
last_alignment_golden = False
|
|
186
|
+
|
|
187
|
+
for idx, block in enumerate(blocks):
|
|
188
|
+
# SMA 계산을 위한 데이터 수집
|
|
189
|
+
if not hasattr(self, 'price_history'):
|
|
190
|
+
self.price_history = []
|
|
191
|
+
|
|
192
|
+
self.price_history.append(block.close)
|
|
193
|
+
|
|
194
|
+
cross_type = "none"
|
|
195
|
+
|
|
196
|
+
# 현재 SMA 계산 (데이터가 충분할 때)
|
|
197
|
+
if len(self.price_history) >= max_period:
|
|
198
|
+
current_short = sum(self.price_history[-short_period:]) / short_period
|
|
199
|
+
current_long = sum(self.price_history[-long_period:]) / long_period
|
|
200
|
+
|
|
201
|
+
# update last alignment state (used after loop)
|
|
202
|
+
last_alignment_golden = current_short > current_long
|
|
203
|
+
|
|
204
|
+
# 이전 시점 SMA가 존재할 때만 크로스 판정
|
|
205
|
+
if len(self.price_history) > max_period:
|
|
206
|
+
prev_short = sum(self.price_history[-short_period-1:-1]) / short_period
|
|
207
|
+
prev_long = sum(self.price_history[-long_period-1:-1]) / long_period
|
|
208
|
+
|
|
209
|
+
# 정렬 상태: strict 비교 (현재 단기 > 장기 => 골든 정렬)
|
|
210
|
+
all_golden_aligned = current_short > current_long
|
|
211
|
+
all_dead_aligned = current_short < current_long
|
|
212
|
+
|
|
213
|
+
# 최근 크로스 발생 여부
|
|
214
|
+
recent_golden_cross = (prev_short <= prev_long and current_short > current_long)
|
|
215
|
+
recent_dead_cross = (prev_short >= prev_long and current_short < current_long)
|
|
216
|
+
|
|
217
|
+
if all_golden_aligned and recent_golden_cross:
|
|
218
|
+
cross_type = "golden"
|
|
219
|
+
elif all_dead_aligned and recent_dead_cross:
|
|
220
|
+
cross_type = "dead"
|
|
221
|
+
|
|
222
|
+
sma_signal = SMASignal(
|
|
223
|
+
cross=cross_type,
|
|
224
|
+
price=block.close,
|
|
225
|
+
volume=block.volume,
|
|
226
|
+
date=block.date,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
# transition tracking: record dead/golden with their indices and prices
|
|
230
|
+
if cross_type == "dead":
|
|
231
|
+
last_dead_price = block.close
|
|
232
|
+
# keep compatibility with instance attribute
|
|
233
|
+
self._last_dead_price = last_dead_price
|
|
234
|
+
elif cross_type == "golden":
|
|
235
|
+
golden_index = idx
|
|
236
|
+
golden_price = block.close
|
|
237
|
+
# golden must be strictly higher than last dead price to be candidate
|
|
238
|
+
if last_dead_price is not None and golden_price > last_dead_price:
|
|
239
|
+
transition_candidate = True
|
|
240
|
+
|
|
241
|
+
signals.append(sma_signal)
|
|
242
|
+
|
|
243
|
+
# Final evaluation: mark transition detected only when
|
|
244
|
+
# 1) we observed a dead->golden where golden_price > dead_price (candidate)
|
|
245
|
+
# 2) the golden occurred within the most recent 2 data points
|
|
246
|
+
# 3) the latest alignment is golden (still maintained)
|
|
247
|
+
self._transition_detected = False
|
|
248
|
+
if transition_candidate and golden_index is not None:
|
|
249
|
+
# number of data points since the golden event
|
|
250
|
+
if len(blocks) - golden_index <= 2 and last_alignment_golden:
|
|
251
|
+
self._transition_detected = True
|
|
252
|
+
|
|
253
|
+
# persist last dead price for external visibility
|
|
254
|
+
self._last_dead_price = last_dead_price
|
|
255
|
+
|
|
256
|
+
return signals
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
authors = [
|
|
3
|
+
{"name" = "프로그램동산","email" = "coding@programgarden.com"}
|
|
4
|
+
]
|
|
5
|
+
homepage = "https://programgarden.com"
|
|
6
|
+
requires-python = ">=3.9"
|
|
7
|
+
name = "programgarden-community"
|
|
8
|
+
version = "0.1.1"
|
|
9
|
+
description = "증권 분석에 필요한 외부 전략 모아둔 플러그인"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
|
|
12
|
+
[tool.poetry]
|
|
13
|
+
packages = [
|
|
14
|
+
{ include = "programgarden_community" }
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[tool.poetry.dependencies]
|
|
18
|
+
pydantic = ">=2.11.7,<3.0.0"
|
|
19
|
+
programgarden-finance = "^0.1.0"
|
|
20
|
+
programgarden-core = "^0.1.0"
|
|
21
|
+
croniter = "^6.0.0"
|
|
22
|
+
python-dotenv = "^1.1.1"
|
|
23
|
+
|
|
24
|
+
[build-system]
|
|
25
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
26
|
+
build-backend = "poetry.core.masonry.api"
|